diff --git a/.env.example b/.env.example
index 4856f23..d8c33dc 100644
--- a/.env.example
+++ b/.env.example
@@ -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"
diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx
new file mode 100644
index 0000000..9abd3f2
--- /dev/null
+++ b/app/(admin)/layout.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx
new file mode 100644
index 0000000..cd5364c
--- /dev/null
+++ b/app/(admin)/page.tsx
@@ -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 (
+
+
+
대시보드
+
+ {session?.user.name} 관리자 계정으로 로그인했습니다.
+
+
+
+
+ {checks.map((item) => {
+ const Icon = item.icon;
+
+ return (
+
+
+
+
+ {item.label}
+ {item.value}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/app/(auth)/login/actions.ts b/app/(auth)/login/actions.ts
new file mode 100644
index 0000000..0288193
--- /dev/null
+++ b/app/(auth)/login/actions.ts
@@ -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;
+ }
+}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..a711fd9
--- /dev/null
+++ b/app/(auth)/login/page.tsx
@@ -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 (
+
+
+
+
+ choco-admin
+
+
관리자 로그인
+
+
+ {hasError ? (
+
+ 관리자 아이디 또는 암호가 올바르지 않습니다.
+
+ ) : null}
+
+
+
+
+ );
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..2e06971
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1 @@
+export { GET, POST } from "@/auth";
diff --git a/app/page.tsx b/app/page.tsx
deleted file mode 100644
index 3e4cb25..0000000
--- a/app/page.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- {checks.map((item) => {
- const Icon = item.icon;
-
- return (
-
-
-
-
- {item.label}
-
- {item.value}
-
-
- );
- })}
-
-
-
- );
-}
diff --git a/auth.ts b/auth.ts
new file mode 100644
index 0000000..b6f27cd
--- /dev/null
+++ b/auth.ts
@@ -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`
+ 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),
+ },
+ };
+ },
+ },
+});
diff --git a/docs/plan/project-plan.md b/docs/plan/project-plan.md
index 0daea3d..c01aabd 100644
--- a/docs/plan/project-plan.md
+++ b/docs/plan/project-plan.md
@@ -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. 마에스트로 목록 및 상세
diff --git a/lib/constants.ts b/lib/constants.ts
new file mode 100644
index 0000000..c981fe8
--- /dev/null
+++ b/lib/constants.ts
@@ -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];
diff --git a/lib/db.ts b/lib/db.ts
new file mode 100644
index 0000000..4837a07
--- /dev/null
+++ b/lib/db.ts
@@ -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;
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index a5ef193..7d21bdd 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -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;
+}
diff --git a/package.json b/package.json
index 25eb28d..f2dfaa3 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 102af73..46ca81d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: {}
diff --git a/proxy.ts b/proxy.ts
new file mode 100644
index 0000000..afd76d4
--- /dev/null
+++ b/proxy.ts
@@ -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|.*\\..*).*)"],
+};
diff --git a/scripts/check-db.ts b/scripts/check-db.ts
new file mode 100644
index 0000000..a78d9d3
--- /dev/null
+++ b/scripts/check-db.ts
@@ -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;
+});
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..d1f730a
--- /dev/null
+++ b/types/index.ts
@@ -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";