12 KiB
12 KiB
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. Ifcount === 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, andmaestro_logmust be updated atomically. - Send email only after a successful commit, never inside the transaction.
- Never use
include: { maestro: true }when queryingmaestro_extensionormaestro_upgrade. Always useselectwith only the needed columns to avoid logging the maestroPasswordfield. - Admin password verification uses MariaDB
PASSWORD()function — the existingadmintable 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 changePlayerCounton upgrade. - When approving an extension or upgrade, close all other
Status = 1requests for the sameMaestroIDby setting them toStatus = 2, then set the approved request toStatus = 3. - Docker containers must set
TZ: Asia/Seoulso 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 isstage-chocoadmin— never rename it tochocoadmin. Identical names cause DNS round-robin inproxy-network; NPM'sset $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 respondsFailed to find Server Action. Always extract to a top-level export in a separateactions.tsfile (e.g.app/(admin)/actions.ts). - After redeployment, reload NPM nginx: Run
docker exec npm nginx -s reloadon 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) requireActivateStatus = 2(ACTIVE). Trial (1) and cancelled (100) accounts are rejected with 400. Approval (PATCH /api/extension-requests/[id]) re-checksActivateStatusand 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 currentAccountType— 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 insideapp/. - Unauthenticated requests are caught by
proxy.ts(middleware) and redirected to/login. - API routes must call
auth()and return 401 if no session. - Session
maxAgeis 8 hours — do not lengthen without review. auth.tstracks failed login attempts per admin name in an in-memoryMap. After 5 consecutive failures the account is locked out for 15 minutes. Counter clears on success.- Session cookie name
${APP_ENV}-chocoadmin.session-tokenmust be set in bothauth.ts(cookies.sessionToken.name) andproxy.ts(getToken({ cookieName })). Setting onlyauth.tscausesproxy.tsto look for the default cookie name, which is never written → middleware redirects every request to/login→ login redirects back to/→ infiniteERR_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 0–8: login, maestro list/detail, extension requests, upgrade requests, Docker setup, transaction safety, auth guards.
- Phase 9 (substantially complete): structured logging (
lib/logger.ts),withApiHandlerwrapper (lib/api-handler.ts),ApiErrorclass (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 intomaestro_extensionusing maestro's currentAccountTyperead server-side; rejects trial account types.POST /api/maestros/[id]/upgrade-requests— inserts intomaestro_upgradeinside a transaction; validatesrequestedAccountType > maestro.AccountType.ExtensionRequestsSection.tsx,UpgradeRequestsSection.tsx— client components with inline action UI; callrouter.refresh()on success.PAID_ACCOUNT_TYPE_VALUESconsolidated inlib/constants.ts— shared bylib/maestros.ts,lib/extension-requests.ts,lib/upgrade-requests.ts, and both new API routes.
- Phase 15:
- Password reset button (
암호 초기화) added toMaestroEditForm.tsx— callsPOST /api/maestros/[id]/reset-password, showswindow.confirmwith maestro name, displays inline success/error message. resetMaestroPassword(maestroID)added tolib/maestros.ts— runsUPDATE maestro SET Password = PASSWORD('123456')inside a$transactionwith areset_maestro_passwordlog entry.app/api/maestros/[id]/reset-password/route.ts—POSThandler wrapped withwithApiHandler.- Deployment shell scripts added:
scripts/deploy-stage.shandscripts/deploy-production.sh— each runs git pull, ensures proxy-network,docker compose up -d --build --remove-orphans, anddocker exec npm nginx -s reload. Production script requiresyesconfirmation.
- Password reset button (
- Phase 13 (security/correctness fixes):
- P0-1:
getActionTargetinlib/extension-requests.tsreplaced with inlineselect(noinclude: { maestro: true }) — preventsPasswordfrom appearing in debug result logs. - P0-2: Approve/cancel/reject flows in both
lib/extension-requests.tsandlib/upgrade-requests.tsnow use an atomicupdateMany({ Status: REQUESTED → target })guard;count === 0triggers 404/409 — eliminates concurrent double-processing. - P0-3:
docker-compose.ymlanddocker-compose.stage.ymlnow setTZ: Asia/Seoul;lib/maestros.tsdate-change log remark usesformatDate()instead oftoISOString().slice(0,10)for KST consistency. - P1-1/P1-2:
createExtensionRequestandapproveExtensionRequestrequireActivateStatus === ACTIVE;UpgradeRequestsSectionandcreateUpgradeRequestallow TRIAL same-tier upgrade. - P1-3:
approveUpgradeRequestre-verifies upgrade direction against maestro's currentAccountTypeat approval time. - P1-4/P1-5:
createExtensionRequestwrapped indb.$transaction; both create functions writerequest_extension_maestro/request_upgrade_maestrolog entries. - P1-6: Extension/upgrade approval routes now
awaitemail send and includeemailSent: booleanin response; table UI shows inline amber warning whenemailSent === false. - P2-2:
updateMaestronow returns 400 instead of silently skipping whenavailableActivateDateTimecannot be parsed. - P2-5:
withApiHandlercatch block returns{ message: "Internal server error" }JSON 500 instead of re-throwing (which produced HTML responses). - P2-6: NextAuth session
maxAgeset 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
statusfilter for extension/upgrade request lists changed fromundefined(all) toREQUEST_STATUSES.REQUESTED; reset links updated to?status=1.
- P0-1:
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 componentszodv4 — Zod v4 API differs from v3; check docs before writing schemasnext-authv5 beta — API surface differs significantly from v4