From 79b41409e57a30f4a82142e5c6d65a68434078aa Mon Sep 17 00:00:00 2001 From: rmoren97 Date: Sat, 7 Feb 2026 12:20:12 -0800 Subject: [PATCH] initial commit --- .github/copilot-instructions.md | 213 +++ .vscode/settings.json | 4 + next.config.ts | 16 +- package-lock.json | 1699 ++++++++++++++++- package.json | 19 +- scripts/seed.ts | 218 +++ src/app/(app)/audit/page.tsx | 169 ++ src/app/(app)/dashboard/page.tsx | 163 ++ src/app/(app)/layout.tsx | 28 + src/app/(app)/roles/page.tsx | 319 ++++ src/app/(app)/servers/[id]/backups/page.tsx | 212 ++ .../(app)/servers/[id]/configuration/page.tsx | 161 ++ src/app/(app)/servers/[id]/console/page.tsx | 33 + src/app/(app)/servers/[id]/files/page.tsx | 1076 +++++++++++ src/app/(app)/servers/[id]/logs/page.tsx | 182 ++ src/app/(app)/servers/[id]/mods/page.tsx | 221 +++ src/app/(app)/servers/[id]/page.tsx | 292 +++ src/app/(app)/servers/[id]/players/page.tsx | 284 +++ src/app/(app)/servers/[id]/plugins/page.tsx | 221 +++ src/app/(app)/servers/page.tsx | 171 ++ src/app/(app)/users/page.tsx | 280 +++ src/app/api/audit/route.ts | 61 + src/app/api/auth/login/route.ts | 123 ++ src/app/api/auth/logout/route.ts | 8 + src/app/api/auth/me/route.ts | 25 + src/app/api/auth/refresh/route.ts | 46 + src/app/api/auth/verify-2fa/route.ts | 110 ++ src/app/api/health/route.ts | 19 + src/app/api/roles/[id]/route.ts | 161 ++ src/app/api/roles/route.ts | 86 + .../[id]/backups/[backupId]/restore/route.ts | 82 + .../servers/[id]/backups/[backupId]/route.ts | 63 + src/app/api/servers/[id]/backups/route.ts | 153 ++ .../api/servers/[id]/configuration/route.ts | 134 ++ src/app/api/servers/[id]/console/route.ts | 148 ++ src/app/api/servers/[id]/files/route.ts | 669 +++++++ src/app/api/servers/[id]/logs/route.ts | 111 ++ .../api/servers/[id]/mods/[filename]/route.ts | 67 + .../[id]/mods/[filename]/toggle/route.ts | 75 + src/app/api/servers/[id]/mods/route.ts | 134 ++ .../[id]/players/[name]/[action]/route.ts | 104 + src/app/api/servers/[id]/players/route.ts | 158 ++ .../servers/[id]/plugins/[filename]/route.ts | 67 + .../[id]/plugins/[filename]/toggle/route.ts | 75 + src/app/api/servers/[id]/plugins/route.ts | 134 ++ src/app/api/servers/[id]/restart/route.ts | 90 + src/app/api/servers/[id]/route.ts | 196 ++ src/app/api/servers/[id]/start/route.ts | 91 + src/app/api/servers/[id]/stats/route.ts | 160 ++ src/app/api/servers/[id]/stop/route.ts | 69 + src/app/api/servers/route.ts | 150 ++ src/app/api/users/[id]/route.ts | 187 ++ src/app/api/users/route.ts | 119 ++ src/app/globals.css | 87 +- src/app/layout.tsx | 40 +- src/app/login/page.tsx | 146 ++ src/app/page.tsx | 84 +- src/components/ConsoleViewer.tsx | 136 ++ src/components/Drawer.tsx | 61 + src/components/Modal.tsx | 57 + src/components/PageHeader.tsx | 25 + src/components/ProtectedComponent.tsx | 23 + src/components/ServerStatusBadge.tsx | 46 + src/components/Sidebar.tsx | 110 ++ src/components/StatsChart.tsx | 127 ++ .../templates/DataManagementTemplate.tsx | 239 +++ src/components/ui/Badge.tsx | 23 + src/components/ui/Button.tsx | 53 + src/components/ui/Input.tsx | 35 + src/components/ui/Select.tsx | 48 + src/components/ui/Spinner.tsx | 18 + src/contexts/AuthContext.tsx | 118 ++ src/contexts/ConfirmationContext.tsx | 121 ++ src/contexts/ToastContext.tsx | 85 + src/hooks/useConsole.ts | 71 + src/hooks/useServerStatus.ts | 85 + src/lib/audit.ts | 69 + src/lib/auth.ts | 151 ++ src/lib/backup-scheduler.ts | 205 ++ src/lib/date-utils.ts | 86 + src/lib/docker.ts | 186 ++ src/lib/email-graph.ts | 87 + src/lib/email.ts | 109 ++ src/lib/input-validation.ts | 69 + src/lib/models.ts | 197 ++ src/lib/mongodb.ts | 43 + src/middleware.ts | 66 + src/types/backup.ts | 15 + src/types/index.ts | 37 + src/types/server.ts | 66 + src/types/user.ts | 42 + 91 files changed, 13009 insertions(+), 113 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .vscode/settings.json create mode 100644 scripts/seed.ts create mode 100644 src/app/(app)/audit/page.tsx create mode 100644 src/app/(app)/dashboard/page.tsx create mode 100644 src/app/(app)/layout.tsx create mode 100644 src/app/(app)/roles/page.tsx create mode 100644 src/app/(app)/servers/[id]/backups/page.tsx create mode 100644 src/app/(app)/servers/[id]/configuration/page.tsx create mode 100644 src/app/(app)/servers/[id]/console/page.tsx create mode 100644 src/app/(app)/servers/[id]/files/page.tsx create mode 100644 src/app/(app)/servers/[id]/logs/page.tsx create mode 100644 src/app/(app)/servers/[id]/mods/page.tsx create mode 100644 src/app/(app)/servers/[id]/page.tsx create mode 100644 src/app/(app)/servers/[id]/players/page.tsx create mode 100644 src/app/(app)/servers/[id]/plugins/page.tsx create mode 100644 src/app/(app)/servers/page.tsx create mode 100644 src/app/(app)/users/page.tsx create mode 100644 src/app/api/audit/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/auth/refresh/route.ts create mode 100644 src/app/api/auth/verify-2fa/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/roles/[id]/route.ts create mode 100644 src/app/api/roles/route.ts create mode 100644 src/app/api/servers/[id]/backups/[backupId]/restore/route.ts create mode 100644 src/app/api/servers/[id]/backups/[backupId]/route.ts create mode 100644 src/app/api/servers/[id]/backups/route.ts create mode 100644 src/app/api/servers/[id]/configuration/route.ts create mode 100644 src/app/api/servers/[id]/console/route.ts create mode 100644 src/app/api/servers/[id]/files/route.ts create mode 100644 src/app/api/servers/[id]/logs/route.ts create mode 100644 src/app/api/servers/[id]/mods/[filename]/route.ts create mode 100644 src/app/api/servers/[id]/mods/[filename]/toggle/route.ts create mode 100644 src/app/api/servers/[id]/mods/route.ts create mode 100644 src/app/api/servers/[id]/players/[name]/[action]/route.ts create mode 100644 src/app/api/servers/[id]/players/route.ts create mode 100644 src/app/api/servers/[id]/plugins/[filename]/route.ts create mode 100644 src/app/api/servers/[id]/plugins/[filename]/toggle/route.ts create mode 100644 src/app/api/servers/[id]/plugins/route.ts create mode 100644 src/app/api/servers/[id]/restart/route.ts create mode 100644 src/app/api/servers/[id]/route.ts create mode 100644 src/app/api/servers/[id]/start/route.ts create mode 100644 src/app/api/servers/[id]/stats/route.ts create mode 100644 src/app/api/servers/[id]/stop/route.ts create mode 100644 src/app/api/servers/route.ts create mode 100644 src/app/api/users/[id]/route.ts create mode 100644 src/app/api/users/route.ts create mode 100644 src/app/login/page.tsx create mode 100644 src/components/ConsoleViewer.tsx create mode 100644 src/components/Drawer.tsx create mode 100644 src/components/Modal.tsx create mode 100644 src/components/PageHeader.tsx create mode 100644 src/components/ProtectedComponent.tsx create mode 100644 src/components/ServerStatusBadge.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/StatsChart.tsx create mode 100644 src/components/templates/DataManagementTemplate.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Select.tsx create mode 100644 src/components/ui/Spinner.tsx create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/contexts/ConfirmationContext.tsx create mode 100644 src/contexts/ToastContext.tsx create mode 100644 src/hooks/useConsole.ts create mode 100644 src/hooks/useServerStatus.ts create mode 100644 src/lib/audit.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/backup-scheduler.ts create mode 100644 src/lib/date-utils.ts create mode 100644 src/lib/docker.ts create mode 100644 src/lib/email-graph.ts create mode 100644 src/lib/email.ts create mode 100644 src/lib/input-validation.ts create mode 100644 src/lib/models.ts create mode 100644 src/lib/mongodb.ts create mode 100644 src/middleware.ts create mode 100644 src/types/backup.ts create mode 100644 src/types/index.ts create mode 100644 src/types/server.ts create mode 100644 src/types/user.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9162ed5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 `` 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` 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 +``` diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c322b10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.fontFamily": "'CaskaydiaMono Nerd Font Mono'", + "editor.allowVariableFonts": true +} \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..3c9afe9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Allow server-side Node.js modules used by dockerode, child_process, etc. + serverExternalPackages: ['dockerode', 'node-cron'], + + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'mc-heads.net', + }, + { + protocol: 'https', + hostname: 'crafatar.com', + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 2c477aa..4aca0fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,34 @@ "name": "mc-manager", "version": "0.1.0", "dependencies": { + "@azure/msal-node": "^5.0.3", + "@microsoft/microsoft-graph-client": "^3.0.7", + "bcryptjs": "^3.0.3", + "dockerode": "^4.0.9", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.563.0", + "mongoose": "^9.1.6", "next": "16.1.6", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.1", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/dockerode": "^4.0.1", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.9", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.2.4", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -36,6 +52,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@azure/msal-common": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.0.3.tgz", + "integrity": "sha512-3aedNnM0CHVuVZ+BqembdZWgovqe96BJ4YxGoIK0+qhoBZQsAhfwXdhjen72K94pkSQHtzlJ7fAq6w7knFZsng==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.0.3.tgz", + "integrity": "sha512-DTiu9SEblUVbgiuXWtCXFS2OoIVZPNbPFfK7AVhsWCD4bCHqvHcnDz9uCxk5wnUYJX9Ik0KxjGDFLwH9/lrE+w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.0.3", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -228,6 +267,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -276,6 +324,12 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -309,6 +363,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -453,6 +949,55 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1021,6 +1566,52 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@microsoft/microsoft-graph-client": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.7.tgz", + "integrity": "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependenciesMeta": { + "@azure/identity": { + "optional": true + }, + "@azure/msal-browser": { + "optional": true + }, + "buffer": { + "optional": true + }, + "stream-browserify": { + "optional": true + } + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", + "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1226,6 +1817,70 @@ "node": ">=12.4.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1524,6 +2179,36 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1545,16 +2230,50 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", @@ -1575,6 +2294,48 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -2153,11 +2914,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2346,6 +3115,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2406,6 +3184,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -2415,6 +3213,35 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2473,6 +3300,54 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2570,17 +3445,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2593,7 +3487,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -2610,6 +3503,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2697,7 +3604,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2764,6 +3670,52 @@ "node": ">=8" } }, + "node_modules/docker-modem": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.9.tgz", + "integrity": "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2777,6 +3729,19 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", + "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2792,6 +3757,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -2806,6 +3780,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", @@ -2997,11 +3980,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3585,6 +4609,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3646,6 +4691,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3890,6 +4944,26 @@ "hermes-estree": "0.25.1" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3927,6 +5001,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4126,6 +5206,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4466,6 +5555,40 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4482,6 +5605,36 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4803,6 +5956,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4810,6 +6005,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4833,6 +6040,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4853,6 +6069,12 @@ "node": ">= 0.4" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4900,13 +6122,123 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.1.6.tgz", + "integrity": "sha512-ZrtgRsJKtW3od36TVXtAnrNHOO3rqhsqfVut6IzyWyJeLLeLTqW66qej/0qB37GZd5jL06nHfAceTikkfnSqbA==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5029,6 +6361,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5036,6 +6377,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5159,6 +6509,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5347,11 +6706,44 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5406,6 +6798,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5450,6 +6856,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5546,6 +6961,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5581,6 +7016,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5803,6 +7244,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5812,6 +7259,38 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5833,6 +7312,35 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5946,6 +7454,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6039,6 +7559,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6100,6 +7648,18 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -6145,6 +7705,32 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6297,7 +7883,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6376,6 +7961,43 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6491,6 +8113,38 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6498,6 +8152,33 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8b63fc7..8f67855 100644 --- a/package.json +++ b/package.json @@ -6,21 +6,38 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "seed": "tsx scripts/seed.ts" }, "dependencies": { + "@azure/msal-node": "^5.0.3", + "@microsoft/microsoft-graph-client": "^3.0.7", + "bcryptjs": "^3.0.3", + "dockerode": "^4.0.9", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.563.0", + "mongoose": "^9.1.6", "next": "16.1.6", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.1", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/dockerode": "^4.0.1", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.9", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.2.4", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } } diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..3ab9dd8 --- /dev/null +++ b/scripts/seed.ts @@ -0,0 +1,218 @@ +/** + * Seed script — creates the initial Admin role and admin user. + * + * Usage: + * npx tsx scripts/seed.ts + * + * Requires a valid MONGODB_URI in .env (or .env.local). + */ + +import mongoose from 'mongoose' +import bcrypt from 'bcryptjs' +import * as readline from 'readline' +import * as dotenv from 'dotenv' + +// Load environment variables +dotenv.config({ path: '.env.local' }) +dotenv.config({ path: '.env' }) + +const MONGODB_URI = process.env.MONGODB_URI + +if (!MONGODB_URI) { + console.error('❌ MONGODB_URI is not defined. Create a .env or .env.local file first.') + process.exit(1) +} + +// ─── Inline Schemas (avoid importing from src/ which needs TS paths) ───── + +const permissionSchema = new mongoose.Schema( + { + resource: { type: String, required: true }, + actions: [{ type: String, required: true }], + }, + { _id: false } +) + +const roleSchema = new mongoose.Schema( + { + name: { type: String, required: true, unique: true, trim: true }, + permissions: [permissionSchema], + description: { type: String, default: '' }, + isDefault: { type: Boolean, default: false }, + }, + { timestamps: true } +) + +const userSchema = new mongoose.Schema( + { + username: { type: String, required: true, unique: true, trim: true }, + email: { type: String, required: true, unique: true, trim: true, lowercase: true }, + passwordHash: { type: String, required: true }, + roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }], + twoFactorCode: { type: String, default: null }, + twoFactorExpiry: { type: Date, default: null }, + loginAttempts: { type: Number, default: 0 }, + lockUntil: { type: Date, default: null }, + status: { type: String, enum: ['active', 'inactive', 'locked'], default: 'active' }, + lastLogin: { type: Date, default: null }, + }, + { timestamps: true } +) + +const Role = mongoose.models.Role || mongoose.model('Role', roleSchema) +const User = mongoose.models.User || mongoose.model('User', userSchema) + +// ─── Helpers ───────────────────────────────────────────────────── + +function prompt(question: string): Promise { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + return new Promise(resolve => { + rl.question(question, answer => { + rl.close() + resolve(answer.trim()) + }) + }) +} + +// ─── Admin Permissions (wildcard on every resource) ────────────── + +const ADMIN_PERMISSIONS = [ + { resource: '*', actions: ['*'] }, +] + +// ─── Main ──────────────────────────────────────────────────────── + +async function main() { + console.log('🌱 MC-Manager Seed Script\n') + + await mongoose.connect(MONGODB_URI!) + console.log('✅ Connected to MongoDB\n') + + // ── 1. Create or update Admin Role ────────────────────────────── + let adminRole = await Role.findOne({ name: 'Admin' }) + + if (adminRole) { + console.log('ℹ️ Admin role already exists — updating permissions...') + adminRole.permissions = ADMIN_PERMISSIONS + adminRole.description = 'Full system administrator with all permissions' + await adminRole.save() + } else { + adminRole = await Role.create({ + name: 'Admin', + permissions: ADMIN_PERMISSIONS, + description: 'Full system administrator with all permissions', + isDefault: false, + }) + console.log('✅ Admin role created') + } + + // ── 2. Create default Viewer role ─────────────────────────────── + let viewerRole = await Role.findOne({ name: 'Viewer' }) + + if (!viewerRole) { + viewerRole = await Role.create({ + name: 'Viewer', + permissions: [ + { resource: 'servers', actions: ['view'] }, + { resource: 'backups', actions: ['view'] }, + { resource: 'plugins', actions: ['view'] }, + { resource: 'mods', actions: ['view'] }, + { resource: 'players', actions: ['view'] }, + ], + description: 'Read-only access to servers and related resources', + isDefault: true, + }) + console.log('✅ Viewer role created') + } else { + console.log('ℹ️ Viewer role already exists — skipped') + } + + // ── 3. Create Operator role ───────────────────────────────────── + let operatorRole = await Role.findOne({ name: 'Operator' }) + + if (!operatorRole) { + operatorRole = await Role.create({ + name: 'Operator', + permissions: [ + { resource: 'servers', actions: ['view', 'start', 'stop', 'restart', 'console'] }, + { resource: 'backups', actions: ['view', 'create'] }, + { resource: 'plugins', actions: ['view', 'install', 'remove', 'toggle'] }, + { resource: 'mods', actions: ['view', 'install', 'remove', 'toggle'] }, + { resource: 'players', actions: ['view', 'whitelist', 'op', 'ban'] }, + ], + description: 'Can operate servers but cannot create/delete them or manage users', + isDefault: false, + }) + console.log('✅ Operator role created') + } else { + console.log('ℹ️ Operator role already exists — skipped') + } + + // ── 4. Create Admin User ──────────────────────────────────────── + const existingAdmin = await User.findOne({ username: 'admin' }) + + if (existingAdmin) { + console.log('\nℹ️ Admin user already exists.') + const reset = await prompt('Reset admin password? (y/N): ') + + if (reset.toLowerCase() === 'y') { + const newPassword = await prompt('New password (min 8 chars): ') + if (newPassword.length < 8) { + console.error('❌ Password must be at least 8 characters.') + await mongoose.disconnect() + process.exit(1) + } + existingAdmin.passwordHash = await bcrypt.hash(newPassword, 12) + existingAdmin.status = 'active' + existingAdmin.loginAttempts = 0 + existingAdmin.lockUntil = null + + // Ensure admin has the Admin role + if (!existingAdmin.roles.some((r: mongoose.Types.ObjectId) => r.equals(adminRole._id))) { + existingAdmin.roles.push(adminRole._id) + } + + await existingAdmin.save() + console.log('✅ Admin password reset successfully') + } + } else { + console.log('\n📝 Create initial admin user:\n') + + const email = await prompt('Admin email: ') + if (!email || !email.includes('@')) { + console.error('❌ A valid email is required.') + await mongoose.disconnect() + process.exit(1) + } + + const password = await prompt('Admin password (min 8 chars): ') + if (password.length < 8) { + console.error('❌ Password must be at least 8 characters.') + await mongoose.disconnect() + process.exit(1) + } + + const passwordHash = await bcrypt.hash(password, 12) + + await User.create({ + username: 'admin', + email, + passwordHash, + roles: [adminRole._id], + status: 'active', + }) + + console.log('✅ Admin user created (username: admin)') + } + + // ── Done ──────────────────────────────────────────────────────── + console.log('\n🎉 Seed complete! You can now log in at http://localhost:3000') + + await mongoose.disconnect() + process.exit(0) +} + +main().catch(err => { + console.error('❌ Seed failed:', err) + process.exit(1) +}) diff --git a/src/app/(app)/audit/page.tsx b/src/app/(app)/audit/page.tsx new file mode 100644 index 0000000..eb63d4f --- /dev/null +++ b/src/app/(app)/audit/page.tsx @@ -0,0 +1,169 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { FileText, Search } from 'lucide-react' +import PageHeader from '@/components/PageHeader' +import Badge from '@/components/ui/Badge' +import Spinner from '@/components/ui/Spinner' +import Select from '@/components/ui/Select' +import { formatDateTime } from '@/lib/date-utils' +import type { AuditLog } from '@/types' + +export default function AuditPage() { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(true) + const [search, setSearch] = useState('') + const [entityFilter, setEntityFilter] = useState('') + const [page, setPage] = useState(1) + const [totalPages, setTotalPages] = useState(1) + + const fetchLogs = useCallback(async () => { + setLoading(true) + try { + const params = new URLSearchParams({ page: String(page), limit: '50' }) + if (search) params.set('search', search) + if (entityFilter) params.set('entityType', entityFilter) + + const res = await fetch(`/api/audit?${params}`, { credentials: 'include' }) + if (res.ok) { + const data = await res.json() + setLogs(data.data || []) + setTotalPages(data.totalPages || 1) + } + } catch { + console.error('Failed to fetch audit logs') + } finally { + setLoading(false) + } + }, [page, search, entityFilter]) + + useEffect(() => { + fetchLogs() + }, [fetchLogs]) + + const entityTypes = [ + { label: 'Server', value: 'server' }, + { label: 'Backup', value: 'backup' }, + { label: 'User', value: 'user' }, + { label: 'Role', value: 'role' }, + { label: 'Plugin', value: 'plugin' }, + { label: 'Mod', value: 'mod' }, + { label: 'Player', value: 'player' }, + ] + + return ( +
+ + + {/* Filters */} +
+
+
+ + { setSearch(e.target.value); setPage(1) }} + placeholder="Search audit logs..." + className="w-full bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none" + /> +
+
+ + +
+ + +
+
+ + +
+ + + + {/* Edit Role Drawer */} + { setShowEdit(false); setSelectedRole(null) }} title={`Edit Role — ${selectedRole?.name}`}> + {selectedRole && ( +
+ + +
+ + +
+
+ + +
+
+ )} +
+ + ) +} diff --git a/src/app/(app)/servers/[id]/backups/page.tsx b/src/app/(app)/servers/[id]/backups/page.tsx new file mode 100644 index 0000000..c745317 --- /dev/null +++ b/src/app/(app)/servers/[id]/backups/page.tsx @@ -0,0 +1,212 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useParams } from 'next/navigation' +import { ArrowLeft, HardDrive, RotateCcw, Plus, Trash2 } from 'lucide-react' +import Link from 'next/link' +import PageHeader from '@/components/PageHeader' +import Button from '@/components/ui/Button' +import Badge from '@/components/ui/Badge' +import Spinner from '@/components/ui/Spinner' +import { useToast } from '@/contexts/ToastContext' +import { useAuth } from '@/contexts/AuthContext' +import { useConfirmation } from '@/contexts/ConfirmationContext' +import { formatDateTime, formatFileSize } from '@/lib/date-utils' +import type { Backup } from '@/types/backup' + +export default function BackupsPage() { + const params = useParams() + const serverId = params.id as string + const { showToast } = useToast() + const { hasPermission } = useAuth() + const { showConfirmation } = useConfirmation() + + const [backups, setBackups] = useState([]) + const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) + + const fetchBackups = useCallback(async () => { + try { + const res = await fetch(`/api/servers/${serverId}/backups`, { credentials: 'include' }) + if (res.ok) { + const data = await res.json() + setBackups(data.data || []) + } + } catch { + showToast('Failed to load backups', 'error') + } finally { + setLoading(false) + } + }, [serverId, showToast]) + + useEffect(() => { + fetchBackups() + }, [fetchBackups]) + + const handleCreate = async () => { + setCreating(true) + try { + const res = await fetch(`/api/servers/${serverId}/backups`, { + method: 'POST', + credentials: 'include', + }) + + if (res.ok) { + showToast('Backup started', 'success') + fetchBackups() + } else { + const data = await res.json() + showToast(data.error || 'Failed to create backup', 'error') + } + } catch { + showToast('Failed to create backup', 'error') + } finally { + setCreating(false) + } + } + + const handleRestore = async (backup: Backup) => { + const confirmed = await showConfirmation({ + title: 'Restore Backup', + message: 'This will stop the server and replace the current world with the backup. Continue?', + itemName: backup.filename, + type: 'warning', + confirmText: 'Restore', + }) + + if (!confirmed) return + + try { + const res = await fetch(`/api/servers/${serverId}/backups/${backup._id}/restore`, { + method: 'POST', + credentials: 'include', + }) + + if (res.ok) { + showToast('Backup restoration started', 'success') + } else { + const data = await res.json() + showToast(data.error || 'Failed to restore backup', 'error') + } + } catch { + showToast('Failed to restore backup', 'error') + } + } + + const handleDelete = async (backup: Backup) => { + const confirmed = await showConfirmation({ + title: 'Delete Backup', + message: 'Are you sure you want to permanently delete', + itemName: backup.filename, + type: 'danger', + confirmText: 'Delete', + }) + + if (!confirmed) return + + try { + const res = await fetch(`/api/servers/${serverId}/backups/${backup._id}`, { + method: 'DELETE', + credentials: 'include', + }) + + if (res.ok) { + showToast('Backup deleted', 'success') + fetchBackups() + } else { + const data = await res.json() + showToast(data.error || 'Failed to delete backup', 'error') + } + } catch { + showToast('Failed to delete backup', 'error') + } + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+ + Back to Server + + + + Create Backup + + ) : undefined + } + /> + +
+ {backups.length === 0 ? ( +
+ +

No backups yet

+

Create your first backup to protect your world data.

+
+ ) : ( + + + + + + + + + + + + + {backups.map(backup => ( + + + + + + + + + ))} + +
FilenameTypeSizeStatusCreatedActions
{backup.filename} + + {backup.type} + + {formatFileSize(backup.fileSize)} + + {backup.status} + + {formatDateTime(backup.createdAt)} +
+ {hasPermission('backups:restore') && backup.status === 'completed' && ( + + )} + {hasPermission('backups:delete') && ( + + )} +
+
+ )} +
+
+ ) +} diff --git a/src/app/(app)/servers/[id]/configuration/page.tsx b/src/app/(app)/servers/[id]/configuration/page.tsx new file mode 100644 index 0000000..00df5dd --- /dev/null +++ b/src/app/(app)/servers/[id]/configuration/page.tsx @@ -0,0 +1,161 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useParams } from 'next/navigation' +import { ArrowLeft, Settings, Save } from 'lucide-react' +import Link from 'next/link' +import PageHeader from '@/components/PageHeader' +import Button from '@/components/ui/Button' +import Input from '@/components/ui/Input' +import Spinner from '@/components/ui/Spinner' +import { useToast } from '@/contexts/ToastContext' + +interface ServerProperties { + [key: string]: string +} + +export default function ConfigurationPage() { + const params = useParams() + const serverId = params.id as string + const { showToast } = useToast() + + const [properties, setProperties] = useState({}) + const [jvmArgs, setJvmArgs] = useState('') + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + const fetchConfig = useCallback(async () => { + try { + const res = await fetch(`/api/servers/${serverId}/configuration`, { credentials: 'include' }) + if (res.ok) { + const data = await res.json() + setProperties(data.data?.properties || {}) + setJvmArgs((data.data?.jvmArgs || []).join(' ')) + } + } catch { + showToast('Failed to load configuration', 'error') + } finally { + setLoading(false) + } + }, [serverId, showToast]) + + useEffect(() => { + fetchConfig() + }, [fetchConfig]) + + const handlePropertyChange = (key: string, value: string) => { + setProperties(prev => ({ ...prev, [key]: value })) + } + + const handleSave = async () => { + setSaving(true) + try { + const res = await fetch(`/api/servers/${serverId}/configuration`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + properties, + jvmArgs: jvmArgs.split(/\s+/).filter(Boolean), + }), + }) + + if (res.ok) { + showToast('Configuration saved. Restart the server to apply changes.', 'success') + } else { + const data = await res.json() + showToast(data.error || 'Failed to save configuration', 'error') + } + } catch { + showToast('Failed to save configuration', 'error') + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+ +
+ ) + } + + const commonProps = [ + 'server-port', 'max-players', 'motd', 'difficulty', 'gamemode', + 'pvp', 'allow-nether', 'spawn-protection', 'view-distance', + 'online-mode', 'white-list', 'enable-command-block', 'level-seed', + 'level-name', 'level-type', 'spawn-npcs', 'spawn-animals', + 'spawn-monsters', 'generate-structures' + ] + + const sortedKeys = Object.keys(properties).sort((a, b) => { + const aCommon = commonProps.indexOf(a) + const bCommon = commonProps.indexOf(b) + if (aCommon !== -1 && bCommon !== -1) return aCommon - bCommon + if (aCommon !== -1) return -1 + if (bCommon !== -1) return 1 + return a.localeCompare(b) + }) + + return ( +
+ + Back to Server + + + + Save Changes + + } + /> + + {/* JVM Args */} +
+

JVM Arguments

+ setJvmArgs(e.target.value)} + placeholder="-XX:+UseG1GC -XX:MaxGCPauseMillis=200" + /> +

+ Space-separated JVM flags. Memory flags are set automatically from server settings. +

+
+ + {/* Server Properties */} +
+
+

server.properties

+
+
+ {sortedKeys.map(key => ( +
+ + handlePropertyChange(key, e.target.value)} + className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none" + /> +
+ ))} + {sortedKeys.length === 0 && ( +
+ No server.properties found. Start the server once to generate the default configuration. +
+ )} +
+
+
+ ) +} diff --git a/src/app/(app)/servers/[id]/console/page.tsx b/src/app/(app)/servers/[id]/console/page.tsx new file mode 100644 index 0000000..703a709 --- /dev/null +++ b/src/app/(app)/servers/[id]/console/page.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useParams } from 'next/navigation' +import { ArrowLeft, Terminal } from 'lucide-react' +import Link from 'next/link' +import PageHeader from '@/components/PageHeader' +import ConsoleViewer from '@/components/ConsoleViewer' + +export default function ConsolePage() { + const params = useParams() + const serverId = params.id as string + + return ( +
+ + Back to Server + + + + +
+ +
+
+ ) +} diff --git a/src/app/(app)/servers/[id]/files/page.tsx b/src/app/(app)/servers/[id]/files/page.tsx new file mode 100644 index 0000000..d9d91e7 --- /dev/null +++ b/src/app/(app)/servers/[id]/files/page.tsx @@ -0,0 +1,1076 @@ +'use client' + +import { useState, useEffect, useCallback, useRef } from 'react' +import { useParams } from 'next/navigation' +import { + ArrowLeft, FolderOpen, File, Folder, ChevronRight, RefreshCw, Home, Eye, + Pencil, Save, Trash2, Copy, Upload, Archive, ArchiveRestore, FolderPlus, X, + CheckSquare, Square, ListChecks, FolderInput, +} from 'lucide-react' +import Link from 'next/link' +import PageHeader from '@/components/PageHeader' +import Button from '@/components/ui/Button' +import Spinner from '@/components/ui/Spinner' +import { useToast } from '@/contexts/ToastContext' +import { useConfirmation } from '@/contexts/ConfirmationContext' +import { formatDateTime } from '@/lib/date-utils' + +interface FileEntry { + name: string + path: string + isDirectory: boolean + size: number | null + modifiedAt: string +} + +const ARCHIVE_EXTENSIONS = new Set(['.zip', '.tar', '.tar.gz', '.tgz']) + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +function isArchive(name: string): boolean { + const lower = name.toLowerCase() + return ARCHIVE_EXTENSIONS.has(lower.slice(lower.lastIndexOf('.'))) || lower.endsWith('.tar.gz') +} + +export default function FilesPage() { + const params = useParams() + const serverId = params.id as string + const { showToast } = useToast() + const { showConfirmation } = useConfirmation() + + const [currentPath, setCurrentPath] = useState('/') + const [entries, setEntries] = useState([]) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(null) + + // File viewer / editor + const [viewingFile, setViewingFile] = useState(null) + const [fileContent, setFileContent] = useState(null) + const [editContent, setEditContent] = useState(null) + const [fileMeta, setFileMeta] = useState<{ name: string; size: number; type: string; message?: string } | null>(null) + const [fileLoading, setFileLoading] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [saving, setSaving] = useState(false) + + // Copy modal + const [copyModal, setCopyModal] = useState<{ source: string; name: string } | null>(null) + const [copyDest, setCopyDest] = useState('') + + // Move modal + const [moveModal, setMoveModal] = useState<{ source: string; name: string } | null>(null) + const [moveDest, setMoveDest] = useState('') + + // New folder modal + const [showNewFolder, setShowNewFolder] = useState(false) + const [newFolderName, setNewFolderName] = useState('') + + // Context menu + const [contextMenu, setContextMenu] = useState<{ entry: FileEntry; x: number; y: number } | null>(null) + + // Upload + const fileInputRef = useRef(null) + const [uploading, setUploading] = useState(false) + + // Multi-select + const [selected, setSelected] = useState>(new Set()) + const [selectMode, setSelectMode] = useState(false) + + const toggleSelect = (path: string) => { + setSelected((prev) => { + const next = new Set(prev) + if (next.has(path)) next.delete(path) + else next.add(path) + return next + }) + } + + const toggleSelectAll = () => { + if (selected.size === entries.length) { + setSelected(new Set()) + } else { + setSelected(new Set(entries.map((e) => e.path))) + } + } + + const clearSelection = () => { + setSelected(new Set()) + setSelectMode(false) + } + + // ─── Data Fetching ──────────────────────────────────────────── + const fetchDirectory = useCallback(async (path: string) => { + setLoading(true) + try { + const res = await fetch( + `/api/servers/${serverId}/files?path=${encodeURIComponent(path)}`, + { credentials: 'include' } + ) + if (res.ok) { + const data = await res.json() + setEntries(data.data.items) + setCurrentPath(data.data.path === '/' ? '/' : data.data.path) + } else { + const data = await res.json() + showToast(data.error || 'Failed to load directory', 'error') + } + } catch { + showToast('Failed to load directory', 'error') + } finally { + setLoading(false) + } + }, [serverId, showToast]) + + useEffect(() => { + fetchDirectory('/') + }, [fetchDirectory]) + + // Close context menu on click outside + useEffect(() => { + const handler = () => setContextMenu(null) + if (contextMenu) { + document.addEventListener('click', handler) + return () => document.removeEventListener('click', handler) + } + }, [contextMenu]) + + // ─── Navigation ─────────────────────────────────────────────── + const navigateTo = (path: string) => { + setViewingFile(null) + setFileContent(null) + setFileMeta(null) + setIsEditing(false) + setEditContent(null) + clearSelection() + fetchDirectory(path) + } + + // ─── Open & View File ──────────────────────────────────────── + const openFile = async (path: string) => { + setFileLoading(true) + setViewingFile(path) + setFileContent(null) + setFileMeta(null) + setIsEditing(false) + setEditContent(null) + + try { + const res = await fetch( + `/api/servers/${serverId}/files?path=${encodeURIComponent(path)}&read=true`, + { credentials: 'include' } + ) + if (res.ok) { + const data = await res.json() + const d = data.data + setFileMeta({ name: d.name, size: d.size, type: d.type, message: d.message }) + if (d.type === 'text') { + setFileContent(d.content) + } + } else { + const data = await res.json() + showToast(data.error || 'Failed to read file', 'error') + setViewingFile(null) + } + } catch { + showToast('Failed to read file', 'error') + setViewingFile(null) + } finally { + setFileLoading(false) + } + } + + // ─── Save Edited File ──────────────────────────────────────── + const saveFile = async () => { + if (!viewingFile || editContent === null) return + setSaving(true) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ path: viewingFile, content: editContent }), + }) + if (res.ok) { + showToast('File saved', 'success') + setFileContent(editContent) + setIsEditing(false) + setEditContent(null) + } else { + const data = await res.json() + showToast(data.error || 'Failed to save file', 'error') + } + } catch { + showToast('Failed to save file', 'error') + } finally { + setSaving(false) + } + } + + // ─── Delete ─────────────────────────────────────────────────── + const handleDelete = async (entry: FileEntry) => { + const confirmed = await showConfirmation({ + title: `Delete ${entry.isDirectory ? 'folder' : 'file'}`, + message: `Are you sure you want to delete "${entry.name}"? ${entry.isDirectory ? 'All contents will be deleted.' : 'This cannot be undone.'}`, + itemName: entry.name, + type: 'danger', + confirmText: 'Delete', + }) + if (!confirmed) return + + setActionLoading(entry.path) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ path: entry.path }), + }) + if (res.ok) { + showToast('Deleted successfully', 'success') + fetchDirectory(currentPath) + } else { + const data = await res.json() + showToast(data.error || 'Delete failed', 'error') + } + } catch { + showToast('Delete failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Copy ───────────────────────────────────────────────────── + const handleCopy = async () => { + if (!copyModal || !copyDest.trim()) return + setActionLoading(copyModal.source) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'copy', source: copyModal.source, destination: copyDest.trim() }), + }) + if (res.ok) { + showToast('Copied successfully', 'success') + setCopyModal(null) + setCopyDest('') + fetchDirectory(currentPath) + } else { + const data = await res.json() + showToast(data.error || 'Copy failed', 'error') + } + } catch { + showToast('Copy failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Move ───────────────────────────────────────────────────── + const handleMove = async () => { + if (!moveModal || !moveDest.trim()) return + setActionLoading(moveModal.source) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'move', source: moveModal.source, destination: moveDest.trim() }), + }) + if (res.ok) { + showToast('Moved successfully', 'success') + setMoveModal(null) + setMoveDest('') + fetchDirectory(currentPath) + } else { + const data = await res.json() + showToast(data.error || 'Move failed', 'error') + } + } catch { + showToast('Move failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Upload ─────────────────────────────────────────────────── + const handleUpload = async (files: FileList) => { + if (!files.length) return + setUploading(true) + try { + const formData = new FormData() + formData.append('path', currentPath === '/' ? '/' : currentPath) + Array.from(files).forEach((f) => formData.append('files', f)) + + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + credentials: 'include', + body: formData, + }) + if (res.ok) { + const data = await res.json() + showToast(`Uploaded ${data.data.length} file(s)`, 'success') + fetchDirectory(currentPath) + } else { + const data = await res.json() + showToast(data.error || 'Upload failed', 'error') + } + } catch { + showToast('Upload failed', 'error') + } finally { + setUploading(false) + if (fileInputRef.current) fileInputRef.current.value = '' + } + } + + // ─── Zip ────────────────────────────────────────────────────── + const handleZip = async (entry: FileEntry) => { + setActionLoading(entry.path) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'zip', path: entry.path }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Zipped', 'success') + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Zip failed', 'error') + } + } catch { + showToast('Zip failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Unzip ──────────────────────────────────────────────────── + const handleUnzip = async (entry: FileEntry) => { + setActionLoading(entry.path) + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'unzip', path: entry.path }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Extracted', 'success') + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Unzip failed', 'error') + } + } catch { + showToast('Unzip failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Create Folder ──────────────────────────────────────────── + const handleCreateFolder = async () => { + if (!newFolderName.trim()) return + const folderPath = currentPath === '/' ? newFolderName.trim() : `${currentPath}/${newFolderName.trim()}` + setActionLoading('new-folder') + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'create-folder', path: folderPath }), + }) + if (res.ok) { + showToast('Folder created', 'success') + setShowNewFolder(false) + setNewFolderName('') + fetchDirectory(currentPath) + } else { + const data = await res.json() + showToast(data.error || 'Failed to create folder', 'error') + } + } catch { + showToast('Failed to create folder', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Bulk Delete ────────────────────────────────────────────── + const handleBulkDelete = async () => { + const paths = Array.from(selected) + const confirmed = await showConfirmation({ + title: 'Delete selected items', + message: `Are you sure you want to delete ${paths.length} item(s)? This cannot be undone.`, + itemName: `${paths.length} items`, + type: 'danger', + confirmText: 'Delete All', + }) + if (!confirmed) return + + setActionLoading('bulk') + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'delete-multiple', paths }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Deleted', 'success') + clearSelection() + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Bulk delete failed', 'error') + } + } catch { + showToast('Bulk delete failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Bulk Zip ───────────────────────────────────────────────── + const handleBulkZip = async () => { + const paths = Array.from(selected) + setActionLoading('bulk') + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'zip-multiple', paths }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Compressed', 'success') + clearSelection() + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Bulk zip failed', 'error') + } + } catch { + showToast('Bulk zip failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Bulk Copy ──────────────────────────────────────────────── + const [bulkCopyModal, setBulkCopyModal] = useState(false) + const [bulkCopyDest, setBulkCopyDest] = useState('') + + const handleBulkCopy = async () => { + if (!bulkCopyDest.trim()) return + const paths = Array.from(selected) + setActionLoading('bulk') + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'copy-multiple', paths, destination: bulkCopyDest.trim() }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Copied', 'success') + setBulkCopyModal(false) + setBulkCopyDest('') + clearSelection() + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Bulk copy failed', 'error') + } + } catch { + showToast('Bulk copy failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Bulk Move ──────────────────────────────────────────────── + const [bulkMoveModal, setBulkMoveModal] = useState(false) + const [bulkMoveDest, setBulkMoveDest] = useState('') + + const handleBulkMove = async () => { + if (!bulkMoveDest.trim()) return + const paths = Array.from(selected) + setActionLoading('bulk') + try { + const res = await fetch(`/api/servers/${serverId}/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'move-multiple', paths, destination: bulkMoveDest.trim() }), + }) + const data = await res.json() + if (res.ok) { + showToast(data.message || 'Moved', 'success') + setBulkMoveModal(false) + setBulkMoveDest('') + clearSelection() + fetchDirectory(currentPath) + } else { + showToast(data.error || 'Bulk move failed', 'error') + } + } catch { + showToast('Bulk move failed', 'error') + } finally { + setActionLoading(null) + } + } + + // ─── Breadcrumbs ────────────────────────────────────────────── + const breadcrumbs = currentPath === '/' + ? [{ label: 'root', path: '/' }] + : [ + { label: 'root', path: '/' }, + ...currentPath.split('/').filter(Boolean).map((segment, i, arr) => ({ + label: segment, + path: arr.slice(0, i + 1).join('/'), + })), + ] + + return ( +
+ + Back to Server + + + + {fileMeta?.type === 'text' && !isEditing && ( + + )} + {isEditing && ( + <> + + + + )} + +
+ ) : ( +
+ + + + +
+ ) + } + /> + + {/* Hidden file input for upload */} + e.target.files && handleUpload(e.target.files)} + /> + + {/* Breadcrumbs */} + {!viewingFile && ( + + )} + + {/* New Folder Inline Form */} + {showNewFolder && !viewingFile && ( +
+ + setNewFolderName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()} + placeholder="Folder name…" + autoFocus + className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500" + /> + + +
+ )} + + {viewingFile ? ( + /* ─── File Viewer / Editor ─────────────────────────────── */ +
+
+
+ + {fileMeta?.name || viewingFile} + {isEditing && ( + Editing + )} +
+ {fileMeta && ( + {formatFileSize(fileMeta.size)} + )} +
+
+ {fileLoading ? ( +
+ +
+ ) : fileMeta?.type === 'binary' ? ( +
+ +

{fileMeta.message}

+

{formatFileSize(fileMeta.size)}

+
+ ) : isEditing ? ( +