mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-03-28 17:26:47 -07:00
updates
This commit is contained in:
parent
fdb7b19cbc
commit
026a558195
409
.claude/CLAUDE.md
Normal file
409
.claude/CLAUDE.md
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
# 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
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
```ts
|
||||||
|
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`)
|
||||||
|
```ts
|
||||||
|
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)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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
|
||||||
|
```ts
|
||||||
|
// 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
|
||||||
|
```env
|
||||||
|
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` → `online`
|
||||||
|
- `created` / `restarting` → `starting`
|
||||||
|
- `paused` / `removing` → `stopping`
|
||||||
|
- `exited` → `offline`
|
||||||
|
- `dead` → `crashed`
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 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
|
||||||
212
src/app/(app)/servers/[id]/admins/page.tsx
Normal file
212
src/app/(app)/servers/[id]/admins/page.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { ArrowLeft, ShieldCheck, UserMinus, UserPlus } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import PageHeader from '@/components/PageHeader'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Spinner from '@/components/ui/Spinner'
|
||||||
|
import { useToast } from '@/contexts/ToastContext'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
import { useConfirmation } from '@/contexts/ConfirmationContext'
|
||||||
|
|
||||||
|
interface AdminUser {
|
||||||
|
_id: string
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllUser {
|
||||||
|
_id: string
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerAdminsPage() {
|
||||||
|
const params = useParams()
|
||||||
|
const serverId = params.id as string
|
||||||
|
const { showToast } = useToast()
|
||||||
|
const { hasPermission } = useAuth()
|
||||||
|
const { showConfirmation } = useConfirmation()
|
||||||
|
|
||||||
|
const canEdit = hasPermission('servers:edit')
|
||||||
|
|
||||||
|
const [admins, setAdmins] = useState<AdminUser[]>([])
|
||||||
|
const [allUsers, setAllUsers] = useState<AllUser[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState('')
|
||||||
|
const [adding, setAdding] = useState(false)
|
||||||
|
|
||||||
|
const fetchAdmins = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/servers/${serverId}/admins`, { credentials: 'include' })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setAdmins(data.data || [])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('Failed to load admins', 'error')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [serverId, showToast])
|
||||||
|
|
||||||
|
const fetchAllUsers = useCallback(async () => {
|
||||||
|
if (!canEdit) return
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/users', { credentials: 'include' })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setAllUsers(data.data || [])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// non-critical
|
||||||
|
}
|
||||||
|
}, [canEdit])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAdmins()
|
||||||
|
fetchAllUsers()
|
||||||
|
}, [fetchAdmins, fetchAllUsers])
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
if (!selectedUserId) return
|
||||||
|
setAdding(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/servers/${serverId}/admins`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userId: selectedUserId }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) {
|
||||||
|
showToast('Admin added', 'success')
|
||||||
|
setSelectedUserId('')
|
||||||
|
fetchAdmins()
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Failed to add admin', 'error')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('Failed to add admin', 'error')
|
||||||
|
} finally {
|
||||||
|
setAdding(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemove = async (admin: AdminUser) => {
|
||||||
|
const confirmed = await showConfirmation({
|
||||||
|
title: 'Remove Admin',
|
||||||
|
message: 'Remove admin access from',
|
||||||
|
itemName: admin.username,
|
||||||
|
type: 'warning',
|
||||||
|
confirmText: 'Remove',
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/servers/${serverId}/admins/${admin._id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) {
|
||||||
|
showToast('Admin removed', 'success')
|
||||||
|
fetchAdmins()
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Failed to remove admin', 'error')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('Failed to remove admin', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableUsers = allUsers.filter(u => !admins.some(a => a._id === u._id))
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Link href={`/servers/${serverId}`} className="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-gray-200 transition-colors">
|
||||||
|
<ArrowLeft size={16} /> Back to Server
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<PageHeader
|
||||||
|
title="Server Admins"
|
||||||
|
description="Users with full operational access to this server"
|
||||||
|
icon={ShieldCheck}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Add Admin */}
|
||||||
|
{canEdit && (
|
||||||
|
<div className="glass rounded-xl p-5 space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-300 flex items-center gap-2">
|
||||||
|
<UserPlus size={16} className="text-cyan-400" />
|
||||||
|
Add Admin
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<select
|
||||||
|
value={selectedUserId}
|
||||||
|
onChange={(e) => setSelectedUserId(e.target.value)}
|
||||||
|
className="flex-1 bg-white/[0.05] border border-white/[0.1] rounded-lg px-3 py-2 text-sm text-gray-200 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/30"
|
||||||
|
>
|
||||||
|
<option value="">Select a user...</option>
|
||||||
|
{availableUsers.map(u => (
|
||||||
|
<option key={u._id} value={u._id}>
|
||||||
|
{u.username} ({u.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button
|
||||||
|
icon={UserPlus}
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={!selectedUserId}
|
||||||
|
loading={adding}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Admin List */}
|
||||||
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
|
<div className="px-5 py-4 border-b border-white/[0.06]">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-300">Current Admins ({admins.length})</h3>
|
||||||
|
</div>
|
||||||
|
{admins.length === 0 ? (
|
||||||
|
<div className="px-5 py-10 text-center text-sm text-gray-500">No admins assigned.</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-white/[0.05]">
|
||||||
|
{admins.map(admin => (
|
||||||
|
<div key={admin._id} className="flex items-center justify-between px-5 py-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-200">{admin.username}</p>
|
||||||
|
<p className="text-xs text-gray-500">{admin.email}</p>
|
||||||
|
</div>
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
icon={UserMinus}
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => handleRemove(admin)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ import { useToast } from '@/contexts/ToastContext'
|
|||||||
import { useConfirmation } from '@/contexts/ConfirmationContext'
|
import { useConfirmation } from '@/contexts/ConfirmationContext'
|
||||||
import {
|
import {
|
||||||
Server, Play, Square, RotateCcw, Terminal, Settings, HardDrive,
|
Server, Play, Square, RotateCcw, Terminal, Settings, HardDrive,
|
||||||
Puzzle, Package, Users, Trash2, ArrowLeft, FileText, FolderOpen
|
Puzzle, Package, Users, Trash2, ArrowLeft, FileText, FolderOpen, ShieldCheck
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import PageHeader from '@/components/PageHeader'
|
import PageHeader from '@/components/PageHeader'
|
||||||
import ServerStatusBadge from '@/components/ServerStatusBadge'
|
import ServerStatusBadge from '@/components/ServerStatusBadge'
|
||||||
@ -31,7 +31,7 @@ export default function ServerDetailPage() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const serverId = params.id as string
|
const serverId = params.id as string
|
||||||
const { hasPermission } = useAuth()
|
const { hasPermission, user } = useAuth()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { showConfirmation } = useConfirmation()
|
const { showConfirmation } = useConfirmation()
|
||||||
|
|
||||||
@ -172,6 +172,10 @@ export default function ServerDetailPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isServerAdmin = server.admins?.includes(user?._id ?? '')
|
||||||
|
const canDo = (permission: string) =>
|
||||||
|
hasPermission(permission) || (permission !== 'servers:delete' && !!isServerAdmin)
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ label: 'Console', href: `/servers/${serverId}/console`, icon: Terminal, permission: 'servers:console' },
|
{ label: 'Console', href: `/servers/${serverId}/console`, icon: Terminal, permission: 'servers:console' },
|
||||||
{ label: 'Logs', href: `/servers/${serverId}/logs`, icon: FileText, permission: 'servers:console' },
|
{ label: 'Logs', href: `/servers/${serverId}/logs`, icon: FileText, permission: 'servers:console' },
|
||||||
@ -181,6 +185,7 @@ export default function ServerDetailPage() {
|
|||||||
...(supportsPlugins(server.type) ? [{ label: 'Plugins', href: `/servers/${serverId}/plugins`, icon: Puzzle, permission: 'plugins:view' }] : []),
|
...(supportsPlugins(server.type) ? [{ label: 'Plugins', href: `/servers/${serverId}/plugins`, icon: Puzzle, permission: 'plugins:view' }] : []),
|
||||||
...(supportsMods(server.type) ? [{ label: 'Mods', href: `/servers/${serverId}/mods`, icon: Package, permission: 'mods:view' }] : []),
|
...(supportsMods(server.type) ? [{ label: 'Mods', href: `/servers/${serverId}/mods`, icon: Package, permission: 'mods:view' }] : []),
|
||||||
{ label: 'Players', href: `/servers/${serverId}/players`, icon: Users, permission: 'players:view' },
|
{ label: 'Players', href: `/servers/${serverId}/players`, icon: Users, permission: 'players:view' },
|
||||||
|
...(hasPermission('servers:edit') ? [{ label: 'Admins', href: `/servers/${serverId}/admins`, icon: ShieldCheck, permission: 'servers:edit' }] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -199,17 +204,17 @@ export default function ServerDetailPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<ServerStatusBadge status={server.status} size="md" />
|
<ServerStatusBadge status={server.status} size="md" />
|
||||||
|
|
||||||
{hasPermission('servers:start') && server.status === 'offline' && (
|
{canDo('servers:start') && server.status === 'offline' && (
|
||||||
<Button icon={Play} size="sm" onClick={() => handleAction('start')} loading={actionLoading === 'start'}>
|
<Button icon={Play} size="sm" onClick={() => handleAction('start')} loading={actionLoading === 'start'}>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasPermission('servers:stop') && server.status === 'online' && (
|
{canDo('servers:stop') && server.status === 'online' && (
|
||||||
<Button icon={Square} size="sm" variant="secondary" onClick={() => handleAction('stop')} loading={actionLoading === 'stop'}>
|
<Button icon={Square} size="sm" variant="secondary" onClick={() => handleAction('stop')} loading={actionLoading === 'stop'}>
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasPermission('servers:restart') && server.status === 'online' && (
|
{canDo('servers:restart') && server.status === 'online' && (
|
||||||
<Button icon={RotateCcw} size="sm" variant="secondary" onClick={() => handleAction('restart')} loading={actionLoading === 'restart'}>
|
<Button icon={RotateCcw} size="sm" variant="secondary" onClick={() => handleAction('restart')} loading={actionLoading === 'restart'}>
|
||||||
Restart
|
Restart
|
||||||
</Button>
|
</Button>
|
||||||
@ -259,7 +264,7 @@ export default function ServerDetailPage() {
|
|||||||
{/* Navigation Cards */}
|
{/* Navigation Cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{navLinks
|
{navLinks
|
||||||
.filter(link => hasPermission(link.permission))
|
.filter(link => canDo(link.permission))
|
||||||
.map(link => {
|
.map(link => {
|
||||||
const Icon = link.icon
|
const Icon = link.icon
|
||||||
return (
|
return (
|
||||||
|
|||||||
68
src/app/api/servers/[id]/admins/[userId]/route.ts
Normal file
68
src/app/api/servers/[id]/admins/[userId]/route.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { validateSession, hasPermission } from '@/lib/auth'
|
||||||
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
|
import { Server, User } from '@/lib/models'
|
||||||
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
|
import { createAuditLog, getClientIP } from '@/lib/audit'
|
||||||
|
|
||||||
|
// DELETE /api/servers/[id]/admins/[userId] — Remove a server admin
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string; userId: string }> }
|
||||||
|
) {
|
||||||
|
const clientIP = getClientIP(request)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await validateSession(request)
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(session, 'servers:edit')) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, userId } = await params
|
||||||
|
if (!isValidObjectId(id) || !isValidObjectId(userId)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase()
|
||||||
|
const server = await Server.findById(id)
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = server.admins.some((a) => a.toString() === userId)
|
||||||
|
if (!isAdmin) {
|
||||||
|
return NextResponse.json({ error: 'User is not an admin of this server' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.admins.length <= 1) {
|
||||||
|
return NextResponse.json({ error: 'Cannot remove the last admin from a server' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
server.admins = server.admins.filter((a) => a.toString() !== userId)
|
||||||
|
await server.save()
|
||||||
|
|
||||||
|
const removedUser = await User.findById(userId, { username: 1 }).lean()
|
||||||
|
|
||||||
|
await createAuditLog({
|
||||||
|
action: 'server_admin_removed',
|
||||||
|
entityType: 'server',
|
||||||
|
entityName: server.name,
|
||||||
|
userId: session._id,
|
||||||
|
userName: session.username,
|
||||||
|
userEmail: session.email,
|
||||||
|
previousValues: { serverId: server._id.toString(), removedUserId: userId, removedUserName: removedUser?.username },
|
||||||
|
clientIP,
|
||||||
|
status: 'success',
|
||||||
|
statusCode: 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Remove admin error:', error)
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/app/api/servers/[id]/admins/route.ts
Normal file
115
src/app/api/servers/[id]/admins/route.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { validateSession, hasPermission, hasServerPermission } from '@/lib/auth'
|
||||||
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
|
import { Server, User } from '@/lib/models'
|
||||||
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
|
import { createAuditLog, getClientIP } from '@/lib/audit'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
// GET /api/servers/[id]/admins — List server admins
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await validateSession(request)
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
if (!isValidObjectId(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase()
|
||||||
|
const server = await Server.findById(id)
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate admin users
|
||||||
|
const adminUsers = await User.find(
|
||||||
|
{ _id: { $in: server.admins } },
|
||||||
|
{ _id: 1, username: 1, email: 1 }
|
||||||
|
).lean()
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: adminUsers })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch admins error:', error)
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/servers/[id]/admins — Add a user as server admin
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const clientIP = getClientIP(request)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await validateSession(request)
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(session, 'servers:edit')) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
if (!isValidObjectId(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const { userId } = body
|
||||||
|
|
||||||
|
if (!userId || !isValidObjectId(userId)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid user ID' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase()
|
||||||
|
const server = await Server.findById(id)
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = await User.findById(userId, { _id: 1, username: 1, email: 1 }).lean()
|
||||||
|
if (!targetUser) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyAdmin = server.admins.some((a) => a.toString() === userId)
|
||||||
|
if (alreadyAdmin) {
|
||||||
|
return NextResponse.json({ error: 'User is already an admin of this server' }, { status: 409 })
|
||||||
|
}
|
||||||
|
|
||||||
|
server.admins.push(new mongoose.Types.ObjectId(userId))
|
||||||
|
await server.save()
|
||||||
|
|
||||||
|
await createAuditLog({
|
||||||
|
action: 'server_admin_added',
|
||||||
|
entityType: 'server',
|
||||||
|
entityName: server.name,
|
||||||
|
userId: session._id,
|
||||||
|
userName: session.username,
|
||||||
|
userEmail: session.email,
|
||||||
|
newValues: { serverId: server._id.toString(), addedUserId: userId, addedUserName: targetUser.username },
|
||||||
|
clientIP,
|
||||||
|
status: 'success',
|
||||||
|
statusCode: 201,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: targetUser }, { status: 201 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Add admin error:', error)
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Backup, Server } from '@/lib/models'
|
import { Backup, Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,10 +21,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'backups:restore')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, backupId } = await params
|
const { id, backupId } = await params
|
||||||
if (!isValidObjectId(id) || !isValidObjectId(backupId)) {
|
if (!isValidObjectId(id) || !isValidObjectId(backupId)) {
|
||||||
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 })
|
||||||
@ -36,6 +32,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'backups:restore', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const backup = await Backup.findOne({ _id: backupId, serverId: id })
|
const backup = await Backup.findOne({ _id: backupId, serverId: id })
|
||||||
if (!backup || backup.status !== 'completed') {
|
if (!backup || backup.status !== 'completed') {
|
||||||
return NextResponse.json({ error: 'Backup not found or incomplete' }, { status: 404 })
|
return NextResponse.json({ error: 'Backup not found or incomplete' }, { status: 404 })
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Backup } from '@/lib/models'
|
import { Backup, Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
import { createAuditLog, getClientIP } from '@/lib/audit'
|
import { createAuditLog, getClientIP } from '@/lib/audit'
|
||||||
import { unlink } from 'fs/promises'
|
import { unlink } from 'fs/promises'
|
||||||
@ -19,16 +19,22 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'backups:delete')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, backupId } = await params
|
const { id, backupId } = await params
|
||||||
if (!isValidObjectId(id) || !isValidObjectId(backupId)) {
|
if (!isValidObjectId(id) || !isValidObjectId(backupId)) {
|
||||||
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase()
|
await connectToDatabase()
|
||||||
|
const server = await Server.findById(id).lean()
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminIds = (server.admins ?? []).map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'backups:delete', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const backup = await Backup.findOne({ _id: backupId, serverId: id })
|
const backup = await Backup.findOne({ _id: backupId, serverId: id })
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return NextResponse.json({ error: 'Backup not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Backup not found' }, { status: 404 })
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server, Backup } from '@/lib/models'
|
import { Server, Backup } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -20,16 +20,22 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'backups:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase()
|
await connectToDatabase()
|
||||||
|
const server = await Server.findById(id).lean()
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminIds = (server.admins ?? []).map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'backups:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const backups = await Backup.find({ serverId: id }).sort({ createdAt: -1 }).lean()
|
const backups = await Backup.find({ serverId: id }).sort({ createdAt: -1 }).lean()
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: backups })
|
return NextResponse.json({ success: true, data: backups })
|
||||||
@ -52,10 +58,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'backups:create')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -67,6 +69,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'backups:create', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const serverDir = getServerPath(server._id.toString())
|
const serverDir = getServerPath(server._id.toString())
|
||||||
const backupsDir = path.join(serverDir, 'backups')
|
const backupsDir = path.join(serverDir, 'backups')
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,10 +19,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const serverDir = getServerPath(server._id.toString())
|
const serverDir = getServerPath(server._id.toString())
|
||||||
const propsPath = path.join(serverDir, 'server.properties')
|
const propsPath = path.join(serverDir, 'server.properties')
|
||||||
|
|
||||||
@ -80,10 +81,6 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -98,6 +95,11 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const serverDir = getServerPath(server._id.toString())
|
const serverDir = getServerPath(server._id.toString())
|
||||||
const propsPath = path.join(serverDir, 'server.properties')
|
const propsPath = path.join(serverDir, 'server.properties')
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -16,10 +16,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:console')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -31,6 +27,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:console', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const container = await getContainerByName(`mc-${server._id}`)
|
const container = await getContainerByName(`mc-${server._id}`)
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return NextResponse.json({ error: 'Container not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Container not found' }, { status: 404 })
|
||||||
@ -93,10 +94,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:console')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -113,6 +110,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:console', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.status !== 'online') {
|
if (server.status !== 'online') {
|
||||||
return NextResponse.json({ error: 'Server is not running' }, { status: 400 })
|
return NextResponse.json({ error: 'Server is not running' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -48,10 +48,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -63,6 +59,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const requestedPath = request.nextUrl.searchParams.get('path') || '/'
|
const requestedPath = request.nextUrl.searchParams.get('path') || '/'
|
||||||
const readContent = request.nextUrl.searchParams.get('read') === 'true'
|
const readContent = request.nextUrl.searchParams.get('read') === 'true'
|
||||||
|
|
||||||
@ -176,10 +177,6 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -191,6 +188,11 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { path: filePath, content } = body
|
const { path: filePath, content } = body
|
||||||
|
|
||||||
@ -228,10 +230,6 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -243,6 +241,11 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const { path: filePath } = await request.json()
|
const { path: filePath } = await request.json()
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return NextResponse.json({ error: 'Path is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Path is required' }, { status: 400 })
|
||||||
@ -283,10 +286,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -298,6 +297,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const serverRoot = resolve(getServerPath(server._id.toString()))
|
const serverRoot = resolve(getServerPath(server._id.toString()))
|
||||||
const contentType = request.headers.get('content-type') || ''
|
const contentType = request.headers.get('content-type') || ''
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,10 +18,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:console')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -33,6 +29,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:console', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const logsDir = join(getServerPath(server._id.toString()), 'logs')
|
const logsDir = join(getServerPath(server._id.toString()), 'logs')
|
||||||
const fileName = request.nextUrl.searchParams.get('file')
|
const fileName = request.nextUrl.searchParams.get('file')
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,10 +21,6 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'mods:remove')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, filename } = await params
|
const { id, filename } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -38,6 +34,11 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Server not found or not a Forge/Fabric server' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found or not a Forge/Fabric server' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'mods:remove', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const filePath = path.join(getServerPath(server._id.toString()), 'mods', decodedFilename)
|
const filePath = path.join(getServerPath(server._id.toString()), 'mods', decodedFilename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,10 +21,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'mods:toggle')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, filename } = await params
|
const { id, filename } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -38,6 +34,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found or not a Forge/Fabric server' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found or not a Forge/Fabric server' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'mods:toggle', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const modsDir = path.join(getServerPath(server._id.toString()), 'mods')
|
const modsDir = path.join(getServerPath(server._id.toString()), 'mods')
|
||||||
const currentPath = path.join(modsDir, decodedFilename)
|
const currentPath = path.join(modsDir, decodedFilename)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,10 +19,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'mods:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'mods:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.type !== 'forge' && server.type !== 'fabric') {
|
if (server.type !== 'forge' && server.type !== 'fabric') {
|
||||||
return NextResponse.json({ error: 'Mods are only available for Forge/Fabric servers' }, { status: 400 })
|
return NextResponse.json({ error: 'Mods are only available for Forge/Fabric servers' }, { status: 400 })
|
||||||
}
|
}
|
||||||
@ -81,10 +82,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'mods:install')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -96,6 +93,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'mods:install', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.type !== 'forge' && server.type !== 'fabric') {
|
if (server.type !== 'forge' && server.type !== 'fabric') {
|
||||||
return NextResponse.json({ error: 'Mods are only available for Forge/Fabric servers' }, { status: 400 })
|
return NextResponse.json({ error: 'Mods are only available for Forge/Fabric servers' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -38,10 +38,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, requiredPermission)) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
}
|
}
|
||||||
@ -52,6 +48,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, requiredPermission, adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
// Map action to MC command
|
// Map action to MC command
|
||||||
const commandMap: Record<string, string> = {
|
const commandMap: Record<string, string> = {
|
||||||
whitelist: `whitelist add ${decodedName}`,
|
whitelist: `whitelist add ${decodedName}`,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -27,10 +27,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'players:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -42,6 +38,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'players:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const serverDir = getServerPath(server._id.toString())
|
const serverDir = getServerPath(server._id.toString())
|
||||||
|
|
||||||
// Parse player lists from JSON files
|
// Parse player lists from JSON files
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,10 +21,6 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'plugins:remove')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, filename } = await params
|
const { id, filename } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -38,6 +34,11 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Server not found or not a Bukkit server' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found or not a Bukkit server' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'plugins:remove', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const filePath = path.join(getServerPath(server._id.toString()), 'plugins', decodedFilename)
|
const filePath = path.join(getServerPath(server._id.toString()), 'plugins', decodedFilename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,10 +21,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'plugins:toggle')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, filename } = await params
|
const { id, filename } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -38,6 +34,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found or not a Bukkit server' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found or not a Bukkit server' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'plugins:toggle', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const pluginsDir = path.join(getServerPath(server._id.toString()), 'plugins')
|
const pluginsDir = path.join(getServerPath(server._id.toString()), 'plugins')
|
||||||
const currentPath = path.join(pluginsDir, decodedFilename)
|
const currentPath = path.join(pluginsDir, decodedFilename)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,10 +19,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'plugins:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'plugins:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.type !== 'bukkit') {
|
if (server.type !== 'bukkit') {
|
||||||
return NextResponse.json({ error: 'Plugins are only available for Bukkit-type servers' }, { status: 400 })
|
return NextResponse.json({ error: 'Plugins are only available for Bukkit-type servers' }, { status: 400 })
|
||||||
}
|
}
|
||||||
@ -81,10 +82,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'plugins:install')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -96,6 +93,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'plugins:install', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.type !== 'bukkit') {
|
if (server.type !== 'bukkit') {
|
||||||
return NextResponse.json({ error: 'Plugins are only available for Bukkit-type servers' }, { status: 400 })
|
return NextResponse.json({ error: 'Plugins are only available for Bukkit-type servers' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,10 +18,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:restart')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:restart', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
// If the server is currently offline, check for port conflicts before restarting
|
// If the server is currently offline, check for port conflicts before restarting
|
||||||
if (server.status !== 'online' && server.status !== 'starting') {
|
if (server.status !== 'online' && server.status !== 'starting') {
|
||||||
const portConflict = await Server.findOne({
|
const portConflict = await Server.findOne({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasPermission, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -17,10 +17,6 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -33,6 +29,11 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = (server.admins ?? []).map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:view', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
// Reconcile status with Docker
|
// Reconcile status with Docker
|
||||||
try {
|
try {
|
||||||
const container = await getContainerByName(`mc-${server._id}`)
|
const container = await getContainerByName(`mc-${server._id}`)
|
||||||
@ -69,10 +70,6 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:edit')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -88,6 +85,11 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:edit', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const previousValues = server.toObject()
|
const previousValues = server.toObject()
|
||||||
|
|
||||||
// Allowlisted update fields
|
// Allowlisted update fields
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,10 +18,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:start')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:start', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.status === 'online' || server.status === 'starting') {
|
if (server.status === 'online' || server.status === 'starting') {
|
||||||
return NextResponse.json({ error: 'Server is already running or starting' }, { status: 400 })
|
return NextResponse.json({ error: 'Server is already running or starting' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -46,10 +46,6 @@ export async function GET(
|
|||||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
|
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:view')) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return new Response(JSON.stringify({ error: 'Invalid server ID' }), { status: 400 })
|
return new Response(JSON.stringify({ error: 'Invalid server ID' }), { status: 400 })
|
||||||
@ -61,6 +57,11 @@ export async function GET(
|
|||||||
return new Response(JSON.stringify({ error: 'Server not found' }), { status: 404 })
|
return new Response(JSON.stringify({ error: 'Server not found' }), { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = (server.admins ?? []).map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:view', adminIds)) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const container = await getContainerByName(`mc-${server._id}`).catch(() => null)
|
const container = await getContainerByName(`mc-${server._id}`).catch(() => null)
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return new Response(JSON.stringify({ error: 'Container not found' }), { status: 404 })
|
return new Response(JSON.stringify({ error: 'Container not found' }), { status: 404 })
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasServerPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,10 +18,6 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:stop')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
if (!isValidObjectId(id)) {
|
if (!isValidObjectId(id)) {
|
||||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 })
|
||||||
@ -34,6 +30,11 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminIds = server.admins.map((a) => a.toString())
|
||||||
|
if (!hasServerPermission(session, 'servers:stop', adminIds)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
if (server.status === 'offline' || server.status === 'stopping') {
|
if (server.status === 'offline' || server.status === 'stopping') {
|
||||||
return NextResponse.json({ error: 'Server is already stopped or stopping' }, { status: 400 })
|
return NextResponse.json({ error: 'Server is already stopped or stopping' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
import { validateSession, hasPermission } from '@/lib/auth'
|
import { validateSession, hasPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Server } from '@/lib/models'
|
import { Server } from '@/lib/models'
|
||||||
@ -14,12 +15,17 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermission(session, 'servers:view')) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
await connectToDatabase()
|
await connectToDatabase()
|
||||||
const servers = await Server.find().sort({ createdAt: -1 }).lean()
|
|
||||||
|
let servers
|
||||||
|
if (hasPermission(session, 'servers:view')) {
|
||||||
|
servers = await Server.find().sort({ createdAt: -1 }).lean()
|
||||||
|
} else {
|
||||||
|
// Return only servers where this user is a server admin
|
||||||
|
servers = await Server.find({
|
||||||
|
admins: new mongoose.Types.ObjectId(session._id),
|
||||||
|
}).sort({ createdAt: -1 }).lean()
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: servers })
|
return NextResponse.json({ success: true, data: servers })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -95,6 +101,7 @@ export async function POST(request: NextRequest) {
|
|||||||
autoStart: false,
|
autoStart: false,
|
||||||
autoRestart: true,
|
autoRestart: true,
|
||||||
createdBy: session._id,
|
createdBy: session._id,
|
||||||
|
admins: [new mongoose.Types.ObjectId(session._id)],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set containerName before save — schema requires it
|
// Set containerName before save — schema requires it
|
||||||
|
|||||||
@ -149,3 +149,18 @@ export function hasPermission(user: AuthUser, permission: string): boolean {
|
|||||||
if (user.permissions.includes('*:*')) return true
|
if (user.permissions.includes('*:*')) return true
|
||||||
return user.permissions.includes(permission)
|
return user.permissions.includes(permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks permission for a specific server, granting access to server admins.
|
||||||
|
* Server admins have full access EXCEPT servers:delete (global-only).
|
||||||
|
*/
|
||||||
|
export function hasServerPermission(
|
||||||
|
user: AuthUser,
|
||||||
|
permission: string,
|
||||||
|
serverAdmins: string[]
|
||||||
|
): boolean {
|
||||||
|
if (user.permissions.includes('*:*')) return true
|
||||||
|
if (user.permissions.includes(permission)) return true
|
||||||
|
if (permission !== 'servers:delete' && serverAdmins.includes(user._id)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -83,6 +83,7 @@ export interface IServer extends Document {
|
|||||||
autoRestart: boolean
|
autoRestart: boolean
|
||||||
backupSchedule: string | null
|
backupSchedule: string | null
|
||||||
backupRetention: number
|
backupRetention: number
|
||||||
|
admins: mongoose.Types.ObjectId[]
|
||||||
createdBy: mongoose.Types.ObjectId
|
createdBy: mongoose.Types.ObjectId
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@ -113,6 +114,7 @@ const serverSchema = new Schema<IServer>(
|
|||||||
autoRestart: { type: Boolean, default: true },
|
autoRestart: { type: Boolean, default: true },
|
||||||
backupSchedule: { type: String, default: null },
|
backupSchedule: { type: String, default: null },
|
||||||
backupRetention: { type: Number, default: 5 },
|
backupRetention: { type: Number, default: 5 },
|
||||||
|
admins: [{ type: Schema.Types.ObjectId, ref: 'User', default: [] }],
|
||||||
createdBy: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
createdBy: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface Server {
|
|||||||
autoRestart: boolean
|
autoRestart: boolean
|
||||||
backupSchedule: string | null
|
backupSchedule: string | null
|
||||||
backupRetention: number
|
backupRetention: number
|
||||||
|
admins: string[]
|
||||||
createdBy: string
|
createdBy: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user