mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
173 lines
No EOL
6.3 KiB
TypeScript
173 lines
No EOL
6.3 KiB
TypeScript
"use server"
|
|
import { SERVICES } from "@/modules/shared/data/services-data"
|
|
import * as z from "zod"
|
|
import { BILLING_CYCLES, type BillingCycle, type Plan } from "../lib/internal-costs";
|
|
import { verifySession } from "@/lib/supabase/dal";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
|
|
const PATH = "/admin/internal-accounts"
|
|
|
|
const BaseSchema = z.object({
|
|
service: z.string().refine((id) => SERVICES.some((service) => service.id === id), { error: "Unbekannter Dienst." }),
|
|
title: z.string().trim().max(120, { error: "Bezeichnung ist zu lang." }),
|
|
username: z.string().trim().min(1, { error: "Benutzername ist erforderlich." }).max(200),
|
|
url: z
|
|
.string()
|
|
.trim()
|
|
.max(500)
|
|
.refine((value) => value === "" || /^https?:\/\//i.test(value), { error: "Login-Adresse bitte mit https:// angeben." }),
|
|
});
|
|
|
|
|
|
const CreateSchema = BaseSchema.extend({
|
|
password: z.string().min(1, { error: "Passwort ist erforderlich." })
|
|
})
|
|
|
|
const UpdateSchema = BaseSchema.extend({
|
|
id: z.string().uuid(),
|
|
password: z.string().optional(),
|
|
})
|
|
|
|
// "12,99", "1.234,50" oder "12.99" → Zahl
|
|
const toNumber = (value: string) => Number(value.includes(",") ? value.replace(/\./g, "").replace(",", ".") : value)
|
|
|
|
const CostSchema = z.discriminatedUnion(
|
|
"plan",
|
|
[
|
|
z.object({ plan: z.literal("free") }),
|
|
z.object({
|
|
plan: z.literal("paid"),
|
|
price: z
|
|
.string()
|
|
.trim()
|
|
.min(1, { error: "Bitte einen Preis angeben." })
|
|
.transform(toNumber)
|
|
.refine((value) => Number.isFinite(value) && value >= 0 && value < 100_000_000, {
|
|
error: "Bitte einen gültigen Preis angeben, z. B. 12,99.",
|
|
}),
|
|
billingCycle: z.enum(BILLING_CYCLES, { error: "Bitte die Abrechnung wählen." }),
|
|
}),
|
|
],
|
|
{ error: "Bitte kostenlos oder bezahlt wählen." }
|
|
)
|
|
|
|
const IdSchema = z.string().uuid()
|
|
|
|
export type InternalCredentialState = { error: string } | { success: true } | undefined
|
|
|
|
async function isAdmin() {
|
|
const session = await verifySession()
|
|
return session.isAdmin
|
|
}
|
|
|
|
function readFields(formData: FormData) {
|
|
return {
|
|
service: formData.get("service"),
|
|
title: formData.get("title") ?? "",
|
|
username: formData.get("username"),
|
|
url: formData.get("url") ?? "",
|
|
}
|
|
}
|
|
|
|
type CostParams = { p_plan: Plan; p_price: number | null; p_billing_cycle: BillingCycle | null }
|
|
|
|
// Kosten als RPC-Parameter; kostenlos = kein Preis und keine Abrechnung.
|
|
// Expliziter Rückgabetyp, damit `"error" in cost` sauber eingrenzt.
|
|
function readCost(formData: FormData): CostParams | { error: string } {
|
|
const parsed = CostSchema.safeParse({
|
|
plan: formData.get("plan"),
|
|
price: formData.get("price") ?? "",
|
|
billingCycle: formData.get("billingCycle"),
|
|
})
|
|
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Bitte die Kosten prüfen." }
|
|
|
|
return parsed.data.plan === "paid"
|
|
? { p_plan: "paid", p_price: Math.round(parsed.data.price * 100) / 100, p_billing_cycle: parsed.data.billingCycle }
|
|
: { p_plan: "free", p_price: null, p_billing_cycle: null }
|
|
}
|
|
|
|
export async function createInternalCredential(
|
|
_prevState: InternalCredentialState,
|
|
formData: FormData
|
|
): Promise<InternalCredentialState> {
|
|
if (!(await isAdmin())) return { error: "Keine Berechtigung." }
|
|
|
|
const parsed = CreateSchema.safeParse({ ...readFields(formData), password: formData.get("password") })
|
|
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Bitte alle Pflichtfelder ausfüllen." }
|
|
|
|
const cost = readCost(formData)
|
|
if ("error" in cost) return cost
|
|
|
|
const { service, title, username, url, password } = parsed.data
|
|
const supabase = await createClient()
|
|
const { error } = await supabase.rpc("create_internal_credential", {
|
|
p_service: service,
|
|
p_title: title,
|
|
p_username: username,
|
|
p_url: url,
|
|
...cost,
|
|
p_password: password,
|
|
})
|
|
if (error) return { error: "Eintrag konnte nicht gespeichert werden." }
|
|
|
|
revalidatePath(PATH)
|
|
return { success: true }
|
|
}
|
|
|
|
export async function updateInternalCredential(
|
|
_prevState: InternalCredentialState,
|
|
formData: FormData
|
|
): Promise<InternalCredentialState> {
|
|
if (!(await isAdmin())) return { error: "Keine Berechtigung." }
|
|
|
|
// Leeres Passwortfeld = Passwort bleibt, wie es ist
|
|
const password = String(formData.get("password") ?? "")
|
|
const parsed = UpdateSchema.safeParse({
|
|
...readFields(formData),
|
|
id: formData.get("id"),
|
|
password: password === "" ? undefined : password,
|
|
})
|
|
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Bitte alle Pflichtfelder ausfüllen." }
|
|
|
|
const cost = readCost(formData)
|
|
if ("error" in cost) return cost
|
|
|
|
const supabase = await createClient()
|
|
const { error } = await supabase.rpc("update_internal_credential", {
|
|
p_id: parsed.data.id,
|
|
p_service: parsed.data.service,
|
|
p_title: parsed.data.title,
|
|
p_username: parsed.data.username,
|
|
p_url: parsed.data.url,
|
|
...cost,
|
|
p_password: parsed.data.password ?? null,
|
|
})
|
|
if (error) return { error: "Eintrag konnte nicht aktualisiert werden." }
|
|
|
|
revalidatePath(PATH)
|
|
return { success: true }
|
|
}
|
|
|
|
export async function revealInternalCredential(id: string): Promise<{ password: string } | { error: string }> {
|
|
if (!(await isAdmin()) || !IdSchema.safeParse(id).success) return { error: "Keine Berechtigung." }
|
|
|
|
const supabase = await createClient()
|
|
const { data, error } = await supabase.rpc("reveal_internal_credential", { p_id: id })
|
|
if (error || typeof data !== "string") return { error: "Passwort konnte nicht abgerufen werden." }
|
|
|
|
return { password: data }
|
|
}
|
|
|
|
export async function deleteInternalCredential(id: string): Promise<InternalCredentialState> {
|
|
if (!(await isAdmin())) return { error: "Keine Berechtigung." }
|
|
if (!IdSchema.safeParse(id).success) return { error: "Ungültiger Eintrag." }
|
|
|
|
const supabase = await createClient()
|
|
const { error } = await supabase.from("internal_credentials").delete().eq("id", id)
|
|
if (error) return { error: "Eintrag konnte nicht gelöscht werden." }
|
|
|
|
revalidatePath(PATH)
|
|
return { success: true }
|
|
} |