126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
"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<HetznerKey, string>
|
|
>;
|
|
|
|
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<typeof hetznerConfigSchema>
|
|
): 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<HetznerKey, string>
|
|
>;
|
|
|
|
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 };
|
|
}
|