since3-portal/modules/shared/actions/credentials.ts
2026-10-01 13:59:20 +02:00

168 lines
6 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 * as z from "zod";
import { revalidatePath } from "next/cache";
import { after } from "next/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { createClient } from "@/lib/supabase/server";
import { verifySession } from "@/lib/supabase/dal";
import { getClientId } from "@/modules/shared/queries/profile";
import { getMailRecipient } from "@/modules/shared/queries/mail-recipient";
import { getServiceById } from "@/modules/shared/data/services-data";
import { getAppUrl } from "@/modules/shared/mail/app-url";
import { sendMail } from "@/modules/shared/mail/send-mail";
import { credentialAddedMail } from "@/modules/shared/mail/templates";
const CredentialSchema = z.object({
profileId: z.string().uuid(),
service: z.string().min(1, { error: "Dienst ist erforderlich." }),
username: z.string().min(1, { error: "Benutzername ist erforderlich." }),
password: z.string().min(1, { error: "Passwort ist erforderlich." }),
});
export type CredentialState = { error: string } | { success: true } | undefined;
export async function createCredential(
_prevState: CredentialState,
formData: FormData
): Promise<CredentialState> {
const session = await verifySession();
const profileId = String(formData.get("profileId") ?? "");
if (!session.isAdmin && session.userId !== profileId) {
return { error: "Keine Berechtigung." };
}
const validatedFields = CredentialSchema.safeParse({
profileId,
service: formData.get("service"),
username: formData.get("username"),
password: formData.get("password"),
});
if (!validatedFields.success) {
const { fieldErrors } = z.flattenError(validatedFields.error);
const firstError = Object.values(fieldErrors).flat()[0];
return { error: firstError ?? "Bitte alle Felder ausfüllen." };
}
const clientId = await getClientId(profileId);
if (!clientId) {
return { error: "Client hat noch keine Stammdaten hinterlegt." };
}
const supabase = await createClient();
const { data: credentialId, error } = await supabase.rpc("create_credential", {
p_client_id: clientId,
p_service: validatedFields.data.service,
p_username: validatedFields.data.username,
p_password: validatedFields.data.password,
});
if (error) return { error: "Eintrag konnte nicht gespeichert werden." };
// Nur wenn wir den Zugang hinterlegen – legt der Kunde ihn selbst an, braucht er keine Mail.
if (session.isAdmin) {
const serviceLabel = getServiceById(validatedFields.data.service).label;
after(async () => {
try {
const recipient = await getMailRecipient(profileId);
if (!recipient) return;
await sendMail({
to: recipient.email,
...credentialAddedMail({ name: recipient.name, serviceLabel, appUrl: await getAppUrl() }),
type: "credential",
idempotencyKey: `credential-${credentialId}`,
});
} catch (error) {
console.error("[mail] Hinweis-Mail zum neuen Zugang fehlgeschlagen:", error);
}
});
}
revalidatePath("/client/passwords");
revalidatePath(`/admin/clients/${profileId}/passwords`);
return { success: true };
}
const UpdateCredentialSchema = z.object({
profileId: z.string().uuid(),
credentialId: z.string().uuid(),
service: z.string().min(1, { error: "Dienst ist erforderlich." }),
username: z.string().min(1, { error: "Benutzername ist erforderlich." }),
password: z.string().optional(),
});
export async function updateCredential(
_prevState: CredentialState,
formData: FormData
): Promise<CredentialState> {
const session = await verifySession();
const profileId = String(formData.get("profileId") ?? "");
if (!session.isAdmin && session.userId !== profileId) {
return { error: "Keine Berechtigung." };
}
const password = String(formData.get("password") ?? "").trim();
const validatedFields = UpdateCredentialSchema.safeParse({
profileId,
credentialId: formData.get("credentialId"),
service: formData.get("service"),
username: formData.get("username"),
password: password === "" ? undefined : password,
});
if (!validatedFields.success) {
const { fieldErrors } = z.flattenError(validatedFields.error);
const firstError = Object.values(fieldErrors).flat()[0];
return { error: firstError ?? "Bitte alle Felder ausfüllen." };
}
const supabase = await createClient();
const { error } = await supabase.rpc("update_credential", {
p_id: validatedFields.data.credentialId,
p_service: validatedFields.data.service,
p_username: validatedFields.data.username,
p_password: validatedFields.data.password ?? null,
});
if (error) return { error: "Eintrag konnte nicht aktualisiert werden." };
revalidatePath("/client/passwords");
revalidatePath(`/admin/clients/${profileId}/passwords`);
return { success: true };
}
export async function revealCredential(credentialId: string): Promise<{ password: string } | { error: string }> {
await verifySession();
const supabase = await createClient();
const { data, error } = await supabase.rpc("reveal_credential", { p_id: credentialId });
if (error || typeof data !== "string") {
return { error: "Passwort konnte nicht abgerufen werden." };
}
return { password: data };
}
export async function deleteCredential(profileId: string, credentialId: string): Promise<CredentialState> {
const session = await verifySession();
if (!session.isAdmin) {
return { error: "Keine Berechtigung." };
}
const supabase = createAdminClient();
const { error } = await supabase.from("credentials").delete().eq("id", credentialId);
if (error) return { error: "Eintrag konnte nicht gelöscht werden." };
revalidatePath(`/admin/clients/${profileId}/passwords`);
return { success: true };
}