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:
parent
52b85f085e
commit
3816d6841d
|
|
@ -2,25 +2,25 @@ import { apiClient } from '@/infrastructure/api/api-client';
|
||||||
import type { HookScriptDto } from '@/application/dto/agent-config.dto';
|
import type { HookScriptDto } from '@/application/dto/agent-config.dto';
|
||||||
|
|
||||||
export async function getHooks(): Promise<HookScriptDto[]> {
|
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> {
|
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',
|
method: 'PUT',
|
||||||
body: hook,
|
body: hook,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createHook(hook: Omit<HookScriptDto, 'id'>): Promise<HookScriptDto> {
|
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',
|
method: 'POST',
|
||||||
body: hook,
|
body: hook,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteHook(hookId: string): Promise<void> {
|
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',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { apiClient } from '@/infrastructure/api/api-client';
|
import { apiClient } from '@/infrastructure/api/api-client';
|
||||||
|
|
||||||
export async function switchEngine(engineType: string): Promise<void> {
|
export async function switchEngine(engineType: string): Promise<void> {
|
||||||
await apiClient('/api/v1/agent/config/engine', {
|
await apiClient('/api/v1/agent-config/engine', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { engineType },
|
body: { engineType },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { apiClient } from '@/infrastructure/api/api-client';
|
import { apiClient } from '@/infrastructure/api/api-client';
|
||||||
|
|
||||||
export async function updateSystemPrompt(prompt: string): Promise<void> {
|
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',
|
method: 'PUT',
|
||||||
body: { prompt },
|
body: { prompt },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,41 @@ import type { AgentConfigDto } from '@/application/dto/agent-config.dto';
|
||||||
|
|
||||||
export const apiAgentConfigRepository: AgentConfigRepository = {
|
export const apiAgentConfigRepository: AgentConfigRepository = {
|
||||||
async getConfig() {
|
async getConfig() {
|
||||||
return apiClient<AgentConfigDto>('/api/v1/agent/config');
|
return apiClient<AgentConfigDto>('/api/v1/agent-config');
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveConfig(config) {
|
async saveConfig(config) {
|
||||||
await apiClient('/api/v1/agent/config', {
|
await apiClient('/api/v1/agent-config', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: config,
|
body: config,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async switchEngine(engineType) {
|
async switchEngine(engineType) {
|
||||||
await apiClient('/api/v1/agent/config/engine', {
|
await apiClient('/api/v1/agent-config/engine', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { engineType },
|
body: { engineType },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getSystemPrompt() {
|
async getSystemPrompt() {
|
||||||
const config = await apiClient<AgentConfigDto>('/api/v1/agent/config');
|
const config = await apiClient<AgentConfigDto>('/api/v1/agent-config');
|
||||||
return config.systemPrompt ?? '';
|
return config.systemPrompt ?? '';
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateSystemPrompt(prompt) {
|
async updateSystemPrompt(prompt) {
|
||||||
await apiClient('/api/v1/agent/config/system-prompt', {
|
await apiClient('/api/v1/agent-config/system-prompt', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { prompt },
|
body: { prompt },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHooks() {
|
async getHooks() {
|
||||||
return apiClient('/api/v1/agent/config/hooks');
|
return apiClient('/api/v1/agent-config/hooks');
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateHook(hook) {
|
async updateHook(hook) {
|
||||||
await apiClient(`/api/v1/agent/config/hooks/${hook.id}`, {
|
await apiClient(`/api/v1/agent-config/hooks/${hook.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: hook,
|
body: hook,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ services:
|
||||||
paths:
|
paths:
|
||||||
- /api/v1/auth
|
- /api/v1/auth
|
||||||
strip_path: false
|
strip_path: false
|
||||||
|
- name: admin-routes
|
||||||
|
paths:
|
||||||
|
- /api/v1/admin
|
||||||
|
strip_path: false
|
||||||
|
|
||||||
- name: agent-service
|
- name: agent-service
|
||||||
url: http://agent-service:3002
|
url: http://agent-service:3002
|
||||||
|
|
@ -195,6 +199,13 @@ plugins:
|
||||||
claims_to_verify:
|
claims_to_verify:
|
||||||
- exp
|
- exp
|
||||||
|
|
||||||
|
- name: jwt
|
||||||
|
route: admin-routes
|
||||||
|
config:
|
||||||
|
key_claim_name: kid
|
||||||
|
claims_to_verify:
|
||||||
|
- exp
|
||||||
|
|
||||||
# ===== Route-specific overrides =====
|
# ===== Route-specific overrides =====
|
||||||
- name: rate-limiting
|
- name: rate-limiting
|
||||||
route: agent-ws
|
route: agent-ws
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { PassportModule } from '@nestjs/passport';
|
||||||
import { DatabaseModule, TenantProvisioningService } from '@it0/database';
|
import { DatabaseModule, TenantProvisioningService } from '@it0/database';
|
||||||
import { AuthController } from './interfaces/rest/controllers/auth.controller';
|
import { AuthController } from './interfaces/rest/controllers/auth.controller';
|
||||||
import { TenantController } from './interfaces/rest/controllers/tenant.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 { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
|
||||||
import { RbacGuard } from './infrastructure/guards/rbac.guard';
|
import { RbacGuard } from './infrastructure/guards/rbac.guard';
|
||||||
import { AuthService } from './application/services/auth.service';
|
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: [
|
providers: [
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RbacGuard,
|
RbacGuard,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Logger,
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { RolesGuard, Roles } from '@it0/common';
|
import { RolesGuard, Roles } from '@it0/common';
|
||||||
|
|
@ -18,7 +17,7 @@ import { Tenant } from '../../../domain/entities/tenant.entity';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
@Controller('api/v1/admin/tenants')
|
@Controller('api/v1/admin/tenants')
|
||||||
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
@UseGuards(RolesGuard)
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
export class TenantController {
|
export class TenantController {
|
||||||
private readonly logger = new Logger(TenantController.name);
|
private readonly logger = new Logger(TenantController.name);
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue