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:
@@ -1,18 +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 { listAdminUsers } from "@/features/admin-users/queries";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { requireRole } from "@/lib/dal";
|
||||
|
||||
export const metadata: Metadata = { title: tr.adminUsers.title };
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
// FAZ 3: const users = await listUsers() (Better Auth admin API)
|
||||
export default async function AdminUsersPage() {
|
||||
const [admin, users] = await Promise.all([requireRole("admin"), listAdminUsers()]);
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={tr.adminUsers.title} description={tr.adminUsers.description} />
|
||||
<UsersTable initialUsers={MOCK_ADMIN_USERS} currentUserId={MOCK_SESSION_USER.id} />
|
||||
<UsersTable users={users} currentUserId={admin.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { MOCK_SESSION_USER } from "@/features/auth/mock";
|
||||
import { requireRole } from "@/lib/dal";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/** Yalnızca sistem admini; diğerlerine 404. Sayfalar ayrıca kendi kontrolünü yapar. */
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
await requireRole("admin");
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { MOCK_SESSION_USER } from "@/features/auth/mock";
|
||||
import { SummaryCardsSkeleton } from "@/features/dashboard/components/summary-cards-skeleton";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { requireSession } from "@/lib/dal";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatMoney } from "@/shared/format";
|
||||
|
||||
@@ -51,8 +51,8 @@ async function SummaryCards() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const user = MOCK_SESSION_USER;
|
||||
export default async function DashboardPage() {
|
||||
const user = await requireSession();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
import { Suspense } from "react";
|
||||
import { AppHeader } from "@/components/layout/app-header";
|
||||
import { AppShellFallback } from "@/components/layout/app-shell-fallback";
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||
import { BottomNav } from "@/components/layout/bottom-nav";
|
||||
import { QuickAddProvider } from "@/components/layout/quick-add";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { MOCK_SESSION_USER } from "@/features/auth/mock";
|
||||
import { MOCK_HOUSEHOLDS } from "@/features/households/mock";
|
||||
import { requireSession } from "@/lib/dal";
|
||||
import { APP_VERSION } from "@/lib/version";
|
||||
|
||||
/**
|
||||
* Oturum gerektiren alan: yan menü (masaüstü), üst bar, alt menü (mobil), hızlı ekle.
|
||||
* FAZ 3: `const session = await requireSession()` — oturum yoksa /login.
|
||||
* Oturum gerektiren alan. Oturum okuması istek anında yapılır (Cache Components), bu yüzden
|
||||
* kullanıcıya bağlı kabuk Suspense içinde render edilir. Her sayfa ayrıca kendi yetkisini
|
||||
* DAL üzerinden kontrol eder (layout istemci gezinmesinde yeniden çalışmaz).
|
||||
*/
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
const user = MOCK_SESSION_USER;
|
||||
return (
|
||||
<Suspense fallback={<AppShellFallback />}>
|
||||
<AuthenticatedShell>{children}</AuthenticatedShell>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function AuthenticatedShell({ children }: { children: React.ReactNode }) {
|
||||
const user = await requireSession();
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<QuickAddProvider>
|
||||
<AppSidebar isAdmin={user.role === "admin"} version={APP_VERSION} />
|
||||
<SidebarInset>
|
||||
{/* FAZ 5: kullanıcının üye olduğu haneler */}
|
||||
<AppHeader user={user} households={MOCK_HOUSEHOLDS} version={APP_VERSION} />
|
||||
{/* Mobilde alt menü yüksekliği kadar boşluk */}
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pb-24 md:p-6 md:pb-6">{children}</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { looksLikeEmail, normalizeUsername } from "@/shared/schemas/common";
|
||||
import { loginSchema, type LoginInput } from "@/shared/schemas/auth";
|
||||
|
||||
export function LoginForm() {
|
||||
@@ -20,11 +22,23 @@ export function LoginForm() {
|
||||
defaultValues: { identifier: "", password: "" },
|
||||
});
|
||||
|
||||
async function onSubmit(_values: LoginInput) {
|
||||
async function onSubmit({ identifier, password }: LoginInput) {
|
||||
setFormError(null);
|
||||
// FAZ 3: looksLikeEmail(identifier) ? authClient.signIn.email : authClient.signIn.username
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
const value = identifier.trim();
|
||||
const { error } = looksLikeEmail(value)
|
||||
? await authClient.signIn.email({ email: value.toLowerCase(), password })
|
||||
: await authClient.signIn.username({ username: normalizeUsername(value), password });
|
||||
|
||||
if (error) {
|
||||
// Hangi bilginin yanlış olduğu söylenmez (hesap tahmini zorlaşır).
|
||||
if (error.status === 429) setFormError(tr.auth.tooManyAttempts);
|
||||
else if (error.code === "BANNED_USER") setFormError(tr.auth.banned);
|
||||
else setFormError(tr.auth.invalidCredentials);
|
||||
form.setValue("password", "");
|
||||
return;
|
||||
}
|
||||
router.replace("/dashboard");
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
const submitting = form.formState.isSubmitting;
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { Logo } from "@/components/brand/logo";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { getCurrentUser } from "@/lib/dal";
|
||||
import { LoginForm } from "./login-form";
|
||||
|
||||
export const metadata: Metadata = { title: tr.auth.loginTitle };
|
||||
|
||||
/** Oturum açıksa dashboard'a yönlendirir (istek anında, Suspense içinde). */
|
||||
async function RedirectIfSignedIn() {
|
||||
const user = await getCurrentUser();
|
||||
if (user) redirect("/dashboard");
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<main className="flex w-full max-w-sm flex-col items-center gap-6">
|
||||
<Suspense fallback={null}>
|
||||
<RedirectIfSignedIn />
|
||||
</Suspense>
|
||||
<Logo priority className="scale-125" />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { connection } from "next/server";
|
||||
import { pingDb } from "@/lib/db/mongo";
|
||||
import { APP_COMMIT, APP_VERSION } from "@/lib/version";
|
||||
|
||||
/**
|
||||
* Sağlık kontrolü (Dokploy / Docker HEALTHCHECK).
|
||||
* Faz 3'te veritabanı ping'i eklenecek; DB erişilemezse 503 dönecek.
|
||||
* Sağlık kontrolü (Dokploy / Docker HEALTHCHECK). Veritabanına erişilemezse 503.
|
||||
* İç hata detayı döndürülmez.
|
||||
*/
|
||||
export async function GET() {
|
||||
// Cache Components açıkken GET handler'ı build'de prerender edilmesin.
|
||||
await connection();
|
||||
const dbOk = await pingDb();
|
||||
return Response.json(
|
||||
{
|
||||
status: "ok",
|
||||
status: dbOk ? "ok" : "degraded",
|
||||
db: dbOk ? "ok" : "unreachable",
|
||||
version: APP_VERSION,
|
||||
commit: APP_COMMIT,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{ headers: { "Cache-Control": "no-store" } },
|
||||
{ status: dbOk ? 200 : 503, headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
/** Uygulama her zaman login ile açılır. FAZ 3: oturum varsa /dashboard'a yönlenecek. */
|
||||
/** Oturum yoksa proxy zaten /login'e yönlendirir; varsa dashboard. */
|
||||
export default function Home() {
|
||||
redirect("/login");
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user