since3-portal/modules/templates/actions/variants.ts
2026-10-05 08:23:02 +02:00

151 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 }
}