All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
297 lines
7.9 KiB
TypeScript
297 lines
7.9 KiB
TypeScript
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<FleetProviderResponse[]> {
|
|
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<FleetProviderResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.assertProviderOwnership(userId, providerId);
|
|
|
|
await this.prisma.llmProvider.delete({
|
|
where: { id: providerId },
|
|
});
|
|
}
|
|
|
|
// --- User Agent Config ---
|
|
|
|
async getAgentConfig(userId: string): Promise<FleetAgentConfigResponse> {
|
|
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<void> {
|
|
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<OidcConfigResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.prisma.systemConfig.deleteMany({
|
|
where: {
|
|
key: {
|
|
in: [...OIDC_KEYS],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// --- Breakglass (admin only) ---
|
|
|
|
async resetBreakglassPassword(
|
|
username: ResetPasswordDto["username"],
|
|
newPassword: ResetPasswordDto["newPassword"]
|
|
): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.prisma.systemConfig.upsert({
|
|
where: { key },
|
|
update: { value, encrypted },
|
|
create: { key, value, encrypted },
|
|
});
|
|
}
|
|
|
|
private normalizeJsonArray(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
}
|