16 KiB
MC-Manager — Claude Code Instructions
Project Overview
Minecraft Server Manager — full-stack Next.js 16 (App Router) web app for managing Minecraft server instances. Each server runs in its own Docker container.
Commands
npm run dev # Dev server (port 3000, Turbopack)
npm run build # Production build
npm run lint # ESLint
npm run seed # Seed DB with default roles + admin user (tsx scripts/seed.ts)
Actual Tech Stack (exact versions matter)
- Framework: Next.js 16.1.6, React 19.2.3, TypeScript 5 (strict)
- Styling: Tailwind CSS 4 — uses
@import "tailwindcss"syntax, NOT@tailwind base/components/utilities - Database: MongoDB via Mongoose 9.1.6 — cached connection in
src/lib/mongodb.ts - Auth: JWT dual-token (1h access + 7d refresh) in HTTP-only cookies; mandatory 2FA via email; bcryptjs (12 rounds)
- Containers: dockerode 4.0.9 — Docker Engine API via Unix socket
- Scheduling: node-cron 4.2.1 — runs inside Next.js process (no external scheduler)
- Icons: Lucide React only — never use any other icon library
- Email: Microsoft Graph API primary (
src/lib/email-graph.ts), SMTP/nodemailer fallback — always dual-provider
Project Structure
src/app/ — Pages + API routes (App Router)
src/components/ — Shared UI; templates/DataManagementTemplate for CRUD pages
src/contexts/ — AuthContext, ToastContext, ConfirmationContext
src/hooks/ — useConsole, useServerStatus, useAuth, useToast, useConfirmation
src/lib/ — Server-only utilities (never import in client components):
auth.ts — validateSession, generateAccessToken/RefreshToken, verifyPassword,
hashPassword, hasPermission, setAuthCookies, clearAuthCookies,
generate2FACode, hash2FACode, verify2FACode, send2FAEmail, sendEmail
email-graph.ts — getMsalClient, getGraphClient, sendGraphEmail (Azure MSAL)
mongodb.ts — connectToDatabase() (cached Mongoose connection)
models.ts — User, Role, Server, Backup, AuditLog (Mongoose schemas + TS interfaces)
docker.ts — getDockerClient, createServerContainer, ensureServerDirectory,
getContainerByName, getContainerById, mapContainerState,
getItzgServerType, getServersBasePath, getServerPath
backup-scheduler.ts — initBackupScheduler, registerBackupJob, removeBackupJob, updateBackupJob
audit.ts — createAuditLog, getClientIP
input-validation.ts — sanitizeString, sanitizeHtml, sanitizeObject, isValidObjectId,
isValidEmail, isValidCron, isValidPort
date-utils.ts — formatDate, formatDateTime, formatDateForInput, formatFileSize,
formatRelativeTime
src/types/ — server.ts (Server, ServerType, supportsPlugins, supportsMods),
user.ts (User, AuthUser, Role, Permission, SessionPayload),
backup.ts (Backup, BackupType, BackupStatus)
src/middleware.ts — Next.js middleware
Critical Conventions
Pages
- Every page MUST start with
'use client' - Add
export const dynamic = 'force-dynamic'only if the page usesuseSearchParams() - Never use raw
<img>— alwaysnext/image - Allowed remote image domains:
mc-heads.net,crafatar.com
Dates & Sizes — NEVER create custom formatters
Always import from @/lib/date-utils:
formatDate(date) // "Jan 15, 2026"
formatDateTime(date) // "Jan 15, 2026 at 2:30 PM"
formatDateForInput(date) // "2026-01-15" (for <input type="date">)
formatFileSize(bytes) // "1.5 GB", "256 MB", "12 KB"
formatRelativeTime(date) // "2 hours ago", "just now"
CRUD Pages
Use DataManagementTemplate from @/components/templates/ — provides search, filter, sort, table, empty state, and create button. Never build custom data tables from scratch.
Drawers over Modals for detail views
Drawers use max-w-3xl animate-slide-in-right. Modals (Modal.tsx) are for creation/confirmation.
Import Order (enforce this order)
- React hooks
- Next.js hooks (
useRouter,useSearchParams, etc.) - Contexts (
useAuth,useToast,useConfirmation) - Lib utilities
- Components
Server Type Helpers (from @/types/server)
supportsPlugins(type: ServerType): boolean // true only for 'bukkit'
supportsMods(type: ServerType): boolean // true for 'forge' | 'fabric'
Always gate plugin/mod UI and API logic behind these — never hardcode type checks.
API Route Pattern (enforce this exact order)
import { NextRequest, NextResponse } from 'next/server'
import { validateSession, hasPermission } from '@/lib/auth'
import { getClientIP, createAuditLog } from '@/lib/audit'
import { sanitizeObject } from '@/lib/input-validation'
import { isValidObjectId } from '@/lib/input-validation'
import { connectToDatabase } from '@/lib/mongodb'
export async function POST(request: NextRequest) {
const clientIP = getClientIP(request)
// 1. Auth
const session = await validateSession(request)
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 2. Permission check — ALWAYS audit-log denials
if (!hasPermission(session, 'resource:action')) {
await createAuditLog({ action: '...', ..., status: 'FAILED', statusCode: 403, clientIP })
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// 3. Sanitize input BEFORE any use
const { field1, field2 } = sanitizeObject(await request.json())
// 4. Validate params
if (!isValidObjectId(someId)) return NextResponse.json({ error: '...' }, { status: 400 })
// 5. DB + business logic
await connectToDatabase()
// ... Mongoose queries ...
// 6. Audit log every mutation (success AND failure)
await createAuditLog({
action: 'entity_created',
entityType: 'entity',
entityId: doc._id.toString(),
entityName: doc.name,
userId: session._id,
userName: session.username,
userEmail: session.email,
newValues: { ... },
clientIP,
status: 'SUCCESS',
statusCode: 201,
})
return NextResponse.json({ success: true, data: doc }, { status: 201 })
}
HTTP status codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 500 Internal Server Error
Security — Non-Negotiable Rules
- Server-side permission checks are authoritative; client-side is UI-only
sanitizeObject()on ALL request bodies before any use- Parameterized Mongoose queries only — never string concatenation in queries
- Audit log ALL mutations (CREATE/UPDATE/DELETE) with previousValues + newValues + clientIP
- Never expose
passwordHash,twoFactorCode,twoFactorExpiry,loginAttemptsin API responses — use.select('-passwordHash -twoFactorCode ...')on Mongoose queries - Permission format:
resource:action(e.g.,servers:edit,backups:delete) - Admin wildcard:
*:*
Permissions Reference
servers:view servers:create servers:edit servers:delete
servers:start servers:stop servers:restart servers:console
backups:view backups:create backups:restore backups:delete
plugins:view plugins:install plugins:remove plugins:toggle
mods:view mods:install mods:remove mods:toggle
players:view players:whitelist players:op players:ban
users:view users:create users:edit users:delete
roles:view roles:create roles:edit roles:delete
audit:view
UI Patterns — Dark Glassmorphism
Use these CSS utility classes (defined in globals.css):
.glass— standard card/panel (rgba white 6%, blur 24px).glass-lg— hero/prominent panels (rgba white 8%, blur 40px).glass-surface— subtle surfaces (rgba white 7%, blur 16px).glass-input— form inputs with cyan glow on focus.glass-shine— adds top-edge gradient highlight via::before.glow-cyan,.glow-emerald,.glow-amber,.glow-red— colored box shadows
Background layers
- Page background:
#050510(set onbodyin globals.css — do NOT override with bg-* classes) - Surfaces:
bg-white/[0.06]or.glass - Elevated:
bg-white/[0.08]or.glass-lg
Color semantics (Tailwind)
| Meaning | Color |
|---|---|
| Primary/action | cyan-400 / cyan-500 |
| Success / online | emerald-400 / emerald-500 |
| Warning / pending | amber-400 / amber-500 |
| Error / danger / offline | red-400 / red-500 |
| Neutral | gray-400 through gray-900 |
Text
- Primary:
text-gray-100 - Secondary:
text-gray-400 - Muted/placeholder:
text-gray-500
Buttons
Primary: bg-cyan-500 hover:bg-cyan-600 text-white rounded-lg
Danger: bg-red-500 hover:bg-red-600 text-white rounded-lg
Secondary: bg-white/[0.08] hover:bg-white/[0.12] text-gray-200 rounded-lg border border-white/[0.12]
Badges
Info: bg-cyan-500/20 text-cyan-400
Success: bg-emerald-500/20 text-emerald-400
Warning: bg-amber-500/20 text-amber-400
Error: bg-red-500/20 text-red-400
Animations (all defined in globals.css)
animate-fade-in(200ms)animate-scale-in(250ms) — modalsanimate-slide-in-right(300ms) — drawers, toastsanimate-slide-up(300ms)- Only animate
transformandopacity; keep under 300ms
Tables
Container: .glass rounded-xl overflow-hidden
Header: bg-white/[0.05] text-gray-400 text-xs uppercase tracking-wider
Row: border-b border-white/[0.06] hover:bg-white/[0.04]
Dividers: divide-y divide-white/[0.06]
Docker Architecture
Key docker.ts patterns
// Always use getDockerClient() singleton
import { getDockerClient, getServerPath, createServerContainer, mapContainerState } from '@/lib/docker'
// Container naming
containerName = `mc-${server._id}`
// getItzgServerType maps server type to Docker env
// 'bukkit' → 'PAPER' (not 'BUKKIT')
// 'vanilla' → 'VANILLA'
// 'forge' → 'FORGE'
// 'fabric' → 'FABRIC'
// Volume mount pattern (SELinux-compatible)
// ${serverPath}:/data:Z
Volume layout
${MC_SERVERS_PATH}/{serverId}/
├── server.properties
├── world/ # Backed up
├── plugins/ # bukkit only
├── mods/ # forge/fabric only
├── logs/
└── backups/
Environment variables
MC_SERVERS_PATH=/opt/mc-servers # Base path for server volumes
DOCKER_SOCKET=/var/run/docker.sock # Docker socket
DOCKER_UID=1000 # Container user ID (chown on dir creation)
DOCKER_GID=1000 # Container group ID
Container status mapping
Docker state → app ServerStatus:
running→onlinecreated/restarting→startingpaused/removing→stoppingexited→offlinedead→crashed
Backup process
- Exec RCON
save-off+save-all flush(pause world saving) tar -czf "${filePath}" -C "${serverPath}" world- Exec RCON
save-on(resume) - Create/update
Backupdocument - Delete oldest backups beyond
backupRetentionlimit
next.config.ts note
dockerode and node-cron are in serverExternalPackages — they can only run server-side.
Data Models Quick Reference
Server.status values
online | offline | starting | stopping | crashed
Server.type values
vanilla | bukkit | forge | fabric
Backup.type / Backup.status
- type:
manual|scheduled - status:
completed|in_progress|failed
User.status values
active | inactive | locked
AuditLog.status values
SUCCESS | FAILED (always uppercase)
Contexts — How to Use
// Auth
const { user, loading, login, verify2FA, logout, hasPermission, refreshUser } = useAuth()
// Toast notifications
const { showToast } = useToast()
showToast('Server started', 'success') // 'success' | 'error' | 'info' | 'warning'
// Confirmation dialogs (returns Promise<boolean>)
const { showConfirmation } = useConfirmation()
const ok = await showConfirmation({
title: 'Delete Server?',
message: 'This will permanently delete',
itemName: server.name,
type: 'danger', // 'danger' | 'warning' | 'info'
})
Hooks Quick Reference
// Console streaming + command sending
const { lines, connected, sending, sendCommand, clearConsole } = useConsole(serverId)
// Single server status polling (default 10s interval)
const { status, players, loading, refresh } = useServerStatus(serverId)
// All servers status (default 15s interval)
const { servers, loading, refresh } = useAllServersStatus()
Real-Time Console (SSE)
- Stream: GET
/api/servers/[id]/console→ EventSource / SSE - Send command: POST
/api/servers/[id]/consolewith{ command: string } - Client-side: use
useConsole()hook — never reimplement SSE manually
Authentication Flow Summary
- POST
/api/auth/login→ sends 2FA email → returns{ requiresTwoFactor: true }+ setspending_2facookie (10 min) - POST
/api/auth/verify-2fa→ validates code → setssession-token(1h) +refresh-token(7d) cookies - GET
/api/auth/me→ returnsAuthUserfrom current session - POST
/api/auth/logout→ clears cookies - POST
/api/auth/refresh→ rotates access token
Account lockout: 5 failed login attempts → 30-minute lockout (tracked via loginAttempts + lockUntil)
API Routes Map
/api/auth/ login, logout, me, refresh, verify-2fa
/api/users/ GET list, POST create
/api/users/[id] PATCH update, DELETE
/api/roles/ GET list, POST create
/api/roles/[id] PATCH update, DELETE
/api/servers/ GET list, POST create
/api/servers/[id] GET, PATCH, DELETE
/api/servers/[id]/start POST
/api/servers/[id]/stop POST
/api/servers/[id]/restart POST
/api/servers/[id]/console GET (SSE stream), POST (command)
/api/servers/[id]/stats GET
/api/servers/[id]/configuration GET/PATCH (server.properties)
/api/servers/[id]/backups GET, POST (create manual)
/api/servers/[id]/backups/[backupId] DELETE
/api/servers/[id]/backups/[backupId]/restore POST
/api/servers/[id]/plugins GET, POST (upload JAR)
/api/servers/[id]/plugins/[filename] DELETE
/api/servers/[id]/plugins/[filename]/toggle PATCH
/api/servers/[id]/mods GET, POST (upload JAR)
/api/servers/[id]/mods/[filename] DELETE
/api/servers/[id]/mods/[filename]/toggle PATCH
/api/servers/[id]/players GET (online players)
/api/servers/[id]/players/[name]/[action] POST (op, deop, ban, unban, whitelist)
/api/servers/[id]/files GET (file browser)
/api/servers/[id]/logs GET (log files)
/api/audit GET
/api/health GET
Pages Map (all 'use client')
/ Redirect → /dashboard or /login
/login Login + 2FA flow
/(app)/dashboard Overview: server stats, quick-access server list
/(app)/servers Server list (DataManagementTemplate)
/(app)/servers/[id] Server overview / detail
/(app)/servers/[id]/console
/(app)/servers/[id]/configuration
/(app)/servers/[id]/plugins
/(app)/servers/[id]/mods
/(app)/servers/[id]/backups
/(app)/servers/[id]/logs
/(app)/servers/[id]/files
/(app)/servers/[id]/players
/(app)/users User management (DataManagementTemplate)
/(app)/roles Role management (DataManagementTemplate)
/(app)/audit Audit log viewer (DataManagementTemplate)
Common Mistakes to Avoid
- Do NOT import
src/lib/utilities in client components — they're server-only (use API routes instead) - Do NOT use
bg-gray-950/bg-gray-900for backgrounds — use.glass,.glass-surface, orbg-white/[0.06]per the glassmorphism system - Do NOT use
tailwind.config.jsextend syntax for new colors — use Tailwind 4's CSS variable approach in globals.css if needed - Do NOT skip sanitizing request body before using any field
- Do NOT build custom date formatters — use
@/lib/date-utils - Do NOT build custom data tables — use
DataManagementTemplate - Do NOT use any icon library other than
lucide-react - Do NOT allow plugin routes on non-bukkit servers or mod routes on non-forge/fabric — return 400 with type mismatch error
- Do NOT expose sensitive user fields in API responses (
passwordHash,twoFactorCode, etc.) - Do NOT skip audit logging on mutations — every CREATE/UPDATE/DELETE must log success AND failure