fix: add users endpoint, admin route, and fix agent-config paths

- Add UsersController to auth-service for user CRUD (GET/POST/PUT/DELETE /api/v1/auth/users)
- Add Kong route /api/v1/admin -> auth-service for tenant management
- Remove AuthGuard from TenantController (Kong handles JWT)
- Fix frontend agent-config API paths from /api/v1/agent/config to /api/v1/agent-config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-22 00:35:57 -08:00
parent 52b85f085e
commit 3816d6841d
8 changed files with 166 additions and 16 deletions

View File

@ -2,25 +2,25 @@ import { apiClient } from '@/infrastructure/api/api-client';
import type { HookScriptDto } from '@/application/dto/agent-config.dto';
export async function getHooks(): Promise<HookScriptDto[]> {
return apiClient<HookScriptDto[]>('/api/v1/agent/config/hooks');
return apiClient<HookScriptDto[]>('/api/v1/agent-config/hooks');
}
export async function updateHook(hook: HookScriptDto): Promise<void> {
await apiClient(`/api/v1/agent/config/hooks/${hook.id}`, {
await apiClient(`/api/v1/agent-config/hooks/${hook.id}`, {
method: 'PUT',
body: hook,
});
}
export async function createHook(hook: Omit<HookScriptDto, 'id'>): Promise<HookScriptDto> {
return apiClient<HookScriptDto>('/api/v1/agent/config/hooks', {
return apiClient<HookScriptDto>('/api/v1/agent-config/hooks', {
method: 'POST',
body: hook,
});
}
export async function deleteHook(hookId: string): Promise<void> {
await apiClient(`/api/v1/agent/config/hooks/${hookId}`, {
await apiClient(`/api/v1/agent-config/hooks/${hookId}`, {
method: 'DELETE',
});
}

View File

@ -1,7 +1,7 @@
import { apiClient } from '@/infrastructure/api/api-client';
export async function switchEngine(engineType: string): Promise<void> {
await apiClient('/api/v1/agent/config/engine', {
await apiClient('/api/v1/agent-config/engine', {
method: 'PUT',
body: { engineType },
});

View File

@ -1,7 +1,7 @@
import { apiClient } from '@/infrastructure/api/api-client';
export async function updateSystemPrompt(prompt: string): Promise<void> {
await apiClient('/api/v1/agent/config/system-prompt', {
await apiClient('/api/v1/agent-config/system-prompt', {
method: 'PUT',
body: { prompt },
});

View File

@ -4,41 +4,41 @@ import type { AgentConfigDto } from '@/application/dto/agent-config.dto';
export const apiAgentConfigRepository: AgentConfigRepository = {
async getConfig() {
return apiClient<AgentConfigDto>('/api/v1/agent/config');
return apiClient<AgentConfigDto>('/api/v1/agent-config');
},
async saveConfig(config) {
await apiClient('/api/v1/agent/config', {
await apiClient('/api/v1/agent-config', {
method: 'PUT',
body: config,
});
},
async switchEngine(engineType) {
await apiClient('/api/v1/agent/config/engine', {
await apiClient('/api/v1/agent-config/engine', {
method: 'PUT',
body: { engineType },
});
},
async getSystemPrompt() {
const config = await apiClient<AgentConfigDto>('/api/v1/agent/config');
const config = await apiClient<AgentConfigDto>('/api/v1/agent-config');
return config.systemPrompt ?? '';
},
async updateSystemPrompt(prompt) {
await apiClient('/api/v1/agent/config/system-prompt', {
await apiClient('/api/v1/agent-config/system-prompt', {
method: 'PUT',
body: { prompt },
});
},
async getHooks() {
return apiClient('/api/v1/agent/config/hooks');
return apiClient('/api/v1/agent-config/hooks');
},
async updateHook(hook) {
await apiClient(`/api/v1/agent/config/hooks/${hook.id}`, {
await apiClient(`/api/v1/agent-config/hooks/${hook.id}`, {
method: 'PUT',
body: hook,
});

View File

@ -15,6 +15,10 @@ services:
paths:
- /api/v1/auth
strip_path: false
- name: admin-routes
paths:
- /api/v1/admin
strip_path: false
- name: agent-service
url: http://agent-service:3002
@ -195,6 +199,13 @@ plugins:
claims_to_verify:
- exp
- name: jwt
route: admin-routes
config:
key_claim_name: kid
claims_to_verify:
- exp
# ===== Route-specific overrides =====
- name: rate-limiting
route: agent-ws

View File

@ -6,6 +6,7 @@ import { PassportModule } from '@nestjs/passport';
import { DatabaseModule, TenantProvisioningService } from '@it0/database';
import { AuthController } from './interfaces/rest/controllers/auth.controller';
import { TenantController } from './interfaces/rest/controllers/tenant.controller';
import { UserController } from './interfaces/rest/controllers/user.controller';
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
import { RbacGuard } from './infrastructure/guards/rbac.guard';
import { AuthService } from './application/services/auth.service';
@ -29,7 +30,7 @@ import { Tenant } from './domain/entities/tenant.entity';
}),
}),
],
controllers: [AuthController, TenantController],
controllers: [AuthController, TenantController, UserController],
providers: [
JwtStrategy,
RbacGuard,

View File

@ -9,7 +9,6 @@ import {
NotFoundException,
Logger,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RolesGuard, Roles } from '@it0/common';
@ -18,7 +17,7 @@ import { Tenant } from '../../../domain/entities/tenant.entity';
import * as crypto from 'crypto';
@Controller('api/v1/admin/tenants')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@UseGuards(RolesGuard)
@Roles('admin')
export class TenantController {
private readonly logger = new Logger(TenantController.name);

View File

@ -0,0 +1,139 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
UseGuards,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RolesGuard, Roles } from '@it0/common';
import { User } from '../../../domain/entities/user.entity';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
@Controller('api/v1/auth/users')
@UseGuards(RolesGuard)
@Roles('admin')
export class UserController {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
@Get()
async listUsers() {
const users = await this.userRepository.find({
order: { createdAt: 'DESC' },
});
const data = users.map((u) => ({
id: u.id,
displayName: u.name,
email: u.email,
role: u.roles?.[0] ?? 'viewer',
tenantId: u.tenantId,
tenantName: u.tenantId,
status: u.isActive ? 'active' : 'disabled',
lastLoginAt: u.lastLoginAt?.toISOString() ?? null,
createdAt: u.createdAt.toISOString(),
}));
return { data, total: data.length };
}
@Get(':id')
async getUser(@Param('id') id: string) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) throw new NotFoundException(`User "${id}" not found`);
return {
id: user.id,
displayName: user.name,
email: user.email,
role: user.roles?.[0] ?? 'viewer',
tenantId: user.tenantId,
tenantName: user.tenantId,
status: user.isActive ? 'active' : 'disabled',
lastLoginAt: user.lastLoginAt?.toISOString() ?? null,
createdAt: user.createdAt.toISOString(),
};
}
@Post()
async createUser(
@Body()
body: {
displayName: string;
email: string;
password: string;
role?: string;
tenantId?: string;
},
) {
const existing = await this.userRepository.findOne({
where: { email: body.email },
});
if (existing) throw new ConflictException('Email already registered');
const passwordHash = await bcrypt.hash(body.password, 12);
const user = this.userRepository.create({
id: crypto.randomUUID(),
tenantId: body.tenantId || 'default',
email: body.email,
passwordHash,
name: body.displayName,
roles: [body.role || 'viewer'],
isActive: true,
});
const saved = await this.userRepository.save(user);
return {
id: saved.id,
displayName: saved.name,
email: saved.email,
role: saved.roles?.[0] ?? 'viewer',
tenantId: saved.tenantId,
status: 'active',
createdAt: saved.createdAt.toISOString(),
};
}
@Put(':id')
async updateUser(
@Param('id') id: string,
@Body() body: { role?: string; status?: string },
) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) throw new NotFoundException(`User "${id}" not found`);
if (body.role) user.roles = [body.role];
if (body.status) user.isActive = body.status === 'active';
const saved = await this.userRepository.save(user);
return {
id: saved.id,
displayName: saved.name,
email: saved.email,
role: saved.roles?.[0] ?? 'viewer',
tenantId: saved.tenantId,
status: saved.isActive ? 'active' : 'disabled',
};
}
@Delete(':id')
async deleteUser(@Param('id') id: string) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) throw new NotFoundException(`User "${id}" not found`);
await this.userRepository.remove(user);
return { success: true };
}
}