From dbf9b3e18079b26039f72f3796e9dfc48bd1924f Mon Sep 17 00:00:00 2001 From: jisangs Date: Tue, 9 Jun 2026 23:37:33 +0900 Subject: [PATCH] =?UTF-8?q?Phase=203.=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + app/(admin)/layout.tsx | 84 +++++++ app/(admin)/page.tsx | 55 +++++ app/(auth)/login/actions.ts | 26 ++ app/(auth)/login/page.tsx | 81 ++++++ app/api/auth/[...nextauth]/route.ts | 1 + app/page.tsx | 62 ----- auth.ts | 115 +++++++++ docs/plan/project-plan.md | 18 +- lib/constants.ts | 81 ++++++ lib/db.ts | 25 ++ lib/utils.ts | 113 +++++++++ package.json | 4 + pnpm-lock.yaml | 365 ++++++++++++++++++++++++++++ proxy.ts | 30 +++ scripts/check-db.ts | 87 +++++++ types/index.ts | 15 ++ 17 files changed, 1092 insertions(+), 71 deletions(-) create mode 100644 app/(admin)/layout.tsx create mode 100644 app/(admin)/page.tsx create mode 100644 app/(auth)/login/actions.ts create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/api/auth/[...nextauth]/route.ts delete mode 100644 app/page.tsx create mode 100644 auth.ts create mode 100644 lib/constants.ts create mode 100644 lib/db.ts create mode 100644 proxy.ts create mode 100644 scripts/check-db.ts create mode 100644 types/index.ts 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 ( +
+ + +
+
+
+

{session.user.name}

+

+ 관리자 ID {session.user.adminID} +

+
+
{ + "use server"; + + await signOut({ redirectTo: "/login" }); + }} + > + +
+
+ +
{children}
+
+
+ ); +} 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 ( -
-
-
-
-

- choco-admin -

-

- Phase 1 initialization -

-
- -
- -
- {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";