mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-03-28 17:26:47 -07:00
Compare commits
No commits in common. "4b57f8fe3baf15e462ad194da8d5ffbfb6ea304a" and "fdb7b19cbc7ea496a3f0f5052b96f5ec0d246ff7" have entirely different histories.
4b57f8fe3b
...
fdb7b19cbc
213
.github/copilot-instructions.md
vendored
Normal file
213
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# MC-Manager — Copilot Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
Minecraft Server Manager — a full-stack Next.js 15+ (App Router) web application for managing Minecraft server instances. Built following Rezzect's organization-wide Next.js standards.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Framework:** Next.js 15+ (App Router, Turbopack), React 19+, TypeScript 5.9+ (strict mode)
|
||||||
|
- **Styling:** Tailwind CSS 3.4+ with dark glassmorphism design system (cyan-500 primary, `bg-gray-900/80 backdrop-blur-lg`)
|
||||||
|
- **Database:** MongoDB 6+ via Mongoose 8+ with serverless-safe cached connections (`src/lib/mongodb.ts`)
|
||||||
|
- **Auth:** Dual-token JWT (1h access + 7d refresh) in HTTP-only cookies, mandatory 2FA via email, bcryptjs (12 rounds)
|
||||||
|
- **Containers:** Docker via dockerode — each MC server runs as its own container
|
||||||
|
- **Icons:** Lucide React only — no other icon libraries
|
||||||
|
- **Email:** Microsoft Graph API primary, SMTP (nodemailer) fallback — always implement dual-provider
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
src/app/ — Pages ('use client' on every page) and API routes
|
||||||
|
src/components/ — Shared components; templates/ for reusable CRUD patterns (DataManagementTemplate)
|
||||||
|
src/contexts/ — AuthContext, ToastContext, ConfirmationContext
|
||||||
|
src/lib/ — Server utilities: auth.ts, mongodb.ts, models.ts, docker.ts, date-utils.ts, input-validation.ts, audit.ts
|
||||||
|
src/hooks/ — Custom React hooks
|
||||||
|
src/types/ — TypeScript interfaces and types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critical Conventions
|
||||||
|
- **Every page** must start with `'use client'`; add `export const dynamic = 'force-dynamic'` if using `useSearchParams`
|
||||||
|
- **Date formatting:** NEVER create custom formatters — always use `formatDate()`, `formatDateTime()`, `formatDateForInput()` from `@/lib/date-utils`
|
||||||
|
- **Images:** Always `next/image` — never raw `<img>` tags
|
||||||
|
- **CRUD pages:** Use `DataManagementTemplate` from `@/components/templates/` for consistent data management UIs
|
||||||
|
- **Drawers** over modals for detail views (`max-w-3xl`, `animate-slide-in-right`)
|
||||||
|
|
||||||
|
## API Route Pattern (every route must follow this order)
|
||||||
|
1. `validateSession(request)` → 401 if missing
|
||||||
|
2. `getClientIP(request)` for audit trail
|
||||||
|
3. `sanitizeObject(await request.json())` — sanitize ALL input
|
||||||
|
4. `isValidObjectId()` / `isValidEmail()` — validate params
|
||||||
|
5. Permission check → 403 + audit log if denied
|
||||||
|
6. `connectToDatabase()` then business logic
|
||||||
|
7. `createAuditLog()` for every CREATE/UPDATE/DELETE (success AND failure)
|
||||||
|
8. Return proper HTTP status: 200/201/400/401/403/404/409/500
|
||||||
|
|
||||||
|
## Security (non-negotiable)
|
||||||
|
- Server-side permission checks are **authoritative**; client-side checks are UI hints only
|
||||||
|
- Permission format: `resource:action` (e.g., `servers:edit`, `servers:view:department`)
|
||||||
|
- Sanitize all inputs via `sanitizeObject()` from `@/lib/input-validation.ts`
|
||||||
|
- Parameterized DB queries only — never string concatenation
|
||||||
|
- Audit log ALL mutations with previous/new values and client IP
|
||||||
|
|
||||||
|
## UI Patterns (Dark Theme)
|
||||||
|
- **Base background:** `bg-gray-950` (page), `bg-gray-900` (surfaces), `bg-gray-800` (elevated elements)
|
||||||
|
- **Color semantics:** cyan=primary, emerald=success/active, amber=warning/pending, red=error/danger, gray=neutral
|
||||||
|
- **Text colors:** `text-gray-100` (primary), `text-gray-400` (secondary), `text-gray-500` (muted)
|
||||||
|
- **Animations:** Only animate `transform` and `opacity`, keep under 300ms
|
||||||
|
- **Buttons:** `bg-cyan-500 hover:bg-cyan-600 text-white rounded-lg` (primary), `bg-red-500 hover:bg-red-600` (danger), `bg-gray-700 hover:bg-gray-600 text-gray-200` (secondary)
|
||||||
|
- **Inputs:** `bg-gray-800 border-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500`
|
||||||
|
- **Cards:** `bg-gray-900/80 backdrop-blur-lg rounded-lg shadow-lg border border-gray-700/50 p-6`
|
||||||
|
- **Glassmorphism card:** `bg-gray-800/60 backdrop-blur-lg rounded-lg border border-gray-700/50 shadow-xl`
|
||||||
|
- **Tables:** `bg-gray-900` body, `bg-gray-800/50` header, `divide-gray-700`, `hover:bg-gray-800` rows
|
||||||
|
- **Badges:** `bg-cyan-500/20 text-cyan-400` (info), `bg-emerald-500/20 text-emerald-400` (success), `bg-amber-500/20 text-amber-400` (warning), `bg-red-500/20 text-red-400` (error)
|
||||||
|
- **Borders:** `border-gray-700` (standard), `border-gray-700/50` (subtle)
|
||||||
|
- **Import order:** React hooks → Next.js hooks → contexts → lib utilities → components
|
||||||
|
|
||||||
|
## Minecraft Domain & Server Types
|
||||||
|
|
||||||
|
Each server instance has a **type** that determines its capabilities:
|
||||||
|
|
||||||
|
| Type | Examples | Supports Plugins | Supports Mods |
|
||||||
|
|------|----------|:-:|:-:|
|
||||||
|
| `vanilla` | Official Mojang JAR | ✗ | ✗ |
|
||||||
|
| `bukkit` | Spigot, PaperMC, Purpur, etc. | ✓ | ✗ |
|
||||||
|
| `forge` | Forge | ✗ | ✓ |
|
||||||
|
| `fabric` | Fabric | ✗ | ✓ |
|
||||||
|
|
||||||
|
**Feature matrix by type — gate UI and API logic on `server.type`:**
|
||||||
|
- **All types:** start/stop/restart, console streaming, command execution, server.properties editing, JVM args, backup/restore, player management (whitelist, ops, bans)
|
||||||
|
- **Bukkit-based only:** plugin install/remove/enable/disable (JAR-based in `plugins/` directory)
|
||||||
|
- **Forge/Fabric only:** mod install/remove/enable/disable (JAR-based in `mods/` directory)
|
||||||
|
- **Vanilla:** no extension management — only core server features
|
||||||
|
|
||||||
|
### Core Feature Areas
|
||||||
|
1. **Server Lifecycle** — create, start, stop, restart, delete instances; real-time status (online/offline/starting/stopping)
|
||||||
|
2. **Console** — live log streaming (tail server stdout), send commands to server stdin
|
||||||
|
3. **Configuration** — edit server.properties, JVM memory/flags, world settings per instance
|
||||||
|
4. **Backups** — manual + scheduled backups of world data; restore to point-in-time
|
||||||
|
5. **Plugins** (bukkit only) — upload/install/remove JARs; enable/disable without removing
|
||||||
|
6. **Mods** (forge/fabric only) — upload/install/remove JARs; enable/disable without removing
|
||||||
|
7. **Player Management** — whitelist add/remove, op/deop, ban/unban, view online players
|
||||||
|
|
||||||
|
## Data Models (Mongoose schemas in `src/lib/models.ts`)
|
||||||
|
|
||||||
|
### User
|
||||||
|
- `username`, `email`, `passwordHash` (bcrypt 12 rounds)
|
||||||
|
- `roles: [ObjectId]` → references Role
|
||||||
|
- `twoFactorCode`, `twoFactorExpiry` — for mandatory 2FA
|
||||||
|
- `loginAttempts`, `lockUntil` — account lockout (5 attempts → 30 min)
|
||||||
|
- `status`: active | inactive | locked
|
||||||
|
- `lastLogin`, `createdAt`, `updatedAt`
|
||||||
|
|
||||||
|
### Role
|
||||||
|
- `name` (e.g., Admin, Operator, Viewer)
|
||||||
|
- `permissions: [{ resource: string, actions: string[] }]`
|
||||||
|
- `description`, `isDefault`, `createdAt`
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- `name`, `type`: vanilla | bukkit | forge | fabric
|
||||||
|
- `version` (MC version, e.g., "1.21.4")
|
||||||
|
- `dockerImage` (default `itzg/minecraft-server`, or a custom image)
|
||||||
|
- `containerId` (Docker container ID — set after creation)
|
||||||
|
- `containerName` (e.g., `mc-{serverId}`)
|
||||||
|
- `port` (host port mapped to container 25565)
|
||||||
|
- `rconPort` (host port mapped to container 25575, optional)
|
||||||
|
- `status`: online | offline | starting | stopping | crashed
|
||||||
|
- `maxPlayers`, `memory` (min/max heap in MB)
|
||||||
|
- `jvmArgs: [string]`
|
||||||
|
- `autoStart: boolean`, `autoRestart: boolean`
|
||||||
|
- `backupSchedule`: cron expression (e.g., `0 */6 * * *` = every 6h) or `null` if manual-only
|
||||||
|
- `backupRetention`: max number of backups to keep per server (oldest auto-deleted)
|
||||||
|
- `createdBy: ObjectId` → User who created it
|
||||||
|
- `createdAt`, `updatedAt`
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- `serverId: ObjectId` → Server
|
||||||
|
- `filename`, `filePath`, `fileSize`
|
||||||
|
- `type`: manual | scheduled
|
||||||
|
- `status`: completed | in_progress | failed
|
||||||
|
- `createdBy: ObjectId`, `createdAt`
|
||||||
|
|
||||||
|
### AuditLog
|
||||||
|
- Standard org pattern: `action`, `entityType`, `entityId`, `entityName`, `userId`, `userName`, `userEmail`, `previousValues`, `newValues`, `changes`, `clientIP`, `status`, `statusCode`
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
Format: `resource:action`. Relevant resources for this project:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin role gets `*:*` (wildcard). Always check `server.type` before allowing plugin/mod actions — return 400 if mismatched (e.g., plugin install on a Forge server).
|
||||||
|
|
||||||
|
## Docker Architecture
|
||||||
|
|
||||||
|
Every Minecraft server runs as an isolated Docker container. Use **dockerode** (`src/lib/docker.ts`) to interact with the Docker Engine API via Unix socket.
|
||||||
|
|
||||||
|
### Docker Images
|
||||||
|
- **Default:** `itzg/minecraft-server` — supports all server types via `TYPE` env var (e.g., `VANILLA`, `PAPER`, `SPIGOT`, `FORGE`, `FABRIC`)
|
||||||
|
- **Custom:** Users can specify any Docker image in `server.dockerImage`; when custom, the app still bind-mounts the same volume layout but skips `itzg`-specific env vars — the user is responsible for image compatibility
|
||||||
|
- Store `dockerImage` on the Server model; default to `itzg/minecraft-server` if not provided
|
||||||
|
|
||||||
|
### Container Lifecycle
|
||||||
|
- **Create:** `docker.createContainer()` with `server.dockerImage`, bind-mount a host volume for persistent data
|
||||||
|
- **Start/Stop/Restart:** `container.start()`, `container.stop()`, `container.restart()` — update `server.status` in MongoDB to match
|
||||||
|
- **Delete:** `container.remove({ force: true })`, then optionally clean up host volume
|
||||||
|
- **Status sync:** On app startup and periodically, reconcile `server.status` with `container.inspect()` state
|
||||||
|
|
||||||
|
### Volume Layout
|
||||||
|
Each server gets a host-mounted volume at a configurable base path (env `MC_SERVERS_PATH`, default `/opt/mc-servers/`):
|
||||||
|
```
|
||||||
|
/opt/mc-servers/{serverId}/
|
||||||
|
├── server.properties # MC config (editable via API)
|
||||||
|
├── world/ # World data (backed up)
|
||||||
|
├── plugins/ # Bukkit-type only
|
||||||
|
├── mods/ # Forge/Fabric only
|
||||||
|
├── logs/ # Server logs
|
||||||
|
└── backups/ # Backup archives
|
||||||
|
```
|
||||||
|
|
||||||
|
### Console & Logs
|
||||||
|
- **Log streaming:** `container.logs({ follow: true, stdout: true, stderr: true })` → stream to client via SSE or WebSocket
|
||||||
|
- **Command execution:** `container.exec()` to run `rcon-cli` or attach to stdin to send commands
|
||||||
|
- Alternative: use RCON protocol directly on the `rconPort` if RCON is enabled in server.properties
|
||||||
|
|
||||||
|
### Key Patterns for `src/lib/docker.ts`
|
||||||
|
- Export a singleton `getDockerClient()` that returns a cached dockerode instance
|
||||||
|
- All container operations must catch Docker API errors and map to proper HTTP status codes
|
||||||
|
- Container names follow convention: `mc-{server._id}` for easy lookup
|
||||||
|
- Always set resource limits: `--memory` from `server.memory.max`, CPU shares as needed
|
||||||
|
- Use `RestartPolicy: { Name: 'unless-stopped' }` when `server.autoRestart` is true
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
- Pause world saving (`save-off` + `save-all` via RCON/exec) → tar/gzip the `world/` directory → resume (`save-on`)
|
||||||
|
- Store backup archives in `{serverDir}/backups/` and record metadata in the Backup model
|
||||||
|
- Restore: stop container → replace `world/` with extracted backup → start container
|
||||||
|
|
||||||
|
### Scheduled Backups (node-cron)
|
||||||
|
- Use **node-cron** running inside the Next.js process — no external scheduler needed
|
||||||
|
- Each server stores a `backupSchedule` cron expression (e.g., `0 */6 * * *`) and `backupRetention` count
|
||||||
|
- On app startup, query all servers with a `backupSchedule` and register cron jobs via `cron.schedule()`
|
||||||
|
- When a server's schedule is created/updated/deleted via API, dynamically add/update/remove the cron job
|
||||||
|
- Keep a `Map<serverId, CronJob>` in memory for lifecycle management
|
||||||
|
- `backupRetention`: after each successful backup, delete oldest archives exceeding the limit
|
||||||
|
- Scheduled backups set `backup.type = 'scheduled'`; manual backups set `backup.type = 'manual'`
|
||||||
|
|
||||||
|
### Environment Variables (Docker-specific)
|
||||||
|
```env
|
||||||
|
MC_SERVERS_PATH=/opt/mc-servers # Base path for server volumes
|
||||||
|
DOCKER_SOCKET=/var/run/docker.sock # Docker socket path (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
```bash
|
||||||
|
npm run dev # Start dev server (port 3000, Turbopack)
|
||||||
|
npm run build # Production build
|
||||||
|
npm run lint # ESLint check
|
||||||
|
```
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -40,12 +40,3 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# github
|
|
||||||
/.github/
|
|
||||||
|
|
||||||
# vscode
|
|
||||||
/.vscode/
|
|
||||||
|
|
||||||
# claude
|
|
||||||
/.claude/
|
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.fontFamily": "'CaskaydiaMono Nerd Font Mono'",
|
||||||
|
"editor.allowVariableFonts": true
|
||||||
|
}
|
||||||
@ -48,7 +48,6 @@ MC-Manager is a self-hosted Minecraft server management panel that lets you crea
|
|||||||
|
|
||||||
### Security & Administration
|
### Security & Administration
|
||||||
- **Role-based access control** — Granular permissions (`resource:action` format) with wildcard support
|
- **Role-based access control** — Granular permissions (`resource:action` format) with wildcard support
|
||||||
- **Per-server admins** — Assign individual users as admins of specific servers for full operational control (start/stop/restart, console, config, backups, plugins/mods, players) without granting global access
|
|
||||||
- **Dual-token JWT auth** — 1-hour access tokens + 7-day refresh tokens in HTTP-only cookies
|
- **Dual-token JWT auth** — 1-hour access tokens + 7-day refresh tokens in HTTP-only cookies
|
||||||
- **Mandatory 2FA** — Email-based two-factor authentication on every login
|
- **Mandatory 2FA** — Email-based two-factor authentication on every login
|
||||||
- **Account lockout** — Automatic lockout after 5 failed login attempts (30-minute cooldown)
|
- **Account lockout** — Automatic lockout after 5 failed login attempts (30-minute cooldown)
|
||||||
@ -159,7 +158,6 @@ src/
|
|||||||
│ │ ├── dashboard/ # Dashboard overview
|
│ │ ├── dashboard/ # Dashboard overview
|
||||||
│ │ ├── servers/ # Server list & detail pages
|
│ │ ├── servers/ # Server list & detail pages
|
||||||
│ │ │ └── [id]/ # Per-server views
|
│ │ │ └── [id]/ # Per-server views
|
||||||
│ │ │ ├── admins/ # Server admin management
|
|
||||||
│ │ │ ├── backups/ # Backup management
|
│ │ │ ├── backups/ # Backup management
|
||||||
│ │ │ ├── configuration/ # server.properties & JVM config
|
│ │ │ ├── configuration/ # server.properties & JVM config
|
||||||
│ │ │ ├── console/ # Live console
|
│ │ │ ├── console/ # Live console
|
||||||
|
|||||||
@ -1,208 +0,0 @@
|
|||||||
'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 Select from '@/components/ui/Select'
|
|
||||||
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)}
|
|
||||||
placeholder="Select a user..."
|
|
||||||
options={availableUsers.map(u => ({ value: u._id, label: `${u.username} (${u.email})` }))}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<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, ShieldCheck
|
Puzzle, Package, Users, Trash2, ArrowLeft, FileText, FolderOpen
|
||||||
} 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, user } = useAuth()
|
const { hasPermission } = useAuth()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { showConfirmation } = useConfirmation()
|
const { showConfirmation } = useConfirmation()
|
||||||
|
|
||||||
@ -172,10 +172,6 @@ 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' },
|
||||||
@ -185,7 +181,6 @@ 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 (
|
||||||
@ -204,17 +199,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" />
|
||||||
|
|
||||||
{canDo('servers:start') && server.status === 'offline' && (
|
{hasPermission('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>
|
||||||
)}
|
)}
|
||||||
{canDo('servers:stop') && server.status === 'online' && (
|
{hasPermission('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>
|
||||||
)}
|
)}
|
||||||
{canDo('servers:restart') && server.status === 'online' && (
|
{hasPermission('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>
|
||||||
@ -264,7 +259,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 => canDo(link.permission))
|
.filter(link => hasPermission(link.permission))
|
||||||
.map(link => {
|
.map(link => {
|
||||||
const Icon = link.icon
|
const Icon = link.icon
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
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'
|
|
||||||
import mongoose from 'mongoose'
|
|
||||||
|
|
||||||
// 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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedUser = await User.findById(userId, { username: 1 }).lean()
|
|
||||||
|
|
||||||
await Server.updateOne(
|
|
||||||
{ _id: id },
|
|
||||||
{ $pull: { admins: new mongoose.Types.ObjectId(userId) } }
|
|
||||||
)
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
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, hasServerPermission } from '@/lib/auth'
|
import { validateSession, hasPermission } 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,6 +21,10 @@ 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 })
|
||||||
@ -32,11 +36,6 @@ 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, hasServerPermission } from '@/lib/auth'
|
import { validateSession, hasPermission } from '@/lib/auth'
|
||||||
import connectToDatabase from '@/lib/mongodb'
|
import connectToDatabase from '@/lib/mongodb'
|
||||||
import { Backup, Server } from '@/lib/models'
|
import { Backup } 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,22 +19,16 @@ 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, hasServerPermission } from '@/lib/auth'
|
import { validateSession, hasPermission } 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,22 +20,16 @@ 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 })
|
||||||
@ -58,6 +52,10 @@ 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 })
|
||||||
@ -69,11 +67,6 @@ 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, hasServerPermission } 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'
|
||||||
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,6 +19,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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')
|
||||||
|
|
||||||
@ -81,6 +80,10 @@ 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 })
|
||||||
@ -95,11 +98,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -16,6 +16,10 @@ 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 })
|
||||||
@ -27,11 +31,6 @@ 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 })
|
||||||
@ -94,6 +93,10 @@ 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 })
|
||||||
@ -110,11 +113,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -48,6 +48,10 @@ 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 })
|
||||||
@ -59,11 +63,6 @@ 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'
|
||||||
|
|
||||||
@ -177,6 +176,10 @@ 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 })
|
||||||
@ -188,11 +191,6 @@ 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
|
||||||
|
|
||||||
@ -230,6 +228,10 @@ 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 })
|
||||||
@ -241,11 +243,6 @@ 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 })
|
||||||
@ -286,6 +283,10 @@ 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 })
|
||||||
@ -297,11 +298,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,6 +18,10 @@ 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 })
|
||||||
@ -29,11 +33,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,6 +21,10 @@ 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 })
|
||||||
@ -34,11 +38,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,6 +21,10 @@ 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 })
|
||||||
@ -34,11 +38,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,6 +19,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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 })
|
||||||
}
|
}
|
||||||
@ -82,6 +81,10 @@ 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 })
|
||||||
@ -93,11 +96,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -38,6 +38,10 @@ 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 })
|
||||||
}
|
}
|
||||||
@ -48,11 +52,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -27,6 +27,10 @@ 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 })
|
||||||
@ -38,11 +42,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,6 +21,10 @@ 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 })
|
||||||
@ -34,11 +38,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -21,6 +21,10 @@ 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 })
|
||||||
@ -34,11 +38,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -19,6 +19,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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 })
|
||||||
}
|
}
|
||||||
@ -82,6 +81,10 @@ 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 })
|
||||||
@ -93,11 +96,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,6 +18,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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, hasServerPermission } 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'
|
||||||
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
import { sanitizeObject, isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -17,6 +17,10 @@ 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 })
|
||||||
@ -29,11 +33,6 @@ 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}`)
|
||||||
@ -70,6 +69,10 @@ 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 })
|
||||||
@ -85,11 +88,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,6 +18,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -46,6 +46,10 @@ 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 })
|
||||||
@ -57,11 +61,6 @@ 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, hasServerPermission } 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'
|
||||||
import { isValidObjectId } from '@/lib/input-validation'
|
import { isValidObjectId } from '@/lib/input-validation'
|
||||||
@ -18,6 +18,10 @@ 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 })
|
||||||
@ -30,11 +34,6 @@ 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,5 +1,4 @@
|
|||||||
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'
|
||||||
@ -15,18 +14,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase()
|
if (!hasPermission(session, 'servers:view')) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await connectToDatabase()
|
||||||
|
const servers = await Server.find().sort({ createdAt: -1 }).lean()
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: servers })
|
return NextResponse.json({ success: true, data: servers })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch servers error:', error)
|
console.error('Fetch servers error:', error)
|
||||||
@ -101,7 +95,6 @@ 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
|
||||||
|
|||||||
@ -59,7 +59,6 @@ body {
|
|||||||
color: rgba(255, 255, 255, 0.3);
|
color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ─── Glass Shine (top border gradient) ───────────────────────── */
|
/* ─── Glass Shine (top border gradient) ───────────────────────── */
|
||||||
|
|
||||||
.glass-shine {
|
.glass-shine {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useState, useMemo } from 'react'
|
|||||||
import { Search, Plus, ChevronUp, ChevronDown } from 'lucide-react'
|
import { Search, Plus, ChevronUp, ChevronDown } from 'lucide-react'
|
||||||
import { LucideIcon } from 'lucide-react'
|
import { LucideIcon } from 'lucide-react'
|
||||||
import PageHeader from '@/components/PageHeader'
|
import PageHeader from '@/components/PageHeader'
|
||||||
import Select from '@/components/ui/Select'
|
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import Spinner from '@/components/ui/Spinner'
|
import Spinner from '@/components/ui/Spinner'
|
||||||
|
|
||||||
@ -150,13 +149,20 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
{filters.map(filter => (
|
{filters.map(filter => (
|
||||||
<Select
|
<select
|
||||||
key={filter.filterKey}
|
key={filter.filterKey}
|
||||||
value={activeFilters[filter.filterKey] || ''}
|
value={activeFilters[filter.filterKey] || ''}
|
||||||
onChange={e => setActiveFilters(prev => ({ ...prev, [filter.filterKey]: e.target.value }))}
|
onChange={e => setActiveFilters(prev => ({ ...prev, [filter.filterKey]: e.target.value }))}
|
||||||
placeholder={filter.label}
|
className="px-3 py-2 glass-input text-gray-100 rounded-lg text-sm
|
||||||
options={filter.options}
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
|
||||||
/>
|
>
|
||||||
|
<option value="">{filter.label}</option>
|
||||||
|
{filter.options.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,124 +1,49 @@
|
|||||||
'use client'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react'
|
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
import { ChevronDown } from 'lucide-react'
|
|
||||||
|
|
||||||
interface SelectProps {
|
|
||||||
label?: string
|
label?: string
|
||||||
error?: string
|
error?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
options: { label: string; value: string }[]
|
options: { label: string; value: string }[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
value?: string
|
|
||||||
defaultValue?: string
|
|
||||||
onChange?: (e: { target: { value: string; name?: string } }) => void
|
|
||||||
disabled?: boolean
|
|
||||||
className?: string
|
|
||||||
name?: string
|
|
||||||
id?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Select({
|
const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
label,
|
({ label, error, required, options, placeholder, className = '', ...props }, ref) => {
|
||||||
error,
|
return (
|
||||||
required,
|
<div className="space-y-1.5">
|
||||||
options,
|
{label && (
|
||||||
placeholder,
|
<label className="block text-sm font-medium text-gray-300">
|
||||||
value,
|
{label}
|
||||||
defaultValue,
|
{required && <span className="text-red-400 ml-1">*</span>}
|
||||||
onChange,
|
</label>
|
||||||
disabled,
|
|
||||||
className = '',
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
}: SelectProps) {
|
|
||||||
const isControlled = value !== undefined
|
|
||||||
const [internalValue, setInternalValue] = useState(defaultValue ?? '')
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const currentValue = isControlled ? value : internalValue
|
|
||||||
const selected = options.find(o => o.value === currentValue)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSelect = (optValue: string) => {
|
|
||||||
if (!isControlled) setInternalValue(optValue)
|
|
||||||
onChange?.({ target: { value: optValue, name } })
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`space-y-1.5 ${className}`}>
|
|
||||||
{label && (
|
|
||||||
<label htmlFor={id} className="block text-sm font-medium text-gray-300">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-400 ml-1">*</span>}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hidden input for FormData compatibility */}
|
|
||||||
{name && <input type="hidden" name={name} value={currentValue} />}
|
|
||||||
|
|
||||||
<div ref={ref} className="relative">
|
|
||||||
<button
|
|
||||||
id={id}
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => !disabled && setOpen(prev => !prev)}
|
|
||||||
className={`w-full px-4 py-2 glass-input text-left flex items-center justify-between rounded-lg
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-cyan-400/40
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200
|
|
||||||
${error ? 'border-red-500' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<span className={selected ? 'text-gray-100 text-sm' : 'text-gray-500 text-sm'}>
|
|
||||||
{selected ? selected.label : (placeholder ?? 'Select...')}
|
|
||||||
</span>
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={`text-gray-400 shrink-0 ml-2 transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="absolute z-50 w-full mt-1 rounded-lg overflow-hidden shadow-xl shadow-black/50 border border-white/[0.15]" style={{ background: 'rgba(15, 15, 28, 0.95)', backdropFilter: 'blur(24px)' }}>
|
|
||||||
{placeholder && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSelect('')}
|
|
||||||
className="w-full px-4 py-2.5 text-left text-sm text-gray-500 hover:bg-white/[0.06] transition-colors"
|
|
||||||
>
|
|
||||||
{placeholder}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{options.map(opt => (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSelect(opt.value)}
|
|
||||||
className={`w-full px-4 py-2.5 text-left text-sm transition-colors
|
|
||||||
${opt.value === currentValue
|
|
||||||
? 'bg-cyan-500/20 text-cyan-400'
|
|
||||||
: 'text-gray-200 hover:bg-white/[0.06]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<select
|
||||||
|
ref={ref}
|
||||||
|
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
|
||||||
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
|
||||||
|
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
|
||||||
|
error ? 'border-red-500 focus:ring-red-500' : ''
|
||||||
|
} ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" className="text-gray-500">
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
Select.displayName = 'Select'
|
||||||
</div>
|
export default Select
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -149,18 +149,3 @@ 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,7 +83,6 @@ 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
|
||||||
@ -114,7 +113,6 @@ 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,7 +25,6 @@ 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