Phase 3. 인증 및 공통 레이아웃 작업

This commit is contained in:
2026-06-09 23:37:33 +09:00
parent a93a6fa8e1
commit dbf9b3e180
17 changed files with 1092 additions and 71 deletions
+1
View File
@@ -1,4 +1,5 @@
# Local development
DATABASE_URL="mysql://USER:PASSWORD@127.0.0.1:3306/chocomae"
AUTH_SECRET="replace-with-local-secret"
NEXTAUTH_SECRET="replace-with-local-secret"
NEXTAUTH_URL="http://localhost:3001"
+84
View File
@@ -0,0 +1,84 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import {
ArrowUpCircle,
ClipboardList,
GraduationCap,
LayoutDashboard,
LogOut,
} from "lucide-react";
import { auth, signOut } from "@/auth";
import { Button } from "@/components/ui/button";
const navItems = [
{ href: "/", label: "대시보드", icon: LayoutDashboard },
{ href: "/maestros", label: "마에스트로", icon: GraduationCap },
{ href: "/extension-requests", label: "연장 신청", icon: ClipboardList },
{ href: "/upgrade-requests", label: "업그레이드", icon: ArrowUpCircle },
];
export default async function AdminLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await auth();
if (!session) {
redirect("/login");
}
return (
<div className="min-h-screen bg-muted/40">
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r bg-background md:block">
<div className="flex h-16 items-center border-b px-5">
<Link className="text-lg font-semibold" href="/">
choco-admin
</Link>
</div>
<nav className="space-y-1 p-3">
{navItems.map((item) => {
const Icon = item.icon;
return (
<Link
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
href={item.href}
key={item.href}
>
<Icon className="size-4" aria-hidden="true" />
{item.label}
</Link>
);
})}
</nav>
</aside>
<div className="md:pl-64">
<header className="sticky top-0 z-10 flex h-16 items-center justify-between border-b bg-background px-5">
<div>
<p className="text-sm font-medium">{session.user.name}</p>
<p className="text-xs text-muted-foreground">
ID {session.user.adminID}
</p>
</div>
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/login" });
}}
>
<Button size="sm" type="submit" variant="outline">
<LogOut className="size-4" aria-hidden="true" />
</Button>
</form>
</header>
<main className="p-5">{children}</main>
</div>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { Database, ShieldCheck, Table2 } from "lucide-react";
import { auth } from "@/auth";
const checks = [
{
label: "Next.js 16",
value: "App Router",
icon: ShieldCheck,
},
{
label: "Prisma",
value: "MariaDB introspected",
icon: Database,
},
{
label: "TanStack Table",
value: "Installed",
icon: Table2,
},
];
export default async function Home() {
const session = await auth();
return (
<section className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-normal"></h1>
<p className="mt-1 text-sm text-muted-foreground">
{session?.user.name} .
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{checks.map((item) => {
const Icon = item.icon;
return (
<article
className="rounded-lg border bg-card p-5 text-card-foreground"
key={item.label}
>
<div className="mb-4 flex size-9 items-center justify-center rounded-md bg-muted">
<Icon className="size-5" aria-hidden="true" />
</div>
<h2 className="text-base font-medium">{item.label}</h2>
<p className="mt-1 text-sm text-muted-foreground">{item.value}</p>
</article>
);
})}
</div>
</section>
);
}
+26
View File
@@ -0,0 +1,26 @@
"use server";
import { AuthError } from "next-auth";
import { redirect } from "next/navigation";
import { signIn } from "@/auth";
export async function loginAction(formData: FormData) {
const callbackUrl = String(formData.get("callbackUrl") ?? "/");
try {
await signIn("credentials", {
adminName: formData.get("adminName"),
password: formData.get("password"),
redirectTo: callbackUrl,
});
} catch (error) {
if (error instanceof AuthError) {
redirect(
`/login?error=CredentialsSignin&callbackUrl=${encodeURIComponent(callbackUrl)}`
);
}
throw error;
}
}
+81
View File
@@ -0,0 +1,81 @@
import { redirect } from "next/navigation";
import { auth } from "@/auth";
import { Button } from "@/components/ui/button";
import { loginAction } from "./actions";
type LoginPageProps = {
searchParams: Promise<{
callbackUrl?: string;
error?: string;
}>;
};
export default async function LoginPage({ searchParams }: LoginPageProps) {
const session = await auth();
if (session) {
redirect("/");
}
const params = await searchParams;
const callbackUrl = params.callbackUrl ?? "/";
const hasError = params.error === "CredentialsSignin";
return (
<main className="flex min-h-screen items-center justify-center bg-muted px-6 py-10">
<section className="w-full max-w-md rounded-lg border bg-background p-8 shadow-sm">
<div className="mb-8">
<h1 className="text-2xl font-semibold tracking-normal">
choco-admin
</h1>
<p className="mt-2 text-sm text-muted-foreground"> </p>
</div>
{hasError ? (
<div className="mb-5 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
.
</div>
) : null}
<form action={loginAction} className="space-y-5">
<input name="callbackUrl" type="hidden" value={callbackUrl} />
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="adminName">
</label>
<input
autoComplete="username"
autoFocus
className="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
id="adminName"
name="adminName"
required
type="text"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="password">
</label>
<input
autoComplete="current-password"
className="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
id="password"
name="password"
required
type="password"
/>
</div>
<Button className="w-full" type="submit">
</Button>
</form>
</section>
</main>
);
}
+1
View File
@@ -0,0 +1 @@
export { GET, POST } from "@/auth";
-62
View File
@@ -1,62 +0,0 @@
import { Database, ShieldCheck, Table2 } from "lucide-react";
import { Button } from "@/components/ui/button";
const checks = [
{
label: "Next.js 16",
value: "App Router",
icon: ShieldCheck,
},
{
label: "Prisma",
value: "MariaDB introspected",
icon: Database,
},
{
label: "TanStack Table",
value: "Installed",
icon: Table2,
},
];
export default function Home() {
return (
<main className="min-h-screen bg-background text-foreground">
<section className="mx-auto flex min-h-screen w-full max-w-6xl flex-col gap-8 px-6 py-8">
<header className="flex items-center justify-between border-b pb-5">
<div>
<h1 className="text-2xl font-semibold tracking-normal">
choco-admin
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Phase 1 initialization
</p>
</div>
<Button type="button">Ready</Button>
</header>
<div className="grid gap-4 md:grid-cols-3">
{checks.map((item) => {
const Icon = item.icon;
return (
<article
className="rounded-lg border bg-card p-5 text-card-foreground"
key={item.label}
>
<div className="mb-4 flex size-9 items-center justify-center rounded-md bg-muted">
<Icon className="size-5" aria-hidden="true" />
</div>
<h2 className="text-base font-medium">{item.label}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{item.value}
</p>
</article>
);
})}
</div>
</section>
</main>
);
}
+115
View File
@@ -0,0 +1,115 @@
import NextAuth, { type DefaultSession } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import { ACTIVATE_STATUSES } from "@/lib/constants";
const credentialsSchema = z.object({
adminName: z.string().trim().min(1),
password: z.string().min(1),
});
type AdminAuthRow = {
AdminID: bigint | number;
Name: string;
Email: string;
AccountType: bigint | number;
ActivateStatus: bigint | number;
};
declare module "next-auth" {
interface Session {
user: {
adminID: number;
accountType: number;
activateStatus: number;
} & DefaultSession["user"];
}
interface User {
accountType: number;
activateStatus: number;
}
}
declare module "next-auth/jwt" {
interface JWT {
accountType?: number;
activateStatus?: number;
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
pages: {
signIn: "/login",
},
session: {
strategy: "jwt",
},
providers: [
Credentials({
credentials: {
adminName: { label: "관리자 아이디", type: "text" },
password: { label: "암호", type: "password" },
},
async authorize(credentials) {
const parsedCredentials = credentialsSchema.safeParse(credentials);
if (!parsedCredentials.success) {
return null;
}
const { db } = await import("@/lib/db");
const { adminName, password } = parsedCredentials.data;
const admins = await db.$queryRaw<AdminAuthRow[]>`
SELECT AdminID, Name, Email, AccountType, ActivateStatus
FROM admin
WHERE Name = ${adminName} AND Password = PASSWORD(${password})
LIMIT 1
`;
const admin = admins[0];
const adminID = Number(admin?.AdminID);
const accountType = Number(admin?.AccountType);
const activateStatus = Number(admin?.ActivateStatus);
if (!admin || activateStatus === ACTIVATE_STATUSES.PENDING) {
return null;
}
return {
id: String(adminID),
name: admin.Name,
email: admin.Email,
accountType,
activateStatus,
};
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.accountType = user.accountType;
token.activateStatus = user.activateStatus;
}
return token;
},
session({ session, token }) {
return {
...session,
user: {
...session.user,
adminID: Number(token.sub),
accountType: Number(token.accountType),
activateStatus: Number(token.activateStatus),
},
};
},
},
});
+9 -9
View File
@@ -310,18 +310,18 @@ choco-admin/
### Phase 2. DB 연결 및 타입 구성
- [ ] Prisma Client 싱글톤 (`lib/db.ts`)
- [ ] 상태값 상수 정의 (`lib/constants.ts`)
- [ ] 공통 유틸 작성 (날짜 포맷, AccountType 레이블 변환 등)
- [ ] 개발 DB에서 `maestro`, `maestro_extension`, `maestro_upgrade` 조회 확인
- [x] Prisma Client 싱글톤 (`lib/db.ts`)
- [x] 상태값 상수 정의 (`lib/constants.ts`)
- [x] 공통 유틸 작성 (날짜 포맷, AccountType 레이블 변환 등)
- [x] 개발 DB에서 `maestro`, `maestro_extension`, `maestro_upgrade` 조회 확인
### Phase 3. 인증 및 공통 레이아웃
- [ ] 로그인 페이지 (`/login`)
- [ ] NextAuth Credentials Provider — `admin` 테이블 기반 검증
- [ ] 인증 미들웨어 (`middleware.ts`) — 비로그인 시 `/login` 리다이렉트
- [ ] 공통 레이아웃 (`(admin)/layout.tsx`) — 사이드바, 헤더
- [ ] 로그아웃 기능
- [x] 로그인 페이지 (`/login`)
- [x] NextAuth Credentials Provider — `admin` 테이블 기반 검증
- [x] 인증 프록시 (`proxy.ts`) — 비로그인 시 `/login` 리다이렉트
- [x] 공통 레이아웃 (`(admin)/layout.tsx`) — 사이드바, 헤더
- [x] 로그아웃 기능
### Phase 4. 마에스트로 목록 및 상세
+81
View File
@@ -0,0 +1,81 @@
export const ACCOUNT_TYPES = {
BASIC_20: 1,
STANDARD_50: 2,
PRO_100: 3,
SCHOOL_500: 4,
SCHOOL_1000: 5,
STUDENT_TRIAL: 100,
MAESTRO_TRIAL: 101,
} as const;
export const ACCOUNT_TYPE_LABELS = {
[ACCOUNT_TYPES.BASIC_20]: "20명 (1만원)",
[ACCOUNT_TYPES.STANDARD_50]: "50명 (2만원)",
[ACCOUNT_TYPES.PRO_100]: "100명 (3만원)",
[ACCOUNT_TYPES.SCHOOL_500]: "500명 (4만원)",
[ACCOUNT_TYPES.SCHOOL_1000]: "1,000명 (5만원)",
[ACCOUNT_TYPES.STUDENT_TRIAL]: "학생체험",
[ACCOUNT_TYPES.MAESTRO_TRIAL]: "마에체험",
} as const;
export const TRIAL_ACCOUNT_TYPE_LABELS = {
[ACCOUNT_TYPES.BASIC_20]: "20명 (0원)",
[ACCOUNT_TYPES.STANDARD_50]: "50명 (0원)",
[ACCOUNT_TYPES.PRO_100]: "100명 (0원)",
[ACCOUNT_TYPES.SCHOOL_500]: "500명 (0원)",
[ACCOUNT_TYPES.SCHOOL_1000]: "1,000명 (0원)",
} as const;
export const ACCOUNT_TYPE_PLAYER_COUNTS = {
[ACCOUNT_TYPES.BASIC_20]: 20,
[ACCOUNT_TYPES.STANDARD_50]: 50,
[ACCOUNT_TYPES.PRO_100]: 100,
[ACCOUNT_TYPES.SCHOOL_500]: 500,
[ACCOUNT_TYPES.SCHOOL_1000]: 1000,
} as const;
export const ACCOUNT_TYPE_PRICES = {
[ACCOUNT_TYPES.BASIC_20]: 10000,
[ACCOUNT_TYPES.STANDARD_50]: 20000,
[ACCOUNT_TYPES.PRO_100]: 30000,
[ACCOUNT_TYPES.SCHOOL_500]: 40000,
[ACCOUNT_TYPES.SCHOOL_1000]: 50000,
} as const;
export const ACTIVATE_STATUSES = {
PENDING: 0,
TRIAL: 1,
ACTIVE: 2,
CANCELED: 100,
} as const;
export const ACTIVATE_STATUS_LABELS = {
[ACTIVATE_STATUSES.PENDING]: "미승인",
[ACTIVATE_STATUSES.TRIAL]: "체험",
[ACTIVATE_STATUSES.ACTIVE]: "활성화",
[ACTIVATE_STATUSES.CANCELED]: "등록 취소",
} as const;
export const REQUEST_STATUSES = {
REQUESTED: 1,
CLOSED: 2,
APPLIED: 3,
} as const;
export const REQUEST_STATUS_LABELS = {
[REQUEST_STATUSES.REQUESTED]: "요청",
[REQUEST_STATUSES.CLOSED]: "취소",
[REQUEST_STATUSES.APPLIED]: "적용 완료",
} as const;
export type AccountType = (typeof ACCOUNT_TYPES)[keyof typeof ACCOUNT_TYPES];
export type PaidAccountType =
| typeof ACCOUNT_TYPES.BASIC_20
| typeof ACCOUNT_TYPES.STANDARD_50
| typeof ACCOUNT_TYPES.PRO_100
| typeof ACCOUNT_TYPES.SCHOOL_500
| typeof ACCOUNT_TYPES.SCHOOL_1000;
export type ActivateStatus =
(typeof ACTIVATE_STATUSES)[keyof typeof ACTIVATE_STATUSES];
export type RequestStatus =
(typeof REQUEST_STATUSES)[keyof typeof REQUEST_STATUSES];
+25
View File
@@ -0,0 +1,25 @@
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "@/lib/generated/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
function createPrismaClient() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required to initialize Prisma Client.");
}
const adapter = new PrismaMariaDb(databaseUrl);
return new PrismaClient({ adapter });
}
export const db = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = db;
}
+113
View File
@@ -1,6 +1,119 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import {
ACCOUNT_TYPE_LABELS,
ACCOUNT_TYPE_PLAYER_COUNTS,
ACCOUNT_TYPE_PRICES,
ACTIVATE_STATUS_LABELS,
REQUEST_STATUS_LABELS,
TRIAL_ACCOUNT_TYPE_LABELS,
type AccountType,
type ActivateStatus,
type PaidAccountType,
type RequestStatus,
} from "@/lib/constants";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getAccountTypeLabel(accountType: number): string {
return (
ACCOUNT_TYPE_LABELS[accountType as AccountType] ?? `${accountType}? (N/A)`
);
}
export function getTrialAccountTypeLabel(accountType: number): string {
return (
TRIAL_ACCOUNT_TYPE_LABELS[accountType as PaidAccountType] ??
`${accountType}? (N/A)`
);
}
export function getAccountTypePlayerCount(accountType: number): number {
return ACCOUNT_TYPE_PLAYER_COUNTS[accountType as PaidAccountType] ?? 0;
}
export function getAccountTypePrice(accountType: number): number {
return ACCOUNT_TYPE_PRICES[accountType as PaidAccountType] ?? 0;
}
export function getActivateStatusLabel(activateStatus: number): string {
return (
ACTIVATE_STATUS_LABELS[activateStatus as ActivateStatus] ??
`${activateStatus}? (N/A)`
);
}
export function getRequestStatusLabel(status: number): string {
return REQUEST_STATUS_LABELS[status as RequestStatus] ?? `${status}? (N/A)`;
}
export function calculateUpgradePrice(
requestedAccountType: number,
registeredAccountType: number
): number {
return (
getAccountTypePrice(requestedAccountType) -
getAccountTypePrice(registeredAccountType)
);
}
export function formatDate(value: Date | string | null | undefined): string {
const date = toDate(value);
if (!date) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
export function formatDateTime(
value: Date | string | null | undefined
): string {
const date = toDate(value);
if (!date) {
return "";
}
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${formatDate(date)} ${hours}:${minutes}:${seconds}`;
}
export function calculateExtendedAvailableDate(
availableDate: Date,
now = new Date()
): Date {
const baseDate =
availableDate.getTime() < now.getTime() ? now : availableDate;
const extendedDate = new Date(baseDate);
extendedDate.setFullYear(extendedDate.getFullYear() + 1);
extendedDate.setHours(0, 0, 0, 0);
return extendedDate;
}
function toDate(value: Date | string | null | undefined): Date | null {
if (!value) {
return null;
}
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date;
}
+4
View File
@@ -7,16 +7,19 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
"db:check": "tsx scripts/check-db.ts",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@prisma/adapter-mariadb": "^7.8.0",
"@prisma/client": "^7.8.0",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"mariadb": "^3.5.2",
"next": "16.2.7",
"next-auth": "5.0.0-beta.31",
"react": "19.2.4",
@@ -37,6 +40,7 @@
"prettier": "^3.8.4",
"prisma": "^7.8.0",
"tailwindcss": "^4",
"tsx": "^4.22.4",
"typescript": "^5"
}
}
+365
View File
@@ -11,6 +11,9 @@ importers:
'@base-ui/react':
specifier: ^1.5.0
version: 1.5.0(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@prisma/adapter-mariadb':
specifier: ^7.8.0
version: 7.8.0
'@prisma/client':
specifier: ^7.8.0
version: 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
@@ -26,6 +29,9 @@ importers:
lucide-react:
specifier: ^1.17.0
version: 1.17.0(react@19.2.4)
mariadb:
specifier: ^3.5.2
version: 3.5.2
next:
specifier: 16.2.7
version: 16.2.7(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -81,6 +87,9 @@ importers:
tailwindcss:
specifier: ^4
version: 4.3.0
tsx:
specifier: ^4.22.4
version: 4.22.4
typescript:
specifier: ^5
version: 5.9.3
@@ -301,6 +310,162 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@esbuild/aix-ppc64@0.28.0':
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.0':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.0':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.0':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.0':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.0':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.0':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.0':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.0':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.0':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.0':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.0':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.0':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.0':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.0':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.0':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.0':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.0':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.0':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.0':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.0':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.0':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.0':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.0':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.0':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.0':
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -657,6 +822,9 @@ packages:
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@prisma/adapter-mariadb@7.8.0':
resolution: {integrity: sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==}
'@prisma/client-runtime-utils@7.8.0':
resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==}
@@ -684,6 +852,9 @@ packages:
'@prisma/dev@0.24.3':
resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==}
'@prisma/driver-adapter-utils@7.8.0':
resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==}
'@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a':
resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==}
@@ -916,6 +1087,9 @@ packages:
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -925,6 +1099,9 @@ packages:
'@types/node@20.19.42':
resolution: {integrity: sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==}
'@types/node@24.13.1':
resolution: {integrity: sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -1592,6 +1769,11 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1852,6 +2034,11 @@ packages:
resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
engines: {node: '>=14.14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -2003,6 +2190,10 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
@@ -2384,6 +2575,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -2399,6 +2593,14 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mariadb@3.4.5:
resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==}
engines: {node: '>= 14'}
mariadb@3.5.2:
resolution: {integrity: sha512-9rztrI4nouxAY/82a+RlzzZ5ie2vxu2eYclkBvTy1ATXH1B9cnvZ0O71Pzsy/mlfDb5P3HhOg0JzQKkDRhctyA==}
engines: {node: '>= 18'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -3095,6 +3297,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.22.4:
resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
engines: {node: '>=18.0.0'}
hasBin: true
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -3141,6 +3348,9 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
@@ -3537,6 +3747,84 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.28.0':
optional: true
'@esbuild/android-arm64@0.28.0':
optional: true
'@esbuild/android-arm@0.28.0':
optional: true
'@esbuild/android-x64@0.28.0':
optional: true
'@esbuild/darwin-arm64@0.28.0':
optional: true
'@esbuild/darwin-x64@0.28.0':
optional: true
'@esbuild/freebsd-arm64@0.28.0':
optional: true
'@esbuild/freebsd-x64@0.28.0':
optional: true
'@esbuild/linux-arm64@0.28.0':
optional: true
'@esbuild/linux-arm@0.28.0':
optional: true
'@esbuild/linux-ia32@0.28.0':
optional: true
'@esbuild/linux-loong64@0.28.0':
optional: true
'@esbuild/linux-mips64el@0.28.0':
optional: true
'@esbuild/linux-ppc64@0.28.0':
optional: true
'@esbuild/linux-riscv64@0.28.0':
optional: true
'@esbuild/linux-s390x@0.28.0':
optional: true
'@esbuild/linux-x64@0.28.0':
optional: true
'@esbuild/netbsd-arm64@0.28.0':
optional: true
'@esbuild/netbsd-x64@0.28.0':
optional: true
'@esbuild/openbsd-arm64@0.28.0':
optional: true
'@esbuild/openbsd-x64@0.28.0':
optional: true
'@esbuild/openharmony-arm64@0.28.0':
optional: true
'@esbuild/sunos-x64@0.28.0':
optional: true
'@esbuild/win32-arm64@0.28.0':
optional: true
'@esbuild/win32-ia32@0.28.0':
optional: true
'@esbuild/win32-x64@0.28.0':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))':
dependencies:
eslint: 9.39.4(jiti@2.7.0)
@@ -3821,6 +4109,11 @@ snapshots:
'@panva/hkdf@1.2.1': {}
'@prisma/adapter-mariadb@7.8.0':
dependencies:
'@prisma/driver-adapter-utils': 7.8.0
mariadb: 3.4.5
'@prisma/client-runtime-utils@7.8.0': {}
'@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)':
@@ -3865,6 +4158,10 @@ snapshots:
transitivePeerDependencies:
- typescript
'@prisma/driver-adapter-utils@7.8.0':
dependencies:
'@prisma/debug': 7.8.0
'@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {}
'@prisma/engines@7.8.0':
@@ -4065,6 +4362,8 @@ snapshots:
'@types/estree@1.0.9': {}
'@types/geojson@7946.0.16': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
@@ -4073,6 +4372,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/node@24.13.1':
dependencies:
undici-types: 7.18.2
'@types/react-dom@19.2.3(@types/react@19.2.17)':
dependencies:
'@types/react': 19.2.17
@@ -4766,6 +5069,35 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
esbuild@0.28.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
'@esbuild/android-arm': 0.28.0
'@esbuild/android-arm64': 0.28.0
'@esbuild/android-x64': 0.28.0
'@esbuild/darwin-arm64': 0.28.0
'@esbuild/darwin-x64': 0.28.0
'@esbuild/freebsd-arm64': 0.28.0
'@esbuild/freebsd-x64': 0.28.0
'@esbuild/linux-arm': 0.28.0
'@esbuild/linux-arm64': 0.28.0
'@esbuild/linux-ia32': 0.28.0
'@esbuild/linux-loong64': 0.28.0
'@esbuild/linux-mips64el': 0.28.0
'@esbuild/linux-ppc64': 0.28.0
'@esbuild/linux-riscv64': 0.28.0
'@esbuild/linux-s390x': 0.28.0
'@esbuild/linux-x64': 0.28.0
'@esbuild/netbsd-arm64': 0.28.0
'@esbuild/netbsd-x64': 0.28.0
'@esbuild/openbsd-arm64': 0.28.0
'@esbuild/openbsd-x64': 0.28.0
'@esbuild/openharmony-arm64': 0.28.0
'@esbuild/sunos-x64': 0.28.0
'@esbuild/win32-arm64': 0.28.0
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -5153,6 +5485,9 @@ snapshots:
jsonfile: 6.2.1
universalify: 2.0.1
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -5295,6 +5630,10 @@ snapshots:
human-signals@8.0.1: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
@@ -5613,6 +5952,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
lru-cache@10.4.3: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -5627,6 +5968,22 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mariadb@3.4.5:
dependencies:
'@types/geojson': 7946.0.16
'@types/node': 24.13.1
denque: 2.1.0
iconv-lite: 0.6.3
lru-cache: 10.4.3
mariadb@3.5.2:
dependencies:
'@types/geojson': 7946.0.16
'@types/node': 20.19.42
denque: 2.1.0
iconv-lite: 0.7.2
lru-cache: 10.4.3
math-intrinsics@1.1.0: {}
media-typer@1.1.0: {}
@@ -6425,6 +6782,12 @@ snapshots:
tslib@2.8.1: {}
tsx@4.22.4:
dependencies:
esbuild: 0.28.0
optionalDependencies:
fsevents: 2.3.3
tw-animate-css@1.4.0: {}
type-check@0.4.0:
@@ -6492,6 +6855,8 @@ snapshots:
undici-types@6.21.0: {}
undici-types@7.18.2: {}
unicorn-magic@0.3.0: {}
universalify@2.0.1: {}
+30
View File
@@ -0,0 +1,30 @@
import { getToken } from "next-auth/jwt";
import { NextResponse, type NextRequest } from "next/server";
const PUBLIC_PATHS = ["/login"];
export async function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const isPublicPath = PUBLIC_PATHS.includes(pathname);
const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET,
});
if (!token && !isPublicPath) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", `${pathname}${search}`);
return NextResponse.redirect(loginUrl);
}
if (token && isPublicPath) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
};
+87
View File
@@ -0,0 +1,87 @@
import { config } from "dotenv";
config({ path: ".env.local" });
async function main() {
const { db } = await import("@/lib/db");
const { getAccountTypeLabel, getActivateStatusLabel, getRequestStatusLabel } =
await import("@/lib/utils");
const [maestroCount, extensionCount, upgradeCount] = await Promise.all([
db.maestro.count(),
db.maestro_extension.count(),
db.maestro_upgrade.count(),
]);
const [maestros, extensionRequests, upgradeRequests] = await Promise.all([
db.maestro.findMany({
orderBy: { MaestroID: "desc" },
select: {
MaestroID: true,
Name: true,
AccountType: true,
ActivateStatus: true,
PlayerCount: true,
},
take: 3,
}),
db.maestro_extension.findMany({
orderBy: { MaestroExtensionID: "desc" },
select: {
MaestroExtensionID: true,
MaestroID: true,
AccountType: true,
Status: true,
},
take: 3,
}),
db.maestro_upgrade.findMany({
orderBy: { MaestroUpgradeID: "desc" },
select: {
MaestroUpgradeID: true,
MaestroID: true,
RegisteredAccountType: true,
RequestedAccountType: true,
Status: true,
},
take: 3,
}),
]);
console.log("DB connection OK");
console.log({
counts: {
maestro: maestroCount,
maestro_extension: extensionCount,
maestro_upgrade: upgradeCount,
},
maestros: maestros.map((maestro) => ({
id: maestro.MaestroID,
name: maestro.Name,
accountType: getAccountTypeLabel(maestro.AccountType),
activateStatus: getActivateStatusLabel(maestro.ActivateStatus),
playerCount: maestro.PlayerCount,
})),
extensionRequests: extensionRequests.map((request) => ({
id: request.MaestroExtensionID,
maestroId: request.MaestroID,
accountType: getAccountTypeLabel(request.AccountType),
status: getRequestStatusLabel(request.Status),
})),
upgradeRequests: upgradeRequests.map((request) => ({
id: request.MaestroUpgradeID,
maestroId: request.MaestroID,
from: getAccountTypeLabel(request.RegisteredAccountType),
to: getAccountTypeLabel(request.RequestedAccountType),
status: getRequestStatusLabel(request.Status),
})),
});
await db.$disconnect();
}
main().catch((error) => {
console.error("DB connection check failed");
console.error(error);
process.exitCode = 1;
});
+15
View File
@@ -0,0 +1,15 @@
export type {
admin,
maestro,
maestro_extension,
maestro_log,
maestro_upgrade,
player,
} from "@/lib/generated/prisma/client";
export type {
AccountType,
ActivateStatus,
PaidAccountType,
RequestStatus,
} from "@/lib/constants";