import { Injectable, NotFoundException } from "@nestjs/common"; import { hash } from "bcryptjs"; import type { Prisma } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import { CryptoService } from "../crypto/crypto.service"; import type { CreateProviderDto, ResetPasswordDto, UpdateAgentConfigDto, UpdateOidcDto, UpdateProviderDto, } from "./fleet-settings.dto"; const BCRYPT_ROUNDS = 12; const DEFAULT_PROVIDER_API_TYPE = "openai-completions"; const OIDC_ISSUER_KEY = "oidc.issuerUrl"; const OIDC_CLIENT_ID_KEY = "oidc.clientId"; const OIDC_CLIENT_SECRET_KEY = "oidc.clientSecret"; const OIDC_KEYS = [OIDC_ISSUER_KEY, OIDC_CLIENT_ID_KEY, OIDC_CLIENT_SECRET_KEY] as const; export interface FleetProviderResponse { id: string; name: string; displayName: string; type: string; baseUrl: string | null; isActive: boolean; models: unknown; } export interface FleetAgentConfigResponse { primaryModel: string | null; fallbackModels: unknown[]; personality: string | null; } export interface OidcConfigResponse { issuerUrl?: string; clientId?: string; configured: boolean; } @Injectable() export class FleetSettingsService { constructor( private readonly prisma: PrismaService, private readonly crypto: CryptoService ) {} // --- LLM Provider CRUD (per-user scoped) --- async listProviders(userId: string): Promise { return this.prisma.llmProvider.findMany({ where: { userId }, select: { id: true, name: true, displayName: true, type: true, baseUrl: true, isActive: true, models: true, }, orderBy: { createdAt: "asc" }, }); } async getProvider(userId: string, providerId: string): Promise { const provider = await this.prisma.llmProvider.findFirst({ where: { id: providerId, userId, }, select: { id: true, name: true, displayName: true, type: true, baseUrl: true, isActive: true, models: true, }, }); if (!provider) { throw new NotFoundException(`Provider ${providerId} not found`); } return provider; } async createProvider(userId: string, data: CreateProviderDto): Promise<{ id: string }> { const provider = await this.prisma.llmProvider.create({ data: { userId, name: data.name, displayName: data.displayName, type: data.type, baseUrl: data.baseUrl ?? null, apiKey: data.apiKey ? this.crypto.encrypt(data.apiKey) : null, apiType: data.apiType ?? DEFAULT_PROVIDER_API_TYPE, models: (data.models ?? []) as Prisma.InputJsonValue, }, select: { id: true, }, }); return provider; } async updateProvider(userId: string, providerId: string, data: UpdateProviderDto): Promise { await this.assertProviderOwnership(userId, providerId); const updateData: Prisma.LlmProviderUpdateInput = {}; if (data.displayName !== undefined) { updateData.displayName = data.displayName; } if (data.baseUrl !== undefined) { updateData.baseUrl = data.baseUrl; } if (data.isActive !== undefined) { updateData.isActive = data.isActive; } if (data.models !== undefined) { updateData.models = data.models as Prisma.InputJsonValue; } if (data.apiKey !== undefined) { updateData.apiKey = data.apiKey.length > 0 ? this.crypto.encrypt(data.apiKey) : null; } await this.prisma.llmProvider.update({ where: { id: providerId }, data: updateData, }); } async deleteProvider(userId: string, providerId: string): Promise { await this.assertProviderOwnership(userId, providerId); await this.prisma.llmProvider.delete({ where: { id: providerId }, }); } // --- User Agent Config --- async getAgentConfig(userId: string): Promise { const config = await this.prisma.userAgentConfig.findUnique({ where: { userId }, select: { primaryModel: true, fallbackModels: true, personality: true, }, }); if (!config) { return { primaryModel: null, fallbackModels: [], personality: null, }; } return { primaryModel: config.primaryModel, fallbackModels: this.normalizeJsonArray(config.fallbackModels), personality: config.personality, }; } async updateAgentConfig(userId: string, data: UpdateAgentConfigDto): Promise { const updateData: Prisma.UserAgentConfigUpdateInput = {}; if (data.primaryModel !== undefined) { updateData.primaryModel = data.primaryModel; } if (data.personality !== undefined) { updateData.personality = data.personality; } if (data.fallbackModels !== undefined) { updateData.fallbackModels = data.fallbackModels as Prisma.InputJsonValue; } const createData: Prisma.UserAgentConfigCreateInput = { userId, fallbackModels: (data.fallbackModels ?? []) as Prisma.InputJsonValue, ...(data.primaryModel !== undefined ? { primaryModel: data.primaryModel } : {}), ...(data.personality !== undefined ? { personality: data.personality } : {}), }; await this.prisma.userAgentConfig.upsert({ where: { userId }, create: createData, update: updateData, }); } // --- OIDC Config (admin only) --- async getOidcConfig(): Promise { const entries = await this.prisma.systemConfig.findMany({ where: { key: { in: [...OIDC_KEYS], }, }, select: { key: true, value: true, }, }); const byKey = new Map(entries.map((entry) => [entry.key, entry.value])); const issuerUrl = byKey.get(OIDC_ISSUER_KEY); const clientId = byKey.get(OIDC_CLIENT_ID_KEY); const hasSecret = byKey.has(OIDC_CLIENT_SECRET_KEY); return { ...(issuerUrl ? { issuerUrl } : {}), ...(clientId ? { clientId } : {}), configured: Boolean(issuerUrl && clientId && hasSecret), }; } async updateOidcConfig(data: UpdateOidcDto): Promise { const encryptedSecret = this.crypto.encrypt(data.clientSecret); await Promise.all([ this.upsertSystemConfig(OIDC_ISSUER_KEY, data.issuerUrl, false), this.upsertSystemConfig(OIDC_CLIENT_ID_KEY, data.clientId, false), this.upsertSystemConfig(OIDC_CLIENT_SECRET_KEY, encryptedSecret, true), ]); } async deleteOidcConfig(): Promise { await this.prisma.systemConfig.deleteMany({ where: { key: { in: [...OIDC_KEYS], }, }, }); } // --- Breakglass (admin only) --- async resetBreakglassPassword( username: ResetPasswordDto["username"], newPassword: ResetPasswordDto["newPassword"] ): Promise { const user = await this.prisma.breakglassUser.findUnique({ where: { username }, select: { id: true }, }); if (!user) { throw new NotFoundException(`Breakglass user ${username} not found`); } const passwordHash = await hash(newPassword, BCRYPT_ROUNDS); await this.prisma.breakglassUser.update({ where: { id: user.id }, data: { passwordHash }, }); } private async assertProviderOwnership(userId: string, providerId: string): Promise { const provider = await this.prisma.llmProvider.findFirst({ where: { id: providerId, userId, }, select: { id: true, }, }); if (!provider) { throw new NotFoundException(`Provider ${providerId} not found`); } } private async upsertSystemConfig(key: string, value: string, encrypted: boolean): Promise { await this.prisma.systemConfig.upsert({ where: { key }, update: { value, encrypted }, create: { key, value, encrypted }, }); } private normalizeJsonArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } }