From 3816d6841dccd7261bb7daadcf4635148c0ecab4 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 22 Feb 2026 00:35:57 -0800 Subject: [PATCH] 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 --- .../use-cases/agent-config/manage-hooks.ts | 8 +- .../use-cases/agent-config/switch-engine.ts | 2 +- .../agent-config/update-system-prompt.ts | 2 +- .../api-agent-config.repository.ts | 14 +- packages/gateway/config/kong.yml | 11 ++ .../services/auth-service/src/auth.module.ts | 3 +- .../rest/controllers/tenant.controller.ts | 3 +- .../rest/controllers/user.controller.ts | 139 ++++++++++++++++++ 8 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 packages/services/auth-service/src/interfaces/rest/controllers/user.controller.ts diff --git a/it0-web-admin/src/application/use-cases/agent-config/manage-hooks.ts b/it0-web-admin/src/application/use-cases/agent-config/manage-hooks.ts index f4388d9..22f1a4c 100644 --- a/it0-web-admin/src/application/use-cases/agent-config/manage-hooks.ts +++ b/it0-web-admin/src/application/use-cases/agent-config/manage-hooks.ts @@ -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 { - return apiClient('/api/v1/agent/config/hooks'); + return apiClient('/api/v1/agent-config/hooks'); } export async function updateHook(hook: HookScriptDto): Promise { - 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): Promise { - return apiClient('/api/v1/agent/config/hooks', { + return apiClient('/api/v1/agent-config/hooks', { method: 'POST', body: hook, }); } export async function deleteHook(hookId: string): Promise { - await apiClient(`/api/v1/agent/config/hooks/${hookId}`, { + await apiClient(`/api/v1/agent-config/hooks/${hookId}`, { method: 'DELETE', }); } diff --git a/it0-web-admin/src/application/use-cases/agent-config/switch-engine.ts b/it0-web-admin/src/application/use-cases/agent-config/switch-engine.ts index ebd0378..ce0b4da 100644 --- a/it0-web-admin/src/application/use-cases/agent-config/switch-engine.ts +++ b/it0-web-admin/src/application/use-cases/agent-config/switch-engine.ts @@ -1,7 +1,7 @@ import { apiClient } from '@/infrastructure/api/api-client'; export async function switchEngine(engineType: string): Promise { - await apiClient('/api/v1/agent/config/engine', { + await apiClient('/api/v1/agent-config/engine', { method: 'PUT', body: { engineType }, }); diff --git a/it0-web-admin/src/application/use-cases/agent-config/update-system-prompt.ts b/it0-web-admin/src/application/use-cases/agent-config/update-system-prompt.ts index f51aeb2..cf16fbf 100644 --- a/it0-web-admin/src/application/use-cases/agent-config/update-system-prompt.ts +++ b/it0-web-admin/src/application/use-cases/agent-config/update-system-prompt.ts @@ -1,7 +1,7 @@ import { apiClient } from '@/infrastructure/api/api-client'; export async function updateSystemPrompt(prompt: string): Promise { - await apiClient('/api/v1/agent/config/system-prompt', { + await apiClient('/api/v1/agent-config/system-prompt', { method: 'PUT', body: { prompt }, }); diff --git a/it0-web-admin/src/infrastructure/repositories/api-agent-config.repository.ts b/it0-web-admin/src/infrastructure/repositories/api-agent-config.repository.ts index ecc6e45..4fa6d2d 100644 --- a/it0-web-admin/src/infrastructure/repositories/api-agent-config.repository.ts +++ b/it0-web-admin/src/infrastructure/repositories/api-agent-config.repository.ts @@ -4,41 +4,41 @@ import type { AgentConfigDto } from '@/application/dto/agent-config.dto'; export const apiAgentConfigRepository: AgentConfigRepository = { async getConfig() { - return apiClient('/api/v1/agent/config'); + return apiClient('/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('/api/v1/agent/config'); + const config = await apiClient('/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, }); diff --git a/packages/gateway/config/kong.yml b/packages/gateway/config/kong.yml index de6d8bc..a219503 100644 --- a/packages/gateway/config/kong.yml +++ b/packages/gateway/config/kong.yml @@ -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 diff --git a/packages/services/auth-service/src/auth.module.ts b/packages/services/auth-service/src/auth.module.ts index c1e5d1e..c538dbd 100644 --- a/packages/services/auth-service/src/auth.module.ts +++ b/packages/services/auth-service/src/auth.module.ts @@ -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, diff --git a/packages/services/auth-service/src/interfaces/rest/controllers/tenant.controller.ts b/packages/services/auth-service/src/interfaces/rest/controllers/tenant.controller.ts index 84befaf..9692078 100644 --- a/packages/services/auth-service/src/interfaces/rest/controllers/tenant.controller.ts +++ b/packages/services/auth-service/src/interfaces/rest/controllers/tenant.controller.ts @@ -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); diff --git a/packages/services/auth-service/src/interfaces/rest/controllers/user.controller.ts b/packages/services/auth-service/src/interfaces/rest/controllers/user.controller.ts new file mode 100644 index 0000000..2544be7 --- /dev/null +++ b/packages/services/auth-service/src/interfaces/rest/controllers/user.controller.ts @@ -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, + ) {} + + @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 }; + } +}