Files

14 KiB
Raw Permalink Blame History

This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ before writing any code. Heed deprecation notices.


Project: chocoadmin

Admin panel for the chocomae service (mouse-typing), extracted into a standalone Next.js 16 app. It shares the existing MariaDB database — do not modify the DB schema.

Critical rules

  • Never alter the DB schema. All tables are shared with the live chocomae service.
  • Race-condition guard for approve/cancel/reject: use a conditional updateMany({ where: { id, Status: REQUEST_STATUSES.REQUESTED }, data: { Status: <target> } }) as the first step inside a transaction. If count === 0, check existence to return 404 vs 409. Never use read-then-write (findUnique → check Status → update) — it allows double-processing under concurrent requests.
  • Wrap every mutation in a transaction: maestro, maestro_extension/maestro_upgrade, and maestro_log must be updated atomically.
  • Send email only after a successful commit, never inside the transaction.
  • Never use include: { maestro: true } when querying maestro_extension or maestro_upgrade. Always use select with only the needed columns to avoid logging the maestro Password field.
  • Admin password verification uses MariaDB PASSWORD() function — the existing admin table stores hashed passwords this way. Do not change this without a migration plan.
  • Extension approval date calculation happens server-side: if current expiry is in the past, use today +1 year; otherwise use current expiry +1 year.
  • Upgrade approval sets AvailableActivateDateTime = NOW() + 1 year. Do not change PlayerCount on upgrade.
  • When approving an extension or upgrade, close all other Status = 1 requests for the same MaestroID by setting them to Status = 2, then set the approved request to Status = 3.
  • Docker containers must set TZ: Asia/Seoul so that date calculations (setHours(0,0,0,0), formatDate) match the KST-based existing data in the shared MariaDB.
  • Docker service names must be unique per environment: Docker registers each services: key as a DNS alias in shared networks. The stage service is chocoadmin-stagenever rename it to chocoadmin. Identical names cause DNS round-robin in proxy-network; NPM's set $server "chocoadmin" will randomly route requests to either container, mixing production and stage traffic.
  • Server actions must be module-level named exports: Inline action={async () => { "use server"; ... }} closures get a new action ID on every build. After redeployment the browser sends the stale ID and Next.js responds Failed to find Server Action. Always extract to a top-level export in a separate actions.ts file (e.g. app/(admin)/actions.ts).
  • After redeployment, reload NPM nginx: Run docker exec npm nginx -s reload on the NAS to flush the upstream DNS cache. Without this NPM may continue routing to the previous container's IP.
  • Extension requests (POST /api/maestros/[id]/extension-requests) require ActivateStatus = 2 (ACTIVE). Trial (1) and cancelled (100) accounts are rejected with 400. Approval (PATCH /api/extension-requests/[id]) re-checks ActivateStatus and throws 409 if not ACTIVE (transaction rolls back the atomic claim).
  • Upgrade requests: TRIAL accounts (ActivateStatus = 1) may request the same tier (trial-to-paid conversion, requestedAccountType >= AccountType). ACTIVE accounts must use a strictly higher tier (>). At approval time, this direction is re-verified against the maestro's current AccountType — if it became invalid (e.g., admin manually raised the tier), the approval throws 409 and rolls back.
  • Admin-initiated extension/upgrade registrations do not send an email (unlike self-registration flows which send payment-instruction emails). This is intentional.

Authentication

  • NextAuth.js v5 (next-auth@5.0.0-beta.31) with Credentials Provider.
  • Config lives in auth.ts (project root), not inside app/.
  • Unauthenticated requests are caught by proxy.ts (middleware) and redirected to /login.
  • API routes must call auth() and return 401 if no session.
  • Session maxAge is 8 hours — do not lengthen without review.
  • auth.ts tracks failed login attempts per admin name in an in-memory Map. After 5 consecutive failures the account is locked out for 15 minutes. Counter clears on success.
  • Session cookie name chocoadmin-${APP_ENV}.session-token must be set in both auth.ts (cookies.sessionToken.name) and proxy.ts (getToken({ cookieName })). Setting only auth.ts causes proxy.ts to look for the default cookie name, which is never written → middleware redirects every request to /login → login redirects back to / → infinite ERR_TOO_MANY_REDIRECTS. In NextAuth v5 the JWT encryption salt equals the cookie name, so both must match exactly.
  • Do not add __Secure- prefix to the cookie name. Next.js 16 middleware runs in Node.js runtime (not Edge) and receives HTTP from Nginx Proxy Manager — __Secure- cookies are silently rejected over HTTP, producing the same redirect loop.

Key files

File Purpose
auth.ts NextAuth config (Credentials Provider, MariaDB PASSWORD verify)
proxy.ts Middleware — redirects unauthenticated users to /login
lib/db.ts Prisma Client singleton with SQL pretty-logging and result table logging
lib/logger.ts Structured logger — debug/info/warn/error, level controlled by LOG_LEVEL / APP_ENV
lib/errors.ts ApiError — unified error class with HTTP status code
lib/api-handler.ts withApiHandler — wraps route handlers with auth, timing, and error logging
lib/constants.ts Status code constants (ACTIVATE_STATUS, ACCOUNT_TYPE, REQUEST_STATUS); PAID_ACCOUNT_TYPE_VALUES (유료 계정 유형 배열 — 중복 정의 방지용 공용 상수)
lib/maestros.ts Maestro DB queries
lib/extension-requests.ts Extension request queries and approval logic
lib/upgrade-requests.ts Upgrade request queries and approval logic
prisma/schema.prisma Generated via prisma db pull — do not hand-edit model fields
docs/business-rules.md Full reference for existing chocomae business logic
docs/phase/phase9.md Log policy: levels, format, masking rules, Docker log operations

maestro_log conventions

Always record a log entry after a successful DB change. Use these Type values:

Type When
extension_maestro Extension approved
cancel_extension_maestro Extension cancelled (new type, not in original)
upgrade_maestro Upgrade approved
reject_upgrade_maestro Upgrade rejected (new type, not in original)
update_maestro_name Maestro name changed
update_maestro_email Maestro email changed
update_maestro_status ActivateStatus changed (admin, new)
update_maestro_account_type AccountType changed (admin, new)
update_maestro_available_date AvailableActivateDateTime changed (admin, new)
update_maestro_allow_enter_code AllowEditEnterCode changed (admin, new)
request_extension_maestro Admin-initiated extension request created
request_upgrade_maestro Admin-initiated upgrade request created
reset_maestro_password Maestro password reset to 123456 by admin

Remark column is char(100) — keep values under 100 characters.

Implemented phases (do not re-implement)

  • Phase 08: login, maestro list/detail, extension requests, upgrade requests, Docker setup, transaction safety, auth guards.

  • Phase 9 (substantially complete): structured logging (lib/logger.ts), withApiHandler wrapper (lib/api-handler.ts), ApiError class (lib/errors.ts), Prisma SQL pretty-logging and result table logging (lib/db.ts). Remaining: correlation IDs, stage/production log verification.

  • Phase 10: Maestro info edit form (PATCH /api/maestros/[id]) + student list with server-side pagination (GET /api/maestros/[id]/students).

  • Phase 11: Email auto-send via Nodemailer after extension/upgrade approval (lib/mail.ts). Upgrade email edge-case verification (trial→paid, paid→paid) still pending.

  • Phase 12: Admin-initiated extension/upgrade request registration from maestro detail page.

    • POST /api/maestros/[id]/extension-requests — inserts into maestro_extension using maestro's current AccountType read server-side; rejects trial account types.
    • POST /api/maestros/[id]/upgrade-requests — inserts into maestro_upgrade inside a transaction; validates requestedAccountType > maestro.AccountType.
    • ExtensionRequestsSection.tsx, UpgradeRequestsSection.tsx — client components with inline action UI; call router.refresh() on success.
    • PAID_ACCOUNT_TYPE_VALUES consolidated in lib/constants.ts — shared by lib/maestros.ts, lib/extension-requests.ts, lib/upgrade-requests.ts, and both new API routes.
  • Phase 15:

    • Password reset button (암호 초기화) added to MaestroEditForm.tsx — calls POST /api/maestros/[id]/reset-password, shows window.confirm with maestro name, displays inline success/error message.
    • resetMaestroPassword(maestroID) added to lib/maestros.ts — runs UPDATE maestro SET Password = PASSWORD('123456') inside a $transaction with a reset_maestro_password log entry.
    • app/api/maestros/[id]/reset-password/route.tsPOST handler wrapped with withApiHandler.
    • Deployment shell scripts added: scripts/deploy-stage.sh and scripts/deploy-production.sh — each runs git pull, ensures proxy-network, docker compose up -d --build --remove-orphans, and docker exec npm nginx -s reload. Production script requires yes confirmation.
  • Phase 13 (security/correctness fixes):

    • P0-1: getActionTarget in lib/extension-requests.ts replaced with inline select (no include: { maestro: true }) — prevents Password from appearing in debug result logs.
    • P0-2: Approve/cancel/reject flows in both lib/extension-requests.ts and lib/upgrade-requests.ts now use an atomic updateMany({ Status: REQUESTED → target }) guard; count === 0 triggers 404/409 — eliminates concurrent double-processing.
    • P0-3: docker-compose.yml and docker-compose.stage.yml now set TZ: Asia/Seoul; lib/maestros.ts date-change log remark uses formatDate() instead of toISOString().slice(0,10) for KST consistency.
    • P1-1/P1-2: createExtensionRequest and approveExtensionRequest require ActivateStatus === ACTIVE; UpgradeRequestsSection and createUpgradeRequest allow TRIAL same-tier upgrade.
    • P1-3: approveUpgradeRequest re-verifies upgrade direction against maestro's current AccountType at approval time.
    • P1-4/P1-5: createExtensionRequest wrapped in db.$transaction; both create functions write request_extension_maestro / request_upgrade_maestro log entries.
    • P1-6: Extension/upgrade approval routes now await email send and include emailSent: boolean in response; table UI shows inline amber warning when emailSent === false.
    • P2-2: updateMaestro now returns 400 instead of silently skipping when availableActivateDateTime cannot be parsed.
    • P2-5: withApiHandler catch block returns { message: "Internal server error" } JSON 500 instead of re-throwing (which produced HTML responses).
    • P2-6: NextAuth session maxAge set to 8 hours (auth.ts).
    • P2-7: In-memory brute-force protection added to auth.ts — 5 failures → 15-minute lockout per admin name.
    • P2-8: Default status filter for extension/upgrade request lists changed from undefined (all) to REQUEST_STATUSES.REQUESTED; reset links updated to ?status=1.
  • Phase 16: 반응형 웹 디자인 (Responsive Web Design)

    • app/(admin)/nav-config.ts — navItems 배열 공유 설정 파일 (layout.tsx·MobileNav.tsx 공용)
    • app/(admin)/MobileNav.tsx — 모바일 슬라이드인 사이드바 클라이언트 컴포넌트. 햄버거 버튼(md:hidden), 반투명 오버레이(탭하면 닫힘), X 버튼, ESC 키, pathname 변경 시 자동 닫힘, body 스크롤 잠금 포함. CSS translate-x 트랜지션으로 애니메이션.
    • app/(admin)/layout.tsx — 헤더에 <MobileNav /> 통합. 데스크탑 <aside>(hidden md:block)는 그대로 유지.
    • ExtensionRequestsSection.tsx, UpgradeRequestsSection.tsx<section>overflow-hidden 추가. SSR props로 표가 초기 렌더에 포함되어 min-w-[560px]가 섹션을 밀어내던 문제 수정. 헤더를 flex-col sm:flex-row로 변경해 모바일에서 액션 버튼 접근성 확보.
    • 페이지네이션 <nav> 5개 파일(extension-requests/page.tsx, upgrade-requests/page.tsx, maestros/page.tsx, StudentsSection.tsx, LogsSection.tsx) — self-end sm:self-auto 추가로 모바일에서 오른쪽 정렬.

Planned phases (not yet implemented)

이후 작업 후보는 docs/plan/project-plan.md Section 10 "MVP 이후 확장 후보" 참조.

Packages to know

  • mariadb + @prisma/adapter-mariadb — Prisma uses the mariadb driver directly (not mysql2)
  • @base-ui/react — used instead of Radix UI primitives for shadcn components
  • zod v4 — Zod v4 API differs from v3; check docs before writing schemas
  • next-auth v5 beta — API surface differs significantly from v4