feat: Google Gemini image generation for project covers

Schema:
- SystemSettings: geminiApiKey, geminiModel, imageProvider fields
- imageProvider: "dalle" (default) or "gemini"

Gemini Client (packages/api/src/gemini-client.ts):
- Direct HTTP call to Gemini REST API with responseModalities: [TEXT, IMAGE]
- Returns base64 data URL
- Error parsing with user-friendly messages

Router (project.ts):
- generateCover: routes to DALL-E or Gemini based on imageProvider setting
- New isImageGenConfigured query returning { configured, provider }

Admin UI (SystemSettingsClient.tsx):
- "Image Generation" section with provider radio buttons (DALL-E / Gemini)
- Conditional fields: DALL-E config or Gemini API key + model
- Separate save button for image settings

Security:
- geminiApiKey sanitized in audit logs (SENSITIVE_FIELDS)
- API key stored server-side only, never sent to client

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-23 15:02:35 +01:00
parent 52d425043b
commit 502ecba9e9
6 changed files with 339 additions and 64 deletions
+19
View File
@@ -12,6 +12,7 @@ const SENSITIVE_FIELDS = new Set([
"smtpPassword",
"azureDalleApiKey",
"anonymizationSeed",
"geminiApiKey",
]);
export const settingsRouter = createTRPCRouter({
@@ -55,6 +56,11 @@ export const settingsRouter = createTRPCRouter({
azureDalleDeployment: settings?.azureDalleDeployment ?? null,
azureDalleEndpoint: settings?.azureDalleEndpoint ?? null,
hasDalleApiKey: !!settings?.azureDalleApiKey,
// Gemini
geminiModel: settings?.geminiModel ?? "gemini-2.0-flash-preview-image-generation",
hasGeminiApiKey: !!settings?.geminiApiKey,
// Image provider
imageProvider: settings?.imageProvider ?? "dalle",
// Vacation defaults
vacationDefaultDays: settings?.vacationDefaultDays ?? 28,
// Timeline
@@ -103,6 +109,11 @@ export const settingsRouter = createTRPCRouter({
azureDalleDeployment: z.string().optional(),
azureDalleEndpoint: z.string().url().optional().or(z.literal("")),
azureDalleApiKey: z.string().optional(),
// Gemini image generation
geminiApiKey: z.string().optional(),
geminiModel: z.string().optional(),
// Image provider selection
imageProvider: z.enum(["dalle", "gemini"]).optional(),
// Vacation
vacationDefaultDays: z.number().int().min(0).max(365).optional(),
// Timeline
@@ -155,6 +166,14 @@ export const settingsRouter = createTRPCRouter({
data.azureDalleEndpoint = input.azureDalleEndpoint || null;
if (input.azureDalleApiKey !== undefined)
data.azureDalleApiKey = input.azureDalleApiKey || null;
// Gemini
if (input.geminiApiKey !== undefined)
data.geminiApiKey = input.geminiApiKey || null;
if (input.geminiModel !== undefined)
data.geminiModel = input.geminiModel || null;
// Image provider
if (input.imageProvider !== undefined)
data.imageProvider = input.imageProvider;
// Vacation
if (input.vacationDefaultDays !== undefined) data.vacationDefaultDays = input.vacationDefaultDays;
// Timeline