mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 21:41:16 +00:00
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
"use server"
|
||
|
||
import { randomBytes } from "node:crypto"
|
||
import * as z from "zod"
|
||
import { revalidatePath } from "next/cache"
|
||
import { redirect } from "next/navigation"
|
||
import { createClient } from "@/lib/supabase/server"
|
||
import { verifySession } from "@/lib/supabase/dal"
|
||
import { resolveContent } from "../lib/content"
|
||
import { SHARE_EXPIRY_OPTIONS } from "../lib/preview"
|
||
import { getTemplate } from "../lib/registry"
|
||
|
||
export type VariantActionResult = { error: string } | { success: true }
|
||
export type ShareResult = { error: string } | { token: string | null; expiresAt: string | null }
|
||
|
||
const IdSchema = z.string().uuid()
|
||
|
||
const SaveSchema = z.object({
|
||
name: z.string().trim().min(1, { error: "Bitte einen Namen für die Variante eingeben." }).max(120),
|
||
clientId: z.string().uuid().nullable(),
|
||
content: z.unknown(),
|
||
})
|
||
|
||
const ShareSchema = z.object({
|
||
enabled: z.boolean(),
|
||
expiresInDays: z.number().refine((days) => SHARE_EXPIRY_OPTIONS.some((option) => option.days === days)),
|
||
regenerate: z.boolean().optional(),
|
||
})
|
||
|
||
async function requireAdmin() {
|
||
const session = await verifySession()
|
||
return session.isAdmin ? session : null
|
||
}
|
||
|
||
function firstIssue(error: z.ZodError) {
|
||
return error.issues[0]?.message ?? "Ungültige Eingabe."
|
||
}
|
||
|
||
// Nicht erratbar, URL-tauglich: 24 Byte ergeben 32 Zeichen
|
||
function newShareToken() {
|
||
return randomBytes(24).toString("base64url")
|
||
}
|
||
|
||
function revalidateTemplates() {
|
||
revalidatePath("/admin/templates", "layout")
|
||
}
|
||
|
||
export async function createVariant(templateKey: string): Promise<{ error: string } | undefined> {
|
||
if (!(await requireAdmin())) return { error: "Keine Berechtigung." }
|
||
|
||
const def = getTemplate(templateKey)
|
||
if (!def) return { error: "Vorlage nicht gefunden." }
|
||
|
||
const supabase = await createClient()
|
||
const { data, error } = await supabase
|
||
.from("template_variants")
|
||
.insert({ template_key: def.key, name: `${def.name} – Entwurf`, content: resolveContent(def, {}) })
|
||
.select("id")
|
||
.single()
|
||
if (error || !data) return { error: "Variante konnte nicht angelegt werden." }
|
||
|
||
revalidateTemplates()
|
||
redirect(`/admin/templates/${data.id}`)
|
||
}
|
||
|
||
export async function saveVariant(id: string, input: z.input<typeof SaveSchema>): Promise<VariantActionResult> {
|
||
if (!(await requireAdmin())) return { error: "Keine Berechtigung." }
|
||
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Variante." }
|
||
|
||
const parsed = SaveSchema.safeParse(input)
|
||
if (!parsed.success) return { error: firstIssue(parsed.error) }
|
||
|
||
const supabase = await createClient()
|
||
const { data: current } = await supabase.from("template_variants").select("template_key").eq("id", id).maybeSingle()
|
||
const def = current ? getTemplate(current.template_key) : undefined
|
||
if (!def) return { error: "Variante nicht gefunden." }
|
||
|
||
const { error } = await supabase
|
||
.from("template_variants")
|
||
.update({
|
||
name: parsed.data.name,
|
||
client_id: parsed.data.clientId,
|
||
content: resolveContent(def, parsed.data.content),
|
||
})
|
||
.eq("id", id)
|
||
if (error) return { error: "Änderungen konnten nicht gespeichert werden." }
|
||
|
||
revalidateTemplates()
|
||
return { success: true }
|
||
}
|
||
|
||
export async function updateShare(id: string, input: z.input<typeof ShareSchema>): Promise<ShareResult> {
|
||
if (!(await requireAdmin())) return { error: "Keine Berechtigung." }
|
||
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Variante." }
|
||
|
||
const parsed = ShareSchema.safeParse(input)
|
||
if (!parsed.success) return { error: "Ungültige Freigabe-Einstellung." }
|
||
const { enabled, expiresInDays, regenerate } = parsed.data
|
||
|
||
const supabase = await createClient()
|
||
const { data: current } = await supabase.from("template_variants").select("share_token").eq("id", id).maybeSingle()
|
||
if (!current) return { error: "Variante nicht gefunden." }
|
||
|
||
const token = enabled ? (regenerate || !current.share_token ? newShareToken() : current.share_token) : null
|
||
const expiresAt = enabled && expiresInDays > 0 ? new Date(Date.now() + expiresInDays * 86_400_000).toISOString() : null
|
||
|
||
const { error } = await supabase
|
||
.from("template_variants")
|
||
.update({ share_token: token, share_expires_at: expiresAt })
|
||
.eq("id", id)
|
||
if (error) return { error: "Freigabe konnte nicht gespeichert werden." }
|
||
|
||
revalidateTemplates()
|
||
return { token, expiresAt }
|
||
}
|
||
|
||
export async function duplicateVariant(id: string): Promise<{ error: string } | undefined> {
|
||
if (!(await requireAdmin())) return { error: "Keine Berechtigung." }
|
||
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Variante." }
|
||
|
||
const supabase = await createClient()
|
||
const { data: source } = await supabase
|
||
.from("template_variants")
|
||
.select("name, template_key, client_id, content")
|
||
.eq("id", id)
|
||
.maybeSingle()
|
||
if (!source) return { error: "Variante nicht gefunden." }
|
||
|
||
// Der Freigabe-Link wird bewusst nicht mitkopiert
|
||
const { data, error } = await supabase
|
||
.from("template_variants")
|
||
.insert({ ...source, name: `${source.name} (Kopie)`.slice(0, 120) })
|
||
.select("id")
|
||
.single()
|
||
if (error || !data) return { error: "Variante konnte nicht dupliziert werden." }
|
||
|
||
revalidateTemplates()
|
||
redirect(`/admin/templates/${data.id}`)
|
||
}
|
||
|
||
export async function deleteVariant(id: string): Promise<VariantActionResult> {
|
||
if (!(await requireAdmin())) return { error: "Keine Berechtigung." }
|
||
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Variante." }
|
||
|
||
const supabase = await createClient()
|
||
const { error } = await supabase.from("template_variants").delete().eq("id", id)
|
||
if (error) return { error: "Variante konnte nicht gelöscht werden." }
|
||
|
||
revalidateTemplates()
|
||
return { success: true }
|
||
}
|