"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { S3Client, PutBucketCorsCommand } from "@aws-sdk/client-s3"; const HETZNER_KEYS = [ "hetzner_endpoint", "hetzner_access_key", "hetzner_secret_key", "hetzner_bucket_name", ] as const; type HetznerKey = (typeof HETZNER_KEYS)[number]; async function requireAdmin() { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); if (session.user.role !== "ADMIN") throw new Error("Admin access required"); return session; } /** Returns the current Hetzner config values (secret key masked for display). */ export async function getHetznerConfig(): Promise<{ hetzner_endpoint: string; hetzner_access_key: string; hetzner_secret_key: string; hetzner_bucket_name: string; }> { await requireAdmin(); const rows = await db.systemConfig.findMany({ where: { key: { in: [...HETZNER_KEYS] } }, }); const map = Object.fromEntries(rows.map((r) => [r.key, r.value])) as Partial< Record >; return { hetzner_endpoint: map.hetzner_endpoint ?? process.env.HETZNER_ENDPOINT ?? "", hetzner_access_key: map.hetzner_access_key ?? process.env.HETZNER_ACCESS_KEY ?? "", hetzner_secret_key: map.hetzner_secret_key ?? process.env.HETZNER_SECRET_KEY ?? "", hetzner_bucket_name: map.hetzner_bucket_name ?? process.env.HETZNER_BUCKET_NAME ?? "", }; } const hetznerConfigSchema = z.object({ hetzner_endpoint: z.string().url("Must be a valid URL").or(z.literal("")), hetzner_access_key: z.string().min(1, "Required"), hetzner_secret_key: z.string().min(1, "Required"), hetzner_bucket_name: z.string().min(1, "Required"), }); /** Upserts all four Hetzner config values into system_config. */ export async function saveHetznerConfig( data: z.infer ): Promise<{ success: true }> { await requireAdmin(); const parsed = hetznerConfigSchema.parse(data); await db.$transaction( (Object.entries(parsed) as [HetznerKey, string][]).map(([key, value]) => db.systemConfig.upsert({ where: { key }, create: { key, value }, update: { value }, }) ) ); revalidatePath("/settings"); return { success: true }; } /** * Applies a CORS policy to the Hetzner bucket that allows direct browser * PUT uploads (used by batch-upload presigned URL flow). */ export async function configureHetznerCors(): Promise<{ success: true }> { await requireAdmin(); const rows = await db.systemConfig.findMany({ where: { key: { in: [...HETZNER_KEYS] } }, }); const map = Object.fromEntries(rows.map((r) => [r.key, r.value])) as Partial< Record >; const endpoint = map.hetzner_endpoint ?? process.env.HETZNER_ENDPOINT ?? ""; const accessKey = map.hetzner_access_key ?? process.env.HETZNER_ACCESS_KEY ?? ""; const secretKey = map.hetzner_secret_key ?? process.env.HETZNER_SECRET_KEY ?? ""; const bucket = map.hetzner_bucket_name ?? process.env.HETZNER_BUCKET_NAME ?? ""; if (!endpoint || !accessKey || !secretKey || !bucket) { throw new Error("Hetzner storage is not fully configured. Save credentials first."); } const client = new S3Client({ region: "auto", endpoint, credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, }); await client.send( new PutBucketCorsCommand({ Bucket: bucket, CORSConfiguration: { CORSRules: [ { AllowedOrigins: ["*"], AllowedMethods: ["PUT", "GET"], AllowedHeaders: ["*"], MaxAgeSeconds: 3600, }, ], }, }) ); return { success: true }; }