mc-manager/.claude/CLAUDE.md
2026-02-13 15:16:54 -08:00

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 uses useSearchParams()
  • Never use raw <img> — always next/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)

  1. React hooks
  2. Next.js hooks (useRouter, useSearchParams, etc.)
  3. Contexts (useAuth, useToast, useConfirmation)
  4. Lib utilities
  5. 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, loginAttempts in 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 on body in 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) — modals
  • animate-slide-in-right (300ms) — drawers, toasts
  • animate-slide-up (300ms)
  • Only animate transform and opacity; 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:

  • runningonline
  • created / restartingstarting
  • paused / removingstopping
  • exitedoffline
  • deadcrashed

Backup process

  1. Exec RCON save-off + save-all flush (pause world saving)
  2. tar -czf "${filePath}" -C "${serverPath}" world
  3. Exec RCON save-on (resume)
  4. Create/update Backup document
  5. Delete oldest backups beyond backupRetention limit

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]/console with { command: string }
  • Client-side: use useConsole() hook — never reimplement SSE manually

Authentication Flow Summary

  1. POST /api/auth/login → sends 2FA email → returns { requiresTwoFactor: true } + sets pending_2fa cookie (10 min)
  2. POST /api/auth/verify-2fa → validates code → sets session-token (1h) + refresh-token (7d) cookies
  3. GET /api/auth/me → returns AuthUser from current session
  4. POST /api/auth/logout → clears cookies
  5. 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-900 for backgrounds — use .glass, .glass-surface, or bg-white/[0.06] per the glassmorphism system
  • Do NOT use tailwind.config.js extend 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