feat(admin): Server Action'larla kullanıcı yönetimi ve gerçek oturumlu sayfalar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:02:00 +03:00
parent 5191a5cce1
commit 2035f73e7d
18 changed files with 437 additions and 270 deletions
@@ -59,18 +59,22 @@ import {
TableRow,
} from "@/components/ui/table";
import { tr } from "@/i18n/tr";
import { fail, ok, type ActionResult } from "@/lib/action-result";
import { fail, type ActionResult } from "@/lib/action-result";
import { cn } from "@/lib/utils";
import { formatDate } from "@/shared/format";
import type { SystemRole } from "@/shared/schemas/common";
import type { UserCreate, UserUpdate } from "@/shared/schemas/user";
import { compareTr, includesTr } from "@/shared/search";
import {
checkAdminAction,
findUniqueConflicts,
type AdminAction,
type GuardViolation,
} from "../guards";
banUserAction,
createUserAction,
removeUserAction,
setPasswordAction,
setRoleAction,
unbanUserAction,
updateUserAction,
} from "../actions";
import { checkAdminAction, type AdminAction, type GuardViolation } from "../guards";
import type { AdminUser } from "../types";
import { BanUserDialog, DeleteUserDialog, SetPasswordDialog } from "./user-action-dialogs";
import { UserFormDialog } from "./user-form-dialog";
@@ -311,15 +315,14 @@ const columns = helper.columns([
]);
export function UsersTable({
initialUsers,
users,
currentUserId,
}: {
initialUsers: AdminUser[];
users: AdminUser[];
currentUserId: string;
}) {
// FAZ 3: yerel durum yerine Server Action + revalidatePath.
const [users, setUsers] = useState(initialUsers);
const [dialog, setDialog] = useState<DialogState>(null);
// Arayüzdeki kural kontrolü yalnızca erken geri bildirim içindir; asıl kontrol sunucuda.
const adminCount = users.filter((u) => u.role === "admin").length;
const guard = (action: AdminAction, target: AdminUser): GuardViolation | null =>
@@ -327,84 +330,41 @@ export function UsersTable({
const guardMessage = (v: GuardViolation) => (v === "self" ? t.guards.self : t.guards.lastAdmin);
// --- Mock işlemler (FAZ 3'te Server Action'lara taşınacak; aynı ActionResult sözleşmesi) ---
async function createUser(values: UserCreate): Promise<ActionResult> {
const conflicts = findUniqueConflicts(users, values);
if (conflicts.username || conflicts.email) {
return fail(t.errors.checkFields, {
username: conflicts.username && t.errors.usernameTaken,
email: conflicts.email && t.errors.emailTaken,
});
/** Server Action'ı çağırır; başarıda bildirim gösterir. Sayfa revalidatePath ile tazelenir. */
async function run(promise: Promise<ActionResult>, success: string): Promise<ActionResult> {
try {
const result = await promise;
if (result.ok) toast.success(success);
return result;
} catch {
return fail(tr.common.genericError);
}
const { password: _password, ...rest } = values;
setUsers((prev) => [
{
...rest,
id: `u_${crypto.randomUUID().slice(0, 8)}`,
banned: false,
createdAt: new Date().toISOString(),
},
...prev,
]);
toast.success(t.toast.created(values.name));
return ok(undefined);
}
async function updateUser(id: string, values: UserUpdate): Promise<ActionResult> {
const target = users.find((u) => u.id === id);
if (!target) return fail(tr.common.genericError);
const conflicts = findUniqueConflicts(users, values, id);
if (conflicts.username || conflicts.email) {
return fail(t.errors.checkFields, {
username: conflicts.username && t.errors.usernameTaken,
email: conflicts.email && t.errors.emailTaken,
});
}
const v =
values.role !== target.role ? guard({ type: "setRole", role: values.role }, target) : null;
if (v) return fail(guardMessage(v), { role: guardMessage(v) });
setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, ...values } : u)));
toast.success(t.toast.updated(values.name));
return ok(undefined);
const createUser = (values: UserCreate) =>
run(createUserAction(values), t.toast.created(values.name));
const updateUser = (id: string, values: UserUpdate) =>
run(updateUserAction(id, values), t.toast.updated(values.name));
async function setRole(user: AdminUser, role: SystemRole) {
const result = await run(setRoleAction(user.id, role), t.toast.roleChanged(user.name));
if (!result.ok) toast.error(result.error);
}
function setRole(user: AdminUser, role: SystemRole) {
const v = guard({ type: "setRole", role }, user);
if (v) return void toast.error(guardMessage(v));
setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, role } : u)));
toast.success(t.toast.roleChanged(user.name));
const setPassword = (user: AdminUser, values: { password: string }) =>
run(setPasswordAction(user.id, values), t.toast.passwordSet(user.name));
const banUser = (user: AdminUser, values: { reason?: string }) =>
run(banUserAction(user.id, values), t.toast.banned(user.name));
async function unbanUser(user: AdminUser) {
const result = await run(unbanUserAction(user.id), t.toast.unbanned(user.name));
if (!result.ok) toast.error(result.error);
}
async function setPassword(user: AdminUser): Promise<ActionResult> {
toast.success(t.toast.passwordSet(user.name));
return ok(undefined);
}
async function banUser(user: AdminUser, { reason }: { reason?: string }): Promise<ActionResult> {
const v = guard({ type: "ban" }, user);
if (v) return fail(guardMessage(v));
setUsers((prev) =>
prev.map((u) => (u.id === user.id ? { ...u, banned: true, banReason: reason } : u)),
);
toast.success(t.toast.banned(user.name));
return ok(undefined);
}
function unbanUser(user: AdminUser) {
setUsers((prev) =>
prev.map((u) => (u.id === user.id ? { ...u, banned: false, banReason: undefined } : u)),
);
toast.success(t.toast.unbanned(user.name));
}
async function removeUser(user: AdminUser): Promise<ActionResult> {
const v = guard({ type: "delete" }, user);
if (v) return fail(guardMessage(v));
setUsers((prev) => prev.filter((u) => u.id !== user.id));
toast.success(t.toast.removed(user.name));
return ok(undefined);
}
const removeUser = (user: AdminUser) =>
run(removeUserAction(user.id), t.toast.removed(user.name));
const table = useTable({
features,