From 3b94bceaf0c0dfd59627ed5fe280823245598592 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 01:35:10 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin):=20kullan=C4=B1c=C4=B1=20y=C3=B6net?= =?UTF-8?q?imi=20tablosu=20ve=20diyaloglar=C4=B1=20(mock)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/app/(app)/admin/kullanicilar/page.tsx | 18 + src/app/(app)/admin/layout.tsx | 11 + .../components/user-action-dialogs.tsx | 219 +++++++ .../components/user-form-dialog.tsx | 226 +++++++ .../admin-users/components/users-table.tsx | 572 ++++++++++++++++++ src/features/admin-users/guards.test.ts | 59 ++ src/features/admin-users/guards.ts | 52 ++ src/features/admin-users/mock.ts | 111 ++++ src/features/admin-users/types.ts | 14 + 9 files changed, 1282 insertions(+) create mode 100644 src/app/(app)/admin/kullanicilar/page.tsx create mode 100644 src/app/(app)/admin/layout.tsx create mode 100644 src/features/admin-users/components/user-action-dialogs.tsx create mode 100644 src/features/admin-users/components/user-form-dialog.tsx create mode 100644 src/features/admin-users/components/users-table.tsx create mode 100644 src/features/admin-users/guards.test.ts create mode 100644 src/features/admin-users/guards.ts create mode 100644 src/features/admin-users/mock.ts create mode 100644 src/features/admin-users/types.ts diff --git a/src/app/(app)/admin/kullanicilar/page.tsx b/src/app/(app)/admin/kullanicilar/page.tsx new file mode 100644 index 0000000..6875a05 --- /dev/null +++ b/src/app/(app)/admin/kullanicilar/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { PageHeader } from "@/components/page-header"; +import { UsersTable } from "@/features/admin-users/components/users-table"; +import { MOCK_ADMIN_USERS } from "@/features/admin-users/mock"; +import { MOCK_SESSION_USER } from "@/features/auth/mock"; +import { tr } from "@/i18n/tr"; + +export const metadata: Metadata = { title: tr.adminUsers.title }; + +export default function AdminUsersPage() { + // FAZ 3: const users = await listUsers() (Better Auth admin API) + return ( + <> + + + + ); +} diff --git a/src/app/(app)/admin/layout.tsx b/src/app/(app)/admin/layout.tsx new file mode 100644 index 0000000..7cc8092 --- /dev/null +++ b/src/app/(app)/admin/layout.tsx @@ -0,0 +1,11 @@ +import { notFound } from "next/navigation"; +import { MOCK_SESSION_USER } from "@/features/auth/mock"; + +/** + * Yalnızca sistem admini. Admin olmayana 404 (sayfanın varlığını bile göstermez). + * FAZ 3: `await requireRole("admin")` (lib/dal.ts) — sunucu tarafı gerçek kontrol. + */ +export default function AdminLayout({ children }: { children: React.ReactNode }) { + if (MOCK_SESSION_USER.role !== "admin") notFound(); + return children; +} diff --git a/src/features/admin-users/components/user-action-dialogs.tsx b/src/features/admin-users/components/user-action-dialogs.tsx new file mode 100644 index 0000000..0e35b64 --- /dev/null +++ b/src/features/admin-users/components/user-action-dialogs.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { PasswordInput } from "@/components/password-input"; +import { ResponsiveDialog } from "@/components/responsive-dialog"; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Textarea } from "@/components/ui/textarea"; +import { tr } from "@/i18n/tr"; +import type { ActionResult } from "@/lib/action-result"; +import { + banUserSchema, + setPasswordSchema, + type BanUserInput, + type SetPasswordInput, +} from "@/shared/schemas/user"; +import type { AdminUser } from "../types"; + +const t = tr.adminUsers; + +type DialogProps = { + user: AdminUser | undefined; + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (user: AdminUser, values: T) => Promise; +}; + +export function SetPasswordDialog({ + user, + open, + onOpenChange, + onConfirm, +}: DialogProps<{ password: string }>) { + const form = useForm({ + resolver: zodResolver(setPasswordSchema), + defaultValues: { password: "" }, + }); + useEffect(() => { + if (open) form.reset({ password: "" }); + }, [open, form]); + + async function onSubmit(values: SetPasswordInput) { + if (!user) return; + const result = await onConfirm(user, values); + if (result.ok) onOpenChange(false); + else form.setError("root", { message: result.error }); + } + + return ( + + + + + } + > +
+ + ( + + {t.password.newPassword} + + {t.form.passwordHint} + {fieldState.invalid ? : null} + + )} + /> + {form.formState.errors.root ? ( + {form.formState.errors.root.message} + ) : null} + +
+
+ ); +} + +export function BanUserDialog({ + user, + open, + onOpenChange, + onConfirm, +}: DialogProps<{ reason?: string }>) { + const form = useForm({ + resolver: zodResolver(banUserSchema), + defaultValues: { reason: "" }, + }); + useEffect(() => { + if (open) form.reset({ reason: "" }); + }, [open, form]); + + async function onSubmit(values: { reason?: string }) { + if (!user) return; + const result = await onConfirm(user, values); + if (result.ok) onOpenChange(false); + else form.setError("root", { message: result.error }); + } + + return ( + + + + + } + > +
+ + ( + + {t.ban.reason} +