"use server"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; import { z } from "zod"; import { tr } from "@/i18n/tr"; import { fail, ok, type ActionResult } from "@/lib/action-result"; import { getAuth } from "@/lib/auth"; import { authErrorCode } from "@/lib/auth-errors"; import { writeAudit } from "@/lib/audit"; import { getActionUser } from "@/lib/dal"; import { removeAllMembershipsOfUser } from "@/features/households/service"; import { storedEmail } from "@/shared/account-email"; import { systemRoleSchema, type SystemRole } from "@/shared/schemas/common"; import { banUserSchema, setPasswordSchema, userCreateSchema, userUpdateSchema, } from "@/shared/schemas/user"; import { checkAdminAction, type AdminAction, type GuardViolation } from "./guards"; /** * Admin kullanıcı yönetimi Server Action'ları. * Sıra: yetki (sunucuda) → zod doğrulama → iş kuralları (guards) → Better Auth admin API → revalidate. */ const t = tr.adminUsers; const PATH = "/admin/kullanicilar"; const userIdSchema = z.string().min(1).max(64); const guardMessage = (v: GuardViolation) => (v === "self" ? t.guards.self : t.guards.lastAdmin); /** Bilinen Better Auth hata kodlarını kullanıcıya gösterilecek alan hatalarına çevirir. */ function mapAuthError(error: unknown): ActionResult { const code = authErrorCode(error); switch (code) { case "USERNAME_IS_ALREADY_TAKEN": return fail(t.errors.checkFields, { username: t.errors.usernameTaken }); case "USER_ALREADY_EXISTS": case "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": return fail(t.errors.checkFields, { email: t.errors.emailTaken }); case "INVALID_USERNAME": case "USERNAME_TOO_SHORT": case "USERNAME_TOO_LONG": return fail(t.errors.checkFields, { username: tr.validation.usernameFormat }); case "INVALID_EMAIL": return fail(t.errors.checkFields, { email: tr.validation.email }); case "PASSWORD_TOO_SHORT": case "PASSWORD_TOO_LONG": return fail(t.errors.checkFields, { password: tr.validation.passwordMin }); case "YOU_CANNOT_BAN_YOURSELF": case "YOU_CANNOT_REMOVE_YOURSELF": return fail(t.guards.self); default: // İç detay kullanıcıya sızdırılmaz; sunucuda loglanır. console.error("[admin-users] beklenmeyen hata", code ?? error); return fail(tr.common.genericError); } } /** Fiziksel kontrol: oturumdaki kullanıcı admin mi? Değilse işlem yapılmaz. */ async function adminContext() { const actor = await getActionUser("admin"); if (!actor) return null; return { actor, headers: await headers(), api: getAuth().api }; } type Ctx = NonNullable>>; async function loadTarget(ctx: Ctx, userId: string) { try { const user = await ctx.api.getUser({ headers: ctx.headers, query: { id: userId } }); return user ? { id: user.id, role: (user.role === "admin" ? "admin" : "user") as SystemRole, name: user.name, } : null; } catch { return null; } } async function countAdmins(ctx: Ctx): Promise { const result = await ctx.api.listUsers({ headers: ctx.headers, query: { filterField: "role", filterOperator: "eq", filterValue: "admin", limit: 1 }, }); return result.total; } async function guard(ctx: Ctx, action: AdminAction, target: { id: string; role: SystemRole }) { const adminCount = target.role === "admin" ? await countAdmins(ctx) : Number.POSITIVE_INFINITY; return checkAdminAction(action, { actorId: ctx.actor.id, target, adminCount }); } // --- Action'lar --- export async function createUserAction(input: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const parsed = userCreateSchema.safeParse(input); if (!parsed.success) return fail(t.errors.checkFields); const v = parsed.data; let createdId = ""; try { const created = await ctx.api.createUser({ headers: ctx.headers, body: { name: v.name, email: storedEmail(v.username, v.email), password: v.password, role: v.role, data: { username: v.username }, }, }); createdId = created.user.id; } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.create", actorId: ctx.actor.id, targetType: "user", targetId: createdId, diff: { username: v.username, role: v.role }, }); revalidatePath(PATH); return ok(undefined); } export async function updateUserAction(userId: unknown, input: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); const parsed = userUpdateSchema.safeParse(input); if (!id.success || !parsed.success) return fail(t.errors.checkFields); const target = await loadTarget(ctx, id.data); if (!target) return fail(tr.common.notFound); const v = parsed.data; if (v.role !== target.role) { const violation = await guard(ctx, { type: "setRole", role: v.role }, target); if (violation) return fail(guardMessage(violation), { role: guardMessage(violation) }); } try { await ctx.api.adminUpdateUser({ headers: ctx.headers, body: { userId: target.id, data: { name: v.name, username: v.username, email: storedEmail(v.username, v.email) }, }, }); if (v.role !== target.role) { await ctx.api.setRole({ headers: ctx.headers, body: { userId: target.id, role: v.role } }); } } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.update", actorId: ctx.actor.id, targetType: "user", targetId: target.id, diff: { name: v.name, username: v.username, role: v.role }, }); revalidatePath(PATH); return ok(undefined); } export async function setRoleAction(userId: unknown, role: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); const r = systemRoleSchema.safeParse(role); if (!id.success || !r.success) return fail(tr.common.genericError); const target = await loadTarget(ctx, id.data); if (!target) return fail(tr.common.notFound); const violation = await guard(ctx, { type: "setRole", role: r.data }, target); if (violation) return fail(guardMessage(violation)); try { await ctx.api.setRole({ headers: ctx.headers, body: { userId: target.id, role: r.data } }); } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.role", actorId: ctx.actor.id, targetType: "user", targetId: target.id, diff: { role: { from: target.role, to: r.data } }, }); revalidatePath(PATH); return ok(undefined); } export async function setPasswordAction(userId: unknown, input: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); const parsed = setPasswordSchema.safeParse(input); if (!id.success || !parsed.success) return fail(t.errors.checkFields); const target = await loadTarget(ctx, id.data); if (!target) return fail(tr.common.notFound); try { await ctx.api.setUserPassword({ headers: ctx.headers, body: { userId: target.id, newPassword: parsed.data.password }, }); // Başka bir kullanıcının şifresi sıfırlandıysa açık oturumlarını kapat. if (target.id !== ctx.actor.id) { await ctx.api.revokeUserSessions({ headers: ctx.headers, body: { userId: target.id } }); } } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.password.reset", actorId: ctx.actor.id, targetType: "user", targetId: target.id, }); return ok(undefined); } export async function banUserAction(userId: unknown, input: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); const parsed = banUserSchema.safeParse(input); if (!id.success || !parsed.success) return fail(t.errors.checkFields); const target = await loadTarget(ctx, id.data); if (!target) return fail(tr.common.notFound); const violation = await guard(ctx, { type: "ban" }, target); if (violation) return fail(guardMessage(violation)); try { // banUser açık oturumları da sonlandırır. await ctx.api.banUser({ headers: ctx.headers, body: { userId: target.id, banReason: parsed.data.reason }, }); } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.ban", actorId: ctx.actor.id, targetType: "user", targetId: target.id, diff: { reason: parsed.data.reason ?? null }, }); revalidatePath(PATH); return ok(undefined); } export async function unbanUserAction(userId: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); if (!id.success) return fail(tr.common.genericError); try { await ctx.api.unbanUser({ headers: ctx.headers, body: { userId: id.data } }); } catch (error) { return mapAuthError(error); } await writeAudit({ action: "user.unban", actorId: ctx.actor.id, targetType: "user", targetId: id.data, }); revalidatePath(PATH); return ok(undefined); } export async function removeUserAction(userId: unknown): Promise { const ctx = await adminContext(); if (!ctx) return fail(tr.common.unauthorized); const id = userIdSchema.safeParse(userId); if (!id.success) return fail(tr.common.genericError); const target = await loadTarget(ctx, id.data); if (!target) return fail(tr.common.notFound); const violation = await guard(ctx, { type: "delete" }, target); if (violation) return fail(guardMessage(violation)); try { await ctx.api.removeUser({ headers: ctx.headers, body: { userId: target.id } }); } catch (error) { return mapAuthError(error); } const removedMemberships = await removeAllMembershipsOfUser(target.id); await writeAudit({ action: "user.remove", actorId: ctx.actor.id, targetType: "user", targetId: target.id, diff: { name: target.name, removedMemberships }, }); revalidatePath(PATH); return ok(undefined); }