feat(01-02): login page UI, logout flow, seed script, and auth unit tests

- src/app/(auth)/layout.tsx: centered auth layout for login page
- src/app/(auth)/login/page.tsx: login form with error/loading states, sign up link
- src/components/providers.tsx: SessionProvider wrapper for client-side session
- src/components/layout/header.tsx: authenticated header with Sign out button
- src/app/(dashboard)/layout.tsx: dashboard layout wrapping Header component
- src/app/(dashboard)/dashboard/page.tsx: basic dashboard page post-login
- src/app/layout.tsx: wrap root with SessionProvider via Providers component
- prisma/seed.ts: idempotent seed for Demo ISP tenant + admin + super-admin users
- package.json: add db:seed script and prisma.seed config, add tsx devDep
- src/lib/__tests__/auth.test.ts: 8 unit tests for authOptions callbacks
This commit is contained in:
kevin-asprec
2026-03-04 18:42:53 +08:00
parent 43761d94ee
commit 3c37cb1866
10 changed files with 895 additions and 4 deletions

18
src/app/(auth)/layout.tsx Normal file
View File

@@ -0,0 +1,18 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "NetForge",
description: "NetForge ISP Management Platform",
};
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
{children}
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { getCurrentUser } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Dashboard</h1>
<p className="text-gray-600">
Welcome back, {user.firstName} {user.lastName}
</p>
<div className="mt-6 p-4 bg-white rounded-lg border border-gray-200 inline-block">
<p className="text-sm text-gray-500">Signed in as</p>
<p className="font-medium text-gray-900">{user.email}</p>
{user.tenantId && (
<p className="text-sm text-gray-500 mt-1">Tenant: {user.tenantId}</p>
)}
{user.isSuperAdmin && (
<p className="text-sm text-blue-600 mt-1 font-medium">
Super Admin
</p>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { Header } from "@/components/layout/header";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<main className="px-6 py-6">{children}</main>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "@/components/providers";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "NetForge",
description: "ISP Management Platform",
};
export default function RootLayout({
@@ -27,7 +28,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Providers>{children}</Providers>
</body>
</html>
);