diff --git a/.opencode/plans/user-view-overhaul.md b/.opencode/plans/user-view-overhaul.md new file mode 100644 index 000000000..95f41adc7 --- /dev/null +++ b/.opencode/plans/user-view-overhaul.md @@ -0,0 +1,107 @@ +# Plan: Full Overhaul of /admin/users/view/:id + +## Goal +Completely redesign the user view page to match the modern admin design patterns (icon cards, read-only info tiles, enhanced danger zone) used in the Nodes, Buckets, and Database Hosts view pages. + +## Scope + +### Frontend Only (no backend changes needed) +The Application API already returns all needed data. The `?include=servers` query parameter can fetch a user's servers through the existing `UserTransformer`. No new backend endpoints are required. + +--- + +## File Changes + +### 1. `resources/scripts/api/admin/users.ts` — Add servers include support + +- Extend the `AdminUser` interface with an optional `servers` array: + ```ts + export interface AdminServerSummary { + id: number; + name: string; + uuid: string; + nodeId: number; + // any other summary fields from ServerTransformer + } + ``` +- Update `rawToUser` to parse `relationships?.servers?.data` if present +- Update `getUser` to accept an optional `include` parameter (e.g., `?include=servers`) + +### 2. `resources/scripts/components/admin/users/AdminUsersContainer.tsx` — Rewrite AdminUserView + +Replace the current `AdminUserView` component entirely. The new design follows the Nodes view pattern: + +#### Layout Structure + +``` +MainPageHeader (title=username, headChildren=Back link, children=Save+Delete buttons) +Tab bar (Details | Servers | Manage) +Tab content +``` + +#### Tab 1: "Details" (default) + +**User Profile Card** (enhanced icon card): +- Left side: Gravatar avatar (via `https://www.gravatar.com/avatar/{md5(email)}?d=identicon&s=96`) + - Fallback: Initials circle if gravatar is disabled +- Right side: Username (large), email, badges (Admin if `rootAdmin`, 2FA Enabled/Disabled) + +**Account Info Card** (read-only info tiles, edit-toggle pattern from Node Overview): +- Toggle between read-only view and edit form via an "Edit" button +- **Read-only mode**: Grid of `bg-mocha-600/50 rounded-lg p-4` tiles: + - User ID, UUID, External ID, Language, Created, Updated +- **Edit mode**: Form with inputs for: + - First Name, Last Name, Email, Language + - Root Admin checkbox +- Save / Cancel buttons + +#### Tab 2: "Servers" + +- Fetch user with `?include=servers` via SWR +- Table of user's servers with columns: Name, Node, Status, Created + - Each name links to `/admin/servers/view/{id}` +- Empty state: "This user has no servers." + +#### Tab 3: "Manage" + +**Enhanced Danger Zone Card** (matching Node Settings pattern): +- Icon header with warning triangle +- `border-2 border-red-800/50 rounded-xl` +- Description text + Delete User button +- Delete confirmation via `Dialog.Confirm` +- On success: navigate to `..` (user list) + +#### Delete/Save via MainPageHeader + +- Save button in `MainPageHeader` children — only visible on the Details tab when editing +- Delete button in `MainPageHeader` children — opens a confirm dialog (same as Manage tab delete) + +--- + +## Design Tokens Used (from Node view pattern) + +| Element | Classes | +|---|---| +| Enhanced card | `bg-mocha-500 border border-mocha-400 rounded-xl p-6` | +| Icon container | `w-10 h-10 bg-mocha-400 rounded-lg flex items-center justify-center` | +| Info tile | `bg-mocha-600/50 rounded-lg p-4` | +| Tile label | `text-mocha-200 text-xs uppercase tracking-wider` | +| Tile value | `text-cream-400 font-medium mt-1` | +| Read-only card | `bg-mocha-500/50 border border-mocha-400/50 rounded-xl p-6` | +| Danger zone | `bg-mocha-500 border-2 border-red-800/50 rounded-xl p-6` | +| Tab active | `text-cream-400 border-cream-400` (matching Node view) | +| Tab inactive | `text-mocha-200 hover:text-mocha-100 border-transparent` | +| Form input | existing `inputClass` constant | +| Status badge | `bg-green-900/50 text-green-400 border border-green-700/50` (enhanced) | +| Admin badge | `bg-red-900/50 text-red-400` (enhanced with border) | + +--- + +## Verification +1. Run `pnpm run ship` to build +2. Navigate to `http://localhost:3000/admin/users/view/1` +3. Verify: Details tab shows profile card with avatar, info tiles, edit toggle +4. Verify: Servers tab shows user's servers (or empty state) +5. Verify: Manage tab shows danger zone with delete +6. Verify: Save, edit toggle, and delete all work without console errors +7. Verify: No React hooks errors (error #310) diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..d7f4d5af6 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,549 @@ +# Hydrodactyl Admin Dashboard Design + +## Overview + +This document outlines the design system, architecture, and UI/UX patterns used in the Hydrodactyl admin dashboard and user-facing pages. The design focuses on a modern, dark-themed interface with consistent patterns across all admin and user views. + +--- + +## Design System + +### Color Palette + +The admin dashboard uses a custom "mocha" color scheme designed for reduced eye strain and improved readability: + +#### Background Colors +- **`mocha-500`** (`#3a3a3a`) - Primary card/panel backgrounds +- **`mocha-600`** (`#2d2d2d`) - Input fields and elevated surfaces +- **`mocha-400`** (`#4a4a4a`) - Borders and dividers +- **`mocha-300`** (`#5a5a5a`) - Hover states and focus rings + +#### Text Colors +- **`cream-400`** (`#f5f5f5`) - Primary text, headings, and important content +- **`cream-500`** (`#e8e8e8`) - Secondary text and hover states +- **`mocha-200`** (`#b0b0b0`) - Labels and muted text +- **`mocha-100`** (`#c0c0c0`) - Tertiary text + +#### Semantic Colors +- **`brand`** - Primary accent color for active states and CTAs +- **`red-400`** / **`red-600`** - Error states and danger zones +- **`red-900/50`** - Danger zone backgrounds +- **`green-400`** / **`green-900/50`** - Success states +- **`yellow-400`** / **`yellow-600`** - Warning states +- **`yellow-900/50`** - Suspended/warning backgrounds + +### Typography + +- **Font Family**: System fonts (default sans-serif stack) +- **Base Size**: 14px (0.875rem) +- **Line Height**: 1.5 (default) +- **Font Weights**: + - `font-medium` (500) - Buttons, tabs, and interactive elements + - `font-semibold` (600) - Headings and emphasis + +### Spacing + +- **Base Unit**: 4px +- **Common Spacing**: 4, 8, 12, 16, 24, 32, 48, 64px +- **Component Padding**: 16px (p-4), 24px (p-6) +- **Grid Gaps**: 16px (gap-4), 24px (gap-6) + +### Border Radius + +- **Small**: `rounded` (4px) - Inputs, small elements +- **Medium**: `rounded-lg` (8px) - Cards and panels +- **Large**: `rounded-xl` (12px) - Buttons and modals + +--- + +## Component Patterns + +### 1. Page Layout + +All admin pages follow a consistent layout structure: + +``` +┌─────────────────────────────────────────┐ +│ MainPageHeader (Title + Actions) │ +├─────────────────────────────────────────┤ +│ │ +│ Content Area (cards, tables, forms) │ +│ │ +└─────────────────────────────────────────┘ +``` + +#### MainPageHeader +- **Purpose**: Page title with optional action buttons +- **Props**: `title` (string), optional children (action buttons) +- **Usage**: + ```tsx + + + + ``` + +### 2. Card/Panel Pattern + +Cards are used to group related content: + +```tsx +
+

Section Title

+ {/* Content */} +
+``` + +**Variants**: +- **Default**: `bg-mocha-500 border border-mocha-400` +- **Danger Zone**: `bg-mocha-500 border border-red-900/50` +- **Success**: `bg-green-900/20 border border-green-800` + +### 3. Tab Navigation + +Tabs are used for multi-section pages (e.g., server details, user view): + +```tsx +const tabs = ['details', 'build', 'startup', 'databases', 'manage'] as const; +const [activeTab, setActiveTab] = useState<'details' | 'build' | ...>('details'); + +
+ {tabs.map((tab) => ( + + ))} +
+``` + +**Key Features**: +- Horizontal scrolling for overflow +- Active state with brand color underline +- Hover states for better UX +- Responsive padding + +### 4. Form Elements + +#### Input Fields +```tsx +const inputClass = 'w-full bg-mocha-600 border border-mocha-400 rounded px-3 py-2 text-sm text-cream-400 focus:outline-none focus:border-mocha-300 transition-colors'; + + setValue(e.target.value)} + className={inputClass} + placeholder='Enter value...' +/> +``` + +**States**: +- **Default**: `border-mocha-400` +- **Focus**: `focus:border-mocha-300` +- **Disabled**: `disabled:opacity-50 disabled:cursor-not-allowed` + +#### Labels +```tsx +const labelClass = 'block text-sm text-mocha-200 mb-1'; + + +``` + +#### Select Dropdowns +```tsx + +``` + +### 5. Buttons + +#### Primary Button +```tsx + +``` + +#### Danger Button +```tsx + +``` + +#### Warning Button +```tsx + +``` + +### 6. Tables + +Tables display list data with consistent styling: + +```tsx +
+ + + + + + + + {items.map((item) => ( + + + + ))} + +
+ Column Name +
{item.name}
+
+``` + +**Features**: +- Hover states on rows +- Border separators +- Responsive text sizing +- Overflow handling with container + +### 7. Status Badges + +Status indicators use colored badges: + +```tsx +{status ? ( + + Active + +) : ( + + Suspended + +)} +``` + +### 8. Information Display + +#### Info Cards (Read-only Data) +```tsx +
+
+ Label +

{value}

+
+
+``` + +#### Success/Error Messages +```tsx +{error &&
Error: {error}
} +{success &&
Operation successful.
} +``` + +### 9. Modals and Dialogs + +#### Confirmation Dialog +```tsx + setIsOpen(false)} + onConfirmed={handleConfirm} + title='Confirm Action' + confirm='Confirm' +> + Are you sure you want to proceed? + +``` + +#### Form Modal +```tsx + +
+ {/* Form fields */} +
+ +
+ + +
+
+
+``` + +### 10. Toast Notifications + +Sonner toasts provide feedback for async operations: + +```tsx +import { toast } from 'sonner'; + +// Success +toast.success('Operation completed successfully'); + +// Error +toast.error('An error occurred: ' + errorMessage); +``` + +**Usage**: +- Success toasts after CRUD operations +- Error toasts for API failures +- Auto-dismiss after 3-5 seconds + +--- + +## Page-Specific Patterns + +### Admin Servers Page + +#### Server List +- Paginated table with server information +- Columns: Name, Owner, Node, Egg, Memory, Disk, Status, Actions +- Inline actions: View, Suspend/Unsuspend, Delete +- Status badges for suspended/running states + +#### Create Server Modal +- Multi-step form with conditional fields +- Fields appear based on previous selections (Nest → Egg → Node → Details) +- User dropdown (first 10 users with pagination notice) +- Domain selection dropdown +- Resource inputs with 0 = unlimited semantics +- Environment variables with add/remove functionality +- Feature limits (databases, allocations, backups) + +#### Server View (Tabs) +- **Details Tab**: Server information + edit form +- **Build Tab**: Resource configuration (memory, disk, CPU, etc.) +- **Startup Tab**: Startup command, docker image, environment variables +- **Databases Tab**: Database list + create form +- **Manage Tab**: Actions (reinstall, suspend, delete) + +### Admin Users Page + +#### User List +- Paginated table with user information +- Columns: ID, Username, Email, Name, Admin, 2FA, Actions +- View and Delete actions + +#### User View (Tabs) +- **Details Tab**: User information display + edit form + - Read-only fields: User ID, Username, Email, 2FA status + - Editable fields: First name, last name, email, language, root admin + - Server count display +- **Manage Tab**: Danger zone with delete action + +--- + +## State Management + +### React State (useState) +- Local component state for forms and UI +- Example: `const [name, setName] = useState('')` + +### SWR (Data Fetching) +- Server state management with SWR +- Pattern: `useSWR(key, fetcherFunction)` +- Example: + ```tsx + const { data, error, mutate } = useSWR( + ['admin:servers', page], + () => getServers({ page }) + ); + ``` + +### Form State +- Simple forms use individual useState hooks +- Complex forms use a single state object: + ```tsx + const [form, setForm] = useState({ + name_first: '', + name_last: '', + email: '', + }); + ``` + +--- + +## API Integration + +### HTTP Client +- Axios-based HTTP client with base URL configuration +- Error handling via `httpErrorToHuman()` utility + +### API Functions +- Located in `resources/scripts/api/admin/` +- Return promises with typed responses +- Example: + ```tsx + export const getUsers = (params: { page: number }): Promise> => + http.get('/api/application/users', { params }).then(({ data }) => data); + ``` + +--- + +## Responsive Design + +### Breakpoints +- **Mobile**: Default (< 768px) +- **Tablet**: `md:` (768px+) +- **Desktop**: `lg:` (1024px+) + +### Grid Systems +- **2 columns**: `grid-cols-2` +- **3 columns**: `grid-cols-3` +- **Responsive**: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` + +### Table Handling +- Horizontal scroll for overflow +- Responsive text sizing +- Hidden columns on mobile (future enhancement) + +--- + +## Accessibility + +### Semantic HTML +- Proper heading hierarchy (h1, h2, h3) +- Form labels associated with inputs +- Button elements for actions (not divs) + +### Keyboard Navigation +- All interactive elements are focusable +- Tab order follows visual layout +- Enter/Space activates buttons + +### Color Contrast +- Text meets WCAG AA standards +- Important information not conveyed by color alone +- Focus indicators visible + +--- + +## Performance + +### Million.js Optimization +- Automatic component optimization via Million.js +- Compiler-time React optimization +- No manual memoization needed for most components + +### Code Splitting +- Route-based code splitting via React Router +- Dynamic imports for large components (future) + +### Build Optimization +- Vite for fast development and optimized production builds +- Tree shaking for unused code elimination +- Gzip compression for assets + +--- + +## Future Improvements + +### Planned Enhancements +1. **Search and Filtering**: Add search bars to list pages +2. **Bulk Actions**: Select multiple items for batch operations +3. **Advanced Tables**: Sorting, filtering, column visibility +4. **Dark/Light Mode**: Theme toggle (currently dark-only) +5. **Keyboard Shortcuts**: Power user shortcuts +6. **Real-time Updates**: WebSocket integration for live data +7. **Export Functionality**: CSV/PDF export for tables +8. **Audit Logging**: Track admin actions + +### Technical Debt +- Migrate remaining inline styles to Tailwind classes +- Add comprehensive error boundaries +- Implement proper form validation library +- Add unit and integration tests +- Document API endpoints with OpenAPI + +--- + +## File Structure + +``` +resources/scripts/ +├── components/ +│ ├── admin/ +│ │ ├── servers/ +│ │ │ └── AdminServersContainer.tsx +│ │ ├── users/ +│ │ │ └── AdminUsersContainer.tsx +│ │ ├── nests/ +│ │ ├── nodes/ +│ │ ├── databases/ +│ │ └── AdminDashboardContainer.tsx +│ ├── elements/ +│ │ ├── Dialog.tsx +│ │ ├── MainPageHeader.tsx +│ │ ├── Pagination.tsx +│ │ └── Spinner.tsx +│ └── layout/ +│ └── Sidebar.tsx +├── api/ +│ └── admin/ +│ ├── servers.ts +│ ├── users.ts +│ ├── nodes.ts +│ └── settings.ts +└── routers/ + └── AdminRouter.tsx +``` + +--- + +## Contributing + +When adding new pages or components: + +1. **Follow the established patterns** in this document +2. **Use the mocha color scheme** for consistency +3. **Implement tab navigation** for multi-section pages +4. **Add proper TypeScript types** for all data structures +5. **Use SWR for data fetching** with proper cache keys +6. **Handle loading and error states** appropriately +7. **Test on mobile and desktop** viewports +8. **Rebuild frontend** after changes: `pnpm build` + +--- + +## Contact + +For questions or clarifications about the design system, please refer to the codebase or contact the development team. \ No newline at end of file diff --git a/app/Http/Controllers/Admin/ApiController.php b/app/Http/Controllers/Admin/ApiController.php index 02ad6e540..8449fa180 100644 --- a/app/Http/Controllers/Admin/ApiController.php +++ b/app/Http/Controllers/Admin/ApiController.php @@ -72,7 +72,7 @@ public function store(StoreApplicationApiKeyRequest $request): RedirectResponse $this->alert->success('A new application API key has been generated for your account.')->flash(); - return redirect()->route('admin.api.index'); + return redirect()->route('admin.depr.api.index'); } /** diff --git a/app/Http/Controllers/Admin/Buckets/BucketViewController.php b/app/Http/Controllers/Admin/Buckets/BucketViewController.php index f656ad7e9..b4355b5cc 100644 --- a/app/Http/Controllers/Admin/Buckets/BucketViewController.php +++ b/app/Http/Controllers/Admin/Buckets/BucketViewController.php @@ -54,7 +54,7 @@ public function update(Request $request, S3 $s3): RedirectResponse $s3->update($validated); - return redirect()->route('admin.buckets.view.details', $s3->id)->with('success', 'S3 configuration updated.'); + return redirect()->route('admin.depr.buckets.view.details', $s3->id)->with('success', 'S3 configuration updated.'); } public function servers(Request $request, S3 $s3): View diff --git a/app/Http/Controllers/Admin/Buckets/CreateBucketController.php b/app/Http/Controllers/Admin/Buckets/CreateBucketController.php index 31f513581..32693d3d4 100644 --- a/app/Http/Controllers/Admin/Buckets/CreateBucketController.php +++ b/app/Http/Controllers/Admin/Buckets/CreateBucketController.php @@ -29,6 +29,6 @@ public function store(BucketFormRequest $request): RedirectResponse $this->alert->success('S3 configuration created')->flash(); - return redirect()->route('admin.buckets.view', $s3->id); + return redirect()->route('admin.depr.buckets.view', $s3->id); } } diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 67ed1bd68..b74f24371 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -79,7 +79,7 @@ public function create(DatabaseHostFormRequest $request): RedirectResponse sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage()) )->flash(); - return redirect()->route('admin.databases')->withInput($request->validated()); + return redirect()->route('admin.depr.databases')->withInput($request->validated()); } else { throw $exception; } @@ -87,7 +87,7 @@ public function create(DatabaseHostFormRequest $request): RedirectResponse $this->alert->success('Successfully created a new database host on the system.')->flash(); - return redirect()->route('admin.databases.view', $host->id); + return redirect()->route('admin.depr.databases.view', $host->id); } /** @@ -97,7 +97,7 @@ public function create(DatabaseHostFormRequest $request): RedirectResponse */ public function update(DatabaseHostFormRequest $request, DatabaseHost $host): RedirectResponse { - $redirect = redirect()->route('admin.databases.view', $host->id); + $redirect = redirect()->route('admin.depr.databases.view', $host->id); try { $this->updateService->handle($host->id, $request->normalize()); @@ -129,7 +129,7 @@ public function delete(int $host): RedirectResponse $this->deletionService->handle($host); $this->alert->success('The requested database host has been deleted from the system.')->flash(); - return redirect()->route('admin.databases'); + return redirect()->route('admin.depr.databases'); } /** diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 829f52daf..551df16a1 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -62,7 +62,7 @@ public function create(LocationFormRequest $request): RedirectResponse $location = $this->creationService->handle($request->normalize()); $this->alert->success('Location was created successfully.')->flash(); - return redirect()->route('admin.locations.view', $location->id); + return redirect()->route('admin.depr.locations.view', $location->id); } /** @@ -79,7 +79,7 @@ public function update(LocationFormRequest $request, Location $location): Redire $this->updateService->handle($location->id, $request->normalize()); $this->alert->success('Location was updated successfully.')->flash(); - return redirect()->route('admin.locations.view', $location->id); + return redirect()->route('admin.depr.locations.view', $location->id); } /** @@ -93,11 +93,11 @@ public function delete(Location $location): RedirectResponse try { $this->deletionService->handle($location->id); - return redirect()->route('admin.locations'); + return redirect()->route('admin.depr.locations'); } catch (DisplayException $ex) { $this->alert->danger($ex->getMessage())->flash(); } - return redirect()->route('admin.locations.view', $location->id); + return redirect()->route('admin.depr.locations.view', $location->id); } } diff --git a/app/Http/Controllers/Admin/MountController.php b/app/Http/Controllers/Admin/MountController.php index a0a93c954..6b571a311 100644 --- a/app/Http/Controllers/Admin/MountController.php +++ b/app/Http/Controllers/Admin/MountController.php @@ -74,7 +74,7 @@ public function create(MountFormRequest $request): RedirectResponse $this->alert->success('Mount was created successfully.')->flash(); - return redirect()->route('admin.mounts.view', $mount->id); + return redirect()->route('admin.depr.mounts.view', $mount->id); } /** @@ -92,7 +92,7 @@ public function update(MountFormRequest $request, Mount $mount): RedirectRespons $this->alert->success('Mount was updated successfully.')->flash(); - return redirect()->route('admin.mounts.view', $mount->id); + return redirect()->route('admin.depr.mounts.view', $mount->id); } /** @@ -104,7 +104,7 @@ public function delete(Mount $mount): RedirectResponse { $mount->delete(); - return redirect()->route('admin.mounts'); + return redirect()->route('admin.depr.mounts'); } /** @@ -123,7 +123,7 @@ public function addEggs(Request $request, Mount $mount): RedirectResponse $this->alert->success('Mount was updated successfully.')->flash(); - return redirect()->route('admin.mounts.view', $mount->id); + return redirect()->route('admin.depr.mounts.view', $mount->id); } /** @@ -140,7 +140,7 @@ public function addNodes(Request $request, Mount $mount): RedirectResponse $this->alert->success('Mount was updated successfully.')->flash(); - return redirect()->route('admin.mounts.view', $mount->id); + return redirect()->route('admin.depr.mounts.view', $mount->id); } /** diff --git a/app/Http/Controllers/Admin/Nests/EggController.php b/app/Http/Controllers/Admin/Nests/EggController.php index 6698d24c9..fab038e0f 100644 --- a/app/Http/Controllers/Admin/Nests/EggController.php +++ b/app/Http/Controllers/Admin/Nests/EggController.php @@ -58,7 +58,7 @@ public function store(EggFormRequest $request): RedirectResponse $egg = $this->creationService->handle($data); $this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash(); - return redirect()->route('admin.nests.egg.view', $egg->id); + return redirect()->route('admin.depr.nests.egg.view', $egg->id); } /** @@ -91,7 +91,7 @@ public function update(EggFormRequest $request, Egg $egg): RedirectResponse $this->updateService->handle($egg, $data); $this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash(); - return redirect()->route('admin.nests.egg.view', $egg->id); + return redirect()->route('admin.depr.nests.egg.view', $egg->id); } /** @@ -105,7 +105,7 @@ public function destroy(Egg $egg): RedirectResponse $this->deletionService->handle($egg->id); $this->alert->success(trans('admin/nests.eggs.notices.deleted'))->flash(); - return redirect()->route('admin.nests.view', $egg->nest_id); + return redirect()->route('admin.depr.nests.view', $egg->nest_id); } /** diff --git a/app/Http/Controllers/Admin/Nests/EggScriptController.php b/app/Http/Controllers/Admin/Nests/EggScriptController.php index 155cf0121..91d347e24 100644 --- a/app/Http/Controllers/Admin/Nests/EggScriptController.php +++ b/app/Http/Controllers/Admin/Nests/EggScriptController.php @@ -60,6 +60,6 @@ public function update(EggScriptFormRequest $request, Egg $egg): RedirectRespons $this->installScriptService->handle($egg, $request->normalize()); $this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash(); - return redirect()->route('admin.nests.egg.scripts', $egg); + return redirect()->route('admin.depr.nests.egg.scripts', $egg); } } diff --git a/app/Http/Controllers/Admin/Nests/EggShareController.php b/app/Http/Controllers/Admin/Nests/EggShareController.php index 822f1534b..9a8c9693b 100644 --- a/app/Http/Controllers/Admin/Nests/EggShareController.php +++ b/app/Http/Controllers/Admin/Nests/EggShareController.php @@ -55,7 +55,7 @@ public function import(EggImportFormRequest $request): RedirectResponse $egg = $this->importerService->handle($request->file('import_file'), $request->input('import_to_nest')); $this->alert->success(trans('admin/nests.eggs.notices.imported'))->flash(); - return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]); + return redirect()->route('admin.depr.nests.egg.view', ['egg' => $egg->id]); } /** @@ -93,7 +93,7 @@ public function importFromUrl(EggImportUrlFormRequest $request): RedirectRespons $egg = $this->importerService->handleFromString($response, $request->input('import_to_nest')); $this->alert->success(trans('admin/nests.eggs.notices.imported'))->flash(); - return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]); + return redirect()->route('admin.depr.nests.egg.view', ['egg' => $egg->id]); } catch (\Throwable $e) { $this->alert->danger($e->getMessage()); return redirect()->back(); @@ -113,6 +113,6 @@ public function update(EggImportFormRequest $request, Egg $egg): RedirectRespons $this->updateImporterService->handle($egg, $request->file('import_file')); $this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash(); - return redirect()->route('admin.nests.egg.view', ['egg' => $egg]); + return redirect()->route('admin.depr.nests.egg.view', ['egg' => $egg]); } } diff --git a/app/Http/Controllers/Admin/Nests/EggVariableController.php b/app/Http/Controllers/Admin/Nests/EggVariableController.php index 520866488..241150eca 100644 --- a/app/Http/Controllers/Admin/Nests/EggVariableController.php +++ b/app/Http/Controllers/Admin/Nests/EggVariableController.php @@ -54,7 +54,7 @@ public function store(EggVariableFormRequest $request, Egg $egg): RedirectRespon $this->creationService->handle($egg->id, $request->normalize()); $this->alert->success(trans('admin/nests.variables.notices.variable_created'))->flash(); - return redirect()->route('admin.nests.egg.variables', $egg->id); + return redirect()->route('admin.depr.nests.egg.variables', $egg->id); } /** @@ -72,7 +72,7 @@ public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $v 'variable' => htmlspecialchars($variable->name), ]))->flash(); - return redirect()->route('admin.nests.egg.variables', $egg->id); + return redirect()->route('admin.depr.nests.egg.variables', $egg->id); } /** @@ -85,6 +85,6 @@ public function destroy(int $egg, EggVariable $variable): RedirectResponse 'variable' => htmlspecialchars($variable->name), ]))->flash(); - return redirect()->route('admin.nests.egg.variables', $egg); + return redirect()->route('admin.depr.nests.egg.variables', $egg); } } diff --git a/app/Http/Controllers/Admin/Nests/NestController.php b/app/Http/Controllers/Admin/Nests/NestController.php index 09220670e..3c5f6b844 100644 --- a/app/Http/Controllers/Admin/Nests/NestController.php +++ b/app/Http/Controllers/Admin/Nests/NestController.php @@ -58,7 +58,7 @@ public function store(StoreNestFormRequest $request): RedirectResponse $nest = $this->nestCreationService->handle($request->normalize()); $this->alert->success(trans('admin/nests.notices.created', ['name' => htmlspecialchars($nest->name)]))->flash(); - return redirect()->route('admin.nests.view', $nest->id); + return redirect()->route('admin.depr.nests.view', $nest->id); } /** @@ -84,7 +84,7 @@ public function update(StoreNestFormRequest $request, int $nest): RedirectRespon $this->nestUpdateService->handle($nest, $request->normalize()); $this->alert->success(trans('admin/nests.notices.updated'))->flash(); - return redirect()->route('admin.nests.view', $nest); + return redirect()->route('admin.depr.nests.view', $nest); } /** @@ -97,6 +97,6 @@ public function destroy(int $nest): RedirectResponse $this->nestDeletionService->handle($nest); $this->alert->success(trans('admin/nests.notices.deleted'))->flash(); - return redirect()->route('admin.nests'); + return redirect()->route('admin.depr.nests'); } } diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 0d021ac6d..1896b6ffe 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -59,7 +59,7 @@ public function create(): View|RedirectResponse if (count($locations) < 1) { $this->alert->warning(trans('admin/node.notices.location_required'))->flash(); - return redirect()->route('admin.locations'); + return redirect()->route('admin.depr.locations'); } return $this->view->make('admin.nodes.new', [ @@ -80,7 +80,7 @@ public function store(NodeFormRequest $request): RedirectResponse $node = $this->creationService->handle($request->normalize()); $this->alert->info(trans('admin/node.notices.node_created'))->flash(); - return redirect()->route('admin.nodes.view.allocation', $node->id); + return redirect()->route('admin.depr.nodes.view.allocation', $node->id); } /** @@ -95,7 +95,7 @@ public function updateSettings(NodeFormRequest $request, Node $node): RedirectRe $this->updateService->handle($node, $request->normalize(), $request->input('reset_secret') === 'on'); $this->alert->success(trans('admin/node.notices.node_updated'))->flash(); - return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); + return redirect()->route('admin.depr.nodes.view.settings', $node->id)->withInput(); } /** @@ -141,7 +141,7 @@ public function allocationRemoveBlock(Request $request, int $node): RedirectResp $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => htmlspecialchars($request->input('ip'))])) ->flash(); - return redirect()->route('admin.nodes.view.allocation', $node); + return redirect()->route('admin.depr.nodes.view.allocation', $node); } /** @@ -172,7 +172,7 @@ public function createAllocation(AllocationFormRequest $request, Node $node): Re $this->assignmentService->handle($node, $request->normalize()); $this->alert->success(trans('admin/node.notices.allocations_added'))->flash(); - return redirect()->route('admin.nodes.view.allocation', $node->id); + return redirect()->route('admin.depr.nodes.view.allocation', $node->id); } /** @@ -185,6 +185,6 @@ public function delete(int|Node $node): RedirectResponse $this->deletionService->handle($node); $this->alert->success(trans('admin/node.notices.node_deleted'))->flash(); - return redirect()->route('admin.nodes'); + return redirect()->route('admin.depr.nodes'); } } diff --git a/app/Http/Controllers/Admin/S3Controller.php b/app/Http/Controllers/Admin/S3Controller.php index 639b17352..7da23bbd6 100644 --- a/app/Http/Controllers/Admin/S3Controller.php +++ b/app/Http/Controllers/Admin/S3Controller.php @@ -45,6 +45,8 @@ public function index(Request $request): View 's3.bucket_name', 's3.use_path_style_endpoint', 's3.enabled', + 's3.is_local', + 's3.minio_instance_url', 's3.created_at', 's3.updated_at', ]) @@ -60,7 +62,10 @@ public function index(Request $request): View public function create(): View { - return $this->view->make('admin.s3.new'); + return $this->view->make('admin.s3.new', [ + 'minio_endpoint' => config('filesystems.disks.s3.endpoint', 'http://localhost:9000'), + 'minio_console_url' => env('MINIO_CONSOLE_URL', 'http://localhost:9001'), + ]); } public function view(S3 $s3): View @@ -74,25 +79,32 @@ public function view(S3 $s3): View public function delete(Request $request, S3 $s3): RedirectResponse { - // Optional: check if in use if ($s3->servers()->exists()) { $this->alert->error('Cannot delete: bucket is used by servers.')->flash(); - return redirect()->route('admin.buckets.view', $s3->id); + return redirect()->route('admin.depr.buckets.view', $s3->id); } $this->deletionService->handle($s3); $this->alert->success('S3 configuration deleted.')->flash(); - return redirect()->route('admin.buckets'); + return redirect()->route('admin.depr.buckets'); } public function store(NewS3FormRequest $request): RedirectResponse { - $s3 = $this->creationService->handle($request->validated()); + $data = $request->validated(); + + if (!empty($data['is_local'])) { + $data['is_local'] = true; + $data['minio_instance_url'] = $data['minio_instance_url'] ?? config('filesystems.disks.s3.endpoint', 'http://localhost:9000'); + $data['use_path_style_endpoint'] = true; + } + + $s3 = $this->creationService->handle($data); $this->alert->success('S3 configuration created.')->flash(); - return redirect()->route('admin.buckets.view', $s3->id); + return redirect()->route('admin.depr.buckets.view', $s3->id); } public function update(S3FormRequest $request, S3 $s3): RedirectResponse @@ -101,7 +113,66 @@ public function update(S3FormRequest $request, S3 $s3): RedirectResponse $this->alert->success('S3 configuration updated.')->flash(); - return redirect()->route('admin.buckets.view', $s3->id); + return redirect()->route('admin.depr.buckets.view', $s3->id); + } + + public function provisionLocal(Request $request): RedirectResponse + { + $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'bucket_name' => 'required|string|max:255|unique:s3,bucket_name', + ]); + + $accessKey = env('MINIO_ROOT_USER', 'minioadmin'); + $secretKey = env('MINIO_ROOT_PASSWORD', 'minioadmin'); + $endpoint = env('MINIO_ENDPOINT', 'http://minio:9000'); + $consoleUrl = env('MINIO_CONSOLE_URL', 'http://localhost:9001'); + $bucketName = $request->input('bucket_name'); + $region = 'us-east-1'; + + try { + $client = new S3Client([ + 'version' => 'latest', + 'region' => $region, + 'endpoint' => $endpoint, + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $accessKey, + 'secret' => $secretKey, + ], + ]); + + if (!$client->doesBucketExist($bucketName)) { + $client->createBucket(['Bucket' => $bucketName]); + } + + $s3 = $this->creationService->handle([ + 'name' => $request->input('name'), + 'description' => $request->input('description'), + 'access_key' => $accessKey, + 'secret_key' => $secretKey, + 'endpoint' => $endpoint, + 'bucket_name' => $bucketName, + 'use_path_style_endpoint' => true, + 'enabled' => true, + 'is_local' => true, + 'minio_instance_url' => $consoleUrl, + ]); + + $this->alert->success("Local MinIO bucket '{$bucketName}' created and configured.")->flash(); + + return redirect()->route('admin.depr.buckets.view', $s3->id); + } catch (AwsException $e) { + $error = $e->getAwsErrorMessage() ?: $e->getMessage(); + $this->alert->error("MinIO provisioning failed: {$error}")->flash(); + + return redirect()->route('admin.depr.buckets.new'); + } catch (\Exception $e) { + $this->alert->error("Failed to provision MinIO bucket: {$e->getMessage()}")->flash(); + + return redirect()->route('admin.depr.buckets.new'); + } } public function testConnection(Request $request): \Illuminate\Http\JsonResponse diff --git a/app/Http/Controllers/Admin/Servers/CreateServerController.php b/app/Http/Controllers/Admin/Servers/CreateServerController.php index 9c71e4198..f902b5638 100644 --- a/app/Http/Controllers/Admin/Servers/CreateServerController.php +++ b/app/Http/Controllers/Admin/Servers/CreateServerController.php @@ -38,7 +38,7 @@ public function index(): View|RedirectResponse if (count($nodes) < 1) { $this->alert->warning(trans('admin/server.alerts.node_required'))->flash(); - return redirect()->route('admin.nodes'); + return redirect()->route('admin.depr.nodes'); } $nests = $this->nestRepository->getWithEggs(); diff --git a/app/Http/Controllers/Admin/Servers/ServerTransferController.php b/app/Http/Controllers/Admin/Servers/ServerTransferController.php index 327667284..94972b056 100644 --- a/app/Http/Controllers/Admin/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Admin/Servers/ServerTransferController.php @@ -53,7 +53,7 @@ public function transfer(Request $request, Server $server): RedirectResponse if (!$node->isViable($server->memory, $server->disk)) { $this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + return redirect()->route('admin.depr.servers.view.manage', $server->id); } $server->validateTransferState(); @@ -90,7 +90,7 @@ public function transfer(Request $request, Server $server): RedirectResponse $this->alert->success(trans('admin/server.alerts.transfer_started'))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + return redirect()->route('admin.depr.servers.view.manage', $server->id); } /** diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 73bb7624e..fb99a0020 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -75,7 +75,7 @@ public function setDetails(Request $request, Server $server): RedirectResponse $this->alert->success(trans('admin/server.alerts.details_updated'))->flash(); - return redirect()->route('admin.servers.view.details', $server->id); + return redirect()->route('admin.depr.servers.view.details', $server->id); } /** @@ -97,7 +97,7 @@ public function toggleInstall(Server $server): RedirectResponse $this->alert->success(trans('admin/server.alerts.install_toggled'))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + return redirect()->route('admin.depr.servers.view.manage', $server->id); } /** @@ -112,7 +112,7 @@ public function reinstallServer(Server $server): RedirectResponse $this->reinstallService->handle($server); $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + return redirect()->route('admin.depr.servers.view.manage', $server->id); } /** @@ -129,7 +129,7 @@ public function manageSuspension(Request $request, Server $server): RedirectResp 'status' => $request->input('action') . 'ed', ]))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + return redirect()->route('admin.depr.servers.view.manage', $server->id); } /** @@ -154,7 +154,7 @@ public function updateBuild(Request $request, Server $server): RedirectResponse $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); - return redirect()->route('admin.servers.view.build', $server->id); + return redirect()->route('admin.depr.servers.view.build', $server->id); } /** @@ -168,7 +168,7 @@ public function delete(Request $request, Server $server): RedirectResponse $this->deletionService->withForce($request->filled('force_delete'))->handle($server); $this->alert->success(trans('admin/server.alerts.server_deleted'))->flash(); - return redirect()->route('admin.servers'); + return redirect()->route('admin.depr.servers'); } /** @@ -194,7 +194,7 @@ public function saveStartup(Request $request, Server $server): RedirectResponse $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); - return redirect()->route('admin.servers.view.startup', $server->id); + return redirect()->route('admin.depr.servers.view.startup', $server->id); } /** @@ -211,7 +211,7 @@ public function newDatabase(StoreServerDatabaseRequest $request, Server $server) 'max_connections' => $request->input('max_connections'), ]); - return redirect()->route('admin.servers.view.database', $server->id)->withInput(); + return redirect()->route('admin.depr.servers.view.database', $server->id)->withInput(); } /** @@ -257,7 +257,7 @@ public function addMount(Request $request, Server $server): RedirectResponse $this->alert->success('Mount was added successfully.')->flash(); - return redirect()->route('admin.servers.view.mounts', $server->id); + return redirect()->route('admin.depr.servers.view.mounts', $server->id); } /** @@ -269,6 +269,6 @@ public function deleteMount(Server $server, Mount $mount): RedirectResponse $this->alert->success('Mount was removed successfully.')->flash(); - return redirect()->route('admin.servers.view.mounts', $server->id); + return redirect()->route('admin.depr.servers.view.mounts', $server->id); } } diff --git a/app/Http/Controllers/Admin/Settings/AdvancedController.php b/app/Http/Controllers/Admin/Settings/AdvancedController.php index de58f3cf3..aee27dc1f 100644 --- a/app/Http/Controllers/Admin/Settings/AdvancedController.php +++ b/app/Http/Controllers/Admin/Settings/AdvancedController.php @@ -49,6 +49,6 @@ public function update(AdvancedSettingsFormRequest $request): RedirectResponse $this->kernel->call('queue:restart'); $this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - return redirect()->route('admin.settings.advanced'); + return redirect()->route('admin.depr.settings.advanced'); } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/Settings/CaptchaController.php b/app/Http/Controllers/Admin/Settings/CaptchaController.php index 289cd3352..f3a6ce64d 100644 --- a/app/Http/Controllers/Admin/Settings/CaptchaController.php +++ b/app/Http/Controllers/Admin/Settings/CaptchaController.php @@ -61,6 +61,6 @@ public function update(CaptchaSettingsFormRequest $request): RedirectResponse $this->kernel->call('queue:restart'); $this->alert->success('Captcha settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - return redirect()->route('admin.settings.captcha'); + return redirect()->route('admin.depr.settings.captcha'); } } diff --git a/app/Http/Controllers/Admin/Settings/DomainsController.php b/app/Http/Controllers/Admin/Settings/DomainsController.php index 8cf70ec3f..8e16d0ffd 100644 --- a/app/Http/Controllers/Admin/Settings/DomainsController.php +++ b/app/Http/Controllers/Admin/Settings/DomainsController.php @@ -76,7 +76,7 @@ public function store(DomainFormRequest $request): RedirectResponse ]); }); - return redirect()->route('admin.settings.domains.index') + return redirect()->route('admin.depr.settings.domains.index') ->with('success', 'Domain created successfully.'); } catch (DnsProviderException $e) { return back()->withInput()->withErrors(['dns_config' => $e->getMessage()]); @@ -139,7 +139,7 @@ public function update(DomainFormRequest $request, Domain $domain): RedirectResp ]); }); - return redirect()->route('admin.settings.domains.index') + return redirect()->route('admin.depr.settings.domains.index') ->with('success', 'Domain updated successfully.'); } catch (DnsProviderException $e) { return back()->withInput()->withErrors(['dns_config' => $e->getMessage()]); @@ -170,7 +170,7 @@ public function destroy(Domain $domain): RedirectResponse $domain->delete(); - return redirect()->route('admin.settings.domains.index') + return redirect()->route('admin.depr.settings.domains.index') ->with('success', 'Domain deleted successfully.'); } catch (\Exception $e) { return back()->withErrors(['general' => 'Failed to delete domain: ' . $e->getMessage()]); diff --git a/app/Http/Controllers/Admin/Settings/IndexController.php b/app/Http/Controllers/Admin/Settings/IndexController.php index b7998adc7..731d17cf3 100644 --- a/app/Http/Controllers/Admin/Settings/IndexController.php +++ b/app/Http/Controllers/Admin/Settings/IndexController.php @@ -55,6 +55,6 @@ public function update(BaseSettingsFormRequest $request): RedirectResponse $this->kernel->call('queue:restart'); $this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - return redirect()->route('admin.settings'); + return redirect()->route('admin.depr.settings'); } } diff --git a/app/Http/Controllers/Admin/Settings/LogoController.php b/app/Http/Controllers/Admin/Settings/LogoController.php index 86044721f..35dac0397 100644 --- a/app/Http/Controllers/Admin/Settings/LogoController.php +++ b/app/Http/Controllers/Admin/Settings/LogoController.php @@ -37,6 +37,6 @@ public function update(LogoFormRequest $request): RedirectResponse $this->kernel->call('queue:restart'); $this->alert->success('Logo settings have been updated successfully.')->flash(); - return redirect()->route('admin.settings.logo'); + return redirect()->route('admin.depr.settings.logo'); } } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index c63442db2..589fc7c72 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -108,7 +108,7 @@ public function delete(Request $request, User $user): RedirectResponse $this->deletionService->handle($user); - return redirect()->route('admin.users'); + return redirect()->route('admin.depr.users'); } /** @@ -122,7 +122,7 @@ public function store(NewUserFormRequest $request): RedirectResponse $user = $this->creationService->handle($request->normalize()); $this->alert->success($this->translator->get('admin/user.notices.account_created'))->flash(); - return redirect()->route('admin.users.view', $user->id); + return redirect()->route('admin.depr.users.view', $user->id); } /** @@ -139,7 +139,7 @@ public function update(UserFormRequest $request, User $user): RedirectResponse $this->alert->success(trans('admin/user.notices.account_updated'))->flash(); - return redirect()->route('admin.users.view', $user->id); + return redirect()->route('admin.depr.users.view', $user->id); } /** diff --git a/app/Http/Controllers/Api/Application/ApiKeys/ApiKeyController.php b/app/Http/Controllers/Api/Application/ApiKeys/ApiKeyController.php new file mode 100644 index 000000000..77d3f32bf --- /dev/null +++ b/app/Http/Controllers/Api/Application/ApiKeys/ApiKeyController.php @@ -0,0 +1,80 @@ +allowedFilters(['memo', 'key_type']) + ->allowedSorts(['id', 'created_at']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($keys) + ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->toArray(); + } + + public function view(GetApiKeyRequest $request, ApiKey $key): array + { + return $this->fractal->item($key) + ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->toArray(); + } + + public function store(StoreApiKeyRequest $request): JsonResponse + { + $key = $this->creationService + ->setKeyType(ApiKey::TYPE_APPLICATION) + ->handle($request->validated(), $request->permissions() ?? []); + + return $this->fractal->item($key) + ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.api-keys.view', [ + 'key' => $key->id, + ]), + 'token' => decrypt($key->token), + ]) + ->respond(201); + } + + public function update(UpdateApiKeyRequest $request, ApiKey $key): array + { + $data = $request->validated(); + $data = array_merge($data, $request->permissions() ?? []); + + $key->forceFill($data)->save(); + + return $this->fractal->item($key->refresh()) + ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->toArray(); + } + + public function delete(DeleteApiKeyRequest $request, ApiKey $key): Response + { + $key->delete(); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Api/Application/DatabaseHosts/DatabaseHostController.php b/app/Http/Controllers/Api/Application/DatabaseHosts/DatabaseHostController.php new file mode 100644 index 000000000..448d585aa --- /dev/null +++ b/app/Http/Controllers/Api/Application/DatabaseHosts/DatabaseHostController.php @@ -0,0 +1,78 @@ +allowedFilters(['name', 'host']) + ->allowedSorts(['name', 'id']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($hosts) + ->transformWith($this->getTransformer(DatabaseHostTransformer::class)) + ->toArray(); + } + + public function view(GetDatabaseHostRequest $request, DatabaseHost $host): array + { + return $this->fractal->item($host) + ->transformWith($this->getTransformer(DatabaseHostTransformer::class)) + ->toArray(); + } + + public function store(StoreDatabaseHostRequest $request): JsonResponse + { + $host = $this->creationService->handle($request->validated()); + + return $this->fractal->item($host) + ->transformWith($this->getTransformer(DatabaseHostTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.database-hosts.view', [ + 'host' => $host->id, + ]), + ]) + ->respond(201); + } + + public function update(UpdateDatabaseHostRequest $request, DatabaseHost $host): array + { + $host = $this->updateService->handle($host->id, $request->validated()); + + return $this->fractal->item($host) + ->transformWith($this->getTransformer(DatabaseHostTransformer::class)) + ->toArray(); + } + + public function delete(DeleteDatabaseHostRequest $request, DatabaseHost $host): Response + { + $this->deletionService->handle($host->id); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Api/Application/Mounts/MountController.php b/app/Http/Controllers/Api/Application/Mounts/MountController.php new file mode 100644 index 000000000..3d36bb1ea --- /dev/null +++ b/app/Http/Controllers/Api/Application/Mounts/MountController.php @@ -0,0 +1,71 @@ +allowedFilters(['name', 'source', 'target']) + ->allowedSorts(['name', 'id']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($mounts) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + public function view(GetMountRequest $request, Mount $mount): array + { + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + public function store(StoreMountRequest $request): JsonResponse + { + $mount = Mount::create($request->validated()); + + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.mounts.view', [ + 'mount' => $mount->id, + ]), + ]) + ->respond(201); + } + + public function update(UpdateMountRequest $request, Mount $mount): array + { + $mount->update($request->validated()); + + return $this->fractal->item($mount->refresh()) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + public function delete(DeleteMountRequest $request, Mount $mount): Response + { + $mount->delete(); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Api/Application/Nests/EggController.php b/app/Http/Controllers/Api/Application/Nests/EggController.php index 44cd9fe6b..d38041319 100644 --- a/app/Http/Controllers/Api/Application/Nests/EggController.php +++ b/app/Http/Controllers/Api/Application/Nests/EggController.php @@ -2,21 +2,43 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nests; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Pterodactyl\Models\Egg; use Pterodactyl\Models\Nest; +use Spatie\QueryBuilder\QueryBuilder; +use Pterodactyl\Services\Eggs\EggCreationService; +use Pterodactyl\Services\Eggs\EggUpdateService; +use Pterodactyl\Services\Eggs\EggDeletionService; use Pterodactyl\Transformers\Api\Application\EggTransformer; use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggRequest; use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggsRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\StoreEggRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\UpdateEggRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\DeleteEggRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class EggController extends ApplicationApiController { + public function __construct( + private EggCreationService $creationService, + private EggUpdateService $updateService, + private EggDeletionService $deletionService, + ) { + parent::__construct(); + } + /** * List all eggs in a nest */ public function index(GetEggsRequest $request, Nest $nest): array { - return $this->fractal->collection($nest->eggs) + $eggs = QueryBuilder::for(Egg::query()->where('nest_id', $nest->id)) + ->allowedFilters(['name']) + ->allowedSorts(['id', 'name']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($eggs) ->transformWith($this->getTransformer(EggTransformer::class)) ->toArray(); } @@ -30,4 +52,45 @@ public function view(GetEggRequest $request, Nest $nest, Egg $egg): array ->transformWith($this->getTransformer(EggTransformer::class)) ->toArray(); } + + /** + * Create a new egg for the given nest. + */ + public function store(StoreEggRequest $request, Nest $nest): JsonResponse + { + $data = array_merge($request->validated(), ['nest_id' => $nest->id]); + $egg = $this->creationService->handle($data); + + return $this->fractal->item($egg) + ->transformWith($this->getTransformer(EggTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.nests.eggs.view', [ + 'nest' => $nest->id, + 'egg' => $egg->id, + ]), + ]) + ->respond(201); + } + + /** + * Update an egg. + */ + public function update(UpdateEggRequest $request, Nest $nest, Egg $egg): array + { + $this->updateService->handle($egg, $request->validated()); + + return $this->fractal->item($egg->refresh()) + ->transformWith($this->getTransformer(EggTransformer::class)) + ->toArray(); + } + + /** + * Delete an egg. + */ + public function delete(DeleteEggRequest $request, Nest $nest, Egg $egg): Response + { + $this->deletionService->handle($egg->id); + + return response('', 204); + } } diff --git a/app/Http/Controllers/Api/Application/Nests/EggVariableController.php b/app/Http/Controllers/Api/Application/Nests/EggVariableController.php new file mode 100644 index 000000000..c14262178 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nests/EggVariableController.php @@ -0,0 +1,75 @@ +fractal->collection($egg->variables) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->toArray(); + } + + public function view(GetEggVariableRequest $request, Nest $nest, Egg $egg, EggVariable $variable): array + { + return $this->fractal->item($variable) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->toArray(); + } + + public function store(StoreEggVariableRequest $request, Nest $nest, Egg $egg): JsonResponse + { + $data = array_merge($request->validated(), ['egg_id' => $egg->id]); + $variable = $this->creationService->handle($data); + + return $this->fractal->item($variable) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.nests.eggs.variables.view', [ + 'nest' => $nest->id, + 'egg' => $egg->id, + 'variable' => $variable->id, + ]), + ]) + ->respond(201); + } + + public function update(UpdateEggVariableRequest $request, Nest $nest, Egg $egg, EggVariable $variable): array + { + $this->updateService->handle($variable, $request->validated()); + + return $this->fractal->item($variable->refresh()) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->toArray(); + } + + public function delete(DeleteEggVariableRequest $request, Nest $nest, Egg $egg, EggVariable $variable): Response + { + $variable->delete(); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php index de352adaa..55e29a58f 100644 --- a/app/Http/Controllers/Api/Application/Nests/NestController.php +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -2,19 +2,28 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nests; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Pterodactyl\Models\Nest; +use Pterodactyl\Services\Nests\NestCreationService; +use Pterodactyl\Services\Nests\NestUpdateService; +use Pterodactyl\Services\Nests\NestDeletionService; use Pterodactyl\Contracts\Repository\NestRepositoryInterface; use Pterodactyl\Transformers\Api\Application\NestTransformer; use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\StoreNestRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\UpdateNestRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\DeleteNestRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class NestController extends ApplicationApiController { - /** - * NestController constructor. - */ - public function __construct(private NestRepositoryInterface $repository) - { + public function __construct( + private NestRepositoryInterface $repository, + private NestCreationService $creationService, + private NestUpdateService $updateService, + private NestDeletionService $deletionService, + ) { parent::__construct(); } @@ -23,7 +32,9 @@ public function __construct(private NestRepositoryInterface $repository) */ public function index(GetNestsRequest $request): array { - $nests = $this->repository->paginated($request->query('per_page') ?? 50); + $nests = Nest::query() + ->withCount('eggs', 'servers') + ->paginate($request->query('per_page') ?? 50); return $this->fractal->collection($nests) ->transformWith($this->getTransformer(NestTransformer::class)) @@ -35,8 +46,41 @@ public function index(GetNestsRequest $request): array */ public function view(GetNestsRequest $request, Nest $nest): array { + $nest->loadCount('eggs', 'servers'); + + return $this->fractal->item($nest) + ->transformWith($this->getTransformer(NestTransformer::class)) + ->toArray(); + } + + public function store(StoreNestRequest $request): JsonResponse + { + $nest = $this->creationService->handle($request->validated()); + $nest->loadCount('eggs', 'servers'); + return $this->fractal->item($nest) + ->transformWith($this->getTransformer(NestTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.nests.view', [ + 'nest' => $nest->id, + ]), + ]) + ->respond(201); + } + + public function update(UpdateNestRequest $request, Nest $nest): array + { + $this->updateService->handle($nest->id, $request->validated()); + + return $this->fractal->item($nest->refresh()->loadCount('eggs', 'servers')) ->transformWith($this->getTransformer(NestTransformer::class)) ->toArray(); } + + public function delete(DeleteNestRequest $request, Nest $nest): Response + { + $this->deletionService->handle($nest->id); + + return response('', 204); + } } diff --git a/app/Http/Controllers/Api/Application/S3/S3Controller.php b/app/Http/Controllers/Api/Application/S3/S3Controller.php new file mode 100644 index 000000000..63784e4ad --- /dev/null +++ b/app/Http/Controllers/Api/Application/S3/S3Controller.php @@ -0,0 +1,78 @@ +allowedFilters(['name']) + ->allowedSorts(['name', 'id']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($buckets) + ->transformWith($this->getTransformer(S3Transformer::class)) + ->toArray(); + } + + public function view(GetBucketRequest $request, S3 $bucket): array + { + return $this->fractal->item($bucket) + ->transformWith($this->getTransformer(S3Transformer::class)) + ->toArray(); + } + + public function store(StoreBucketRequest $request): JsonResponse + { + $bucket = $this->creationService->handle($request->validated()); + + return $this->fractal->item($bucket) + ->transformWith($this->getTransformer(S3Transformer::class)) + ->addMeta([ + 'resource' => route('api.application.buckets.view', [ + 'bucket' => $bucket->id, + ]), + ]) + ->respond(201); + } + + public function update(UpdateBucketRequest $request, S3 $bucket): array + { + $bucket = $this->updateService->handle($bucket, $request->validated()); + + return $this->fractal->item($bucket) + ->transformWith($this->getTransformer(S3Transformer::class)) + ->toArray(); + } + + public function delete(DeleteBucketRequest $request, S3 $bucket): Response + { + $this->deletionService->handle($bucket); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Api/Application/Settings/SettingsController.php b/app/Http/Controllers/Api/Application/Settings/SettingsController.php new file mode 100644 index 000000000..658bd5d9c --- /dev/null +++ b/app/Http/Controllers/Api/Application/Settings/SettingsController.php @@ -0,0 +1,425 @@ + $this->config->get('app.name'), + 'app:locale' => $this->config->get('app.locale'), + 'pterodactyl:auth:2fa_required' => (int) $this->config->get('pterodactyl.auth.2fa_required', 0), + 'available_languages' => $this->getAvailableLanguages(true), + ]); + } + + public function updateGeneral(Request $request): JsonResponse + { + $data = $request->validate([ + 'app:name' => 'required|string|max:191', + 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', + 'app:locale' => 'required|string|max:10', + ]); + + foreach ($data as $key => $value) { + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + + return new JsonResponse(['success' => true]); + } + + public function mail(): JsonResponse + { + return new JsonResponse([ + 'disabled' => $this->config->get('mail.default') !== 'smtp', + 'mail:mailers:smtp:host' => $this->config->get('mail.mailers.smtp.host'), + 'mail:mailers:smtp:port' => $this->config->get('mail.mailers.smtp.port'), + 'mail:mailers:smtp:encryption' => $this->config->get('mail.mailers.smtp.encryption'), + 'mail:mailers:smtp:username' => $this->config->get('mail.mailers.smtp.username'), + 'mail:from:address' => $this->config->get('mail.from.address'), + 'mail:from:name' => $this->config->get('mail.from.name'), + ]); + } + + public function updateMail(Request $request): JsonResponse + { + if ($this->config->get('mail.default') !== 'smtp') { + throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.'); + } + + $data = $request->validate([ + 'mail:mailers:smtp:host' => 'required|string', + 'mail:mailers:smtp:port' => 'required|integer|between:1,65535', + 'mail:mailers:smtp:encryption' => 'present|in:null,"",tls,ssl', + 'mail:mailers:smtp:username' => 'nullable|string|max:191', + 'mail:mailers:smtp:password' => 'nullable|string|max:191', + 'mail:from:address' => 'required|string|email', + 'mail:from:name' => 'nullable|string|max:191', + ]); + + if (($data['mail:mailers:smtp:password'] ?? '') === '!e') { + $data['mail:mailers:smtp:password'] = ''; + } + + foreach ($data as $key => $value) { + if (empty($value) && $key === 'mail:mailers:smtp:password') { + continue; + } + + if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) { + $value = $this->encrypter->encrypt($value); + } + + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + + return new JsonResponse(['success' => true]); + } + + public function testMail(Request $request): JsonResponse + { + try { + Notification::route('mail', $request->user()->email) + ->notify(new MailTested($request->user())); + } catch (\Exception $exception) { + $message = $exception->getMessage(); + + if (str_contains($message, 'stream_socket_client') || str_contains($message, 'getaddrinfo')) { + return new JsonResponse([ + 'error' => 'Could not connect to the mail server. Please verify your SMTP host and port settings are correct and that the server is reachable.', + ], 500); + } + + return new JsonResponse(['error' => 'Failed to send test email: ' . $message], 500); + } + + return new JsonResponse(['success' => true]); + } + + public function captcha(): JsonResponse + { + return new JsonResponse([ + 'providers' => Captchas::all(), + 'pterodactyl:captcha:provider' => $this->config->get('pterodactyl.captcha.provider', 'none'), + 'pterodactyl:captcha:turnstile:site_key' => $this->config->get('pterodactyl.captcha.turnstile.site_key', ''), + 'pterodactyl:captcha:hcaptcha:site_key' => $this->config->get('pterodactyl.captcha.hcaptcha.site_key', ''), + 'pterodactyl:captcha:recaptcha:site_key' => $this->config->get('pterodactyl.captcha.recaptcha.site_key', ''), + ]); + } + + public function updateCaptcha(Request $request): JsonResponse + { + $data = $request->validate([ + 'pterodactyl:captcha:provider' => 'required|string|in:none,turnstile,hcaptcha,recaptcha', + 'pterodactyl:captcha:turnstile:site_key' => 'nullable|string|max:255', + 'pterodactyl:captcha:turnstile:secret_key' => 'nullable|string|max:255', + 'pterodactyl:captcha:hcaptcha:site_key' => 'nullable|string|max:255', + 'pterodactyl:captcha:hcaptcha:secret_key' => 'nullable|string|max:255', + 'pterodactyl:captcha:recaptcha:site_key' => 'nullable|string|max:255', + 'pterodactyl:captcha:recaptcha:secret_key' => 'nullable|string|max:255', + ]); + + $provider = $data['pterodactyl:captcha:provider']; + + if ($provider === 'none') { + $data['pterodactyl:captcha:turnstile:site_key'] = ''; + $data['pterodactyl:captcha:turnstile:secret_key'] = ''; + $data['pterodactyl:captcha:hcaptcha:site_key'] = ''; + $data['pterodactyl:captcha:hcaptcha:secret_key'] = ''; + $data['pterodactyl:captcha:recaptcha:site_key'] = ''; + $data['pterodactyl:captcha:recaptcha:secret_key'] = ''; + } elseif ($provider === 'turnstile') { + $data['pterodactyl:captcha:hcaptcha:site_key'] = ''; + $data['pterodactyl:captcha:hcaptcha:secret_key'] = ''; + $data['pterodactyl:captcha:recaptcha:site_key'] = ''; + $data['pterodactyl:captcha:recaptcha:secret_key'] = ''; + $request->validate([ + 'pterodactyl:captcha:turnstile:site_key' => 'required|string|max:255', + 'pterodactyl:captcha:turnstile:secret_key' => 'required|string|max:255', + ]); + } elseif ($provider === 'hcaptcha') { + $data['pterodactyl:captcha:turnstile:site_key'] = ''; + $data['pterodactyl:captcha:turnstile:secret_key'] = ''; + $data['pterodactyl:captcha:recaptcha:site_key'] = ''; + $data['pterodactyl:captcha:recaptcha:secret_key'] = ''; + $request->validate([ + 'pterodactyl:captcha:hcaptcha:site_key' => 'required|string|max:255', + 'pterodactyl:captcha:hcaptcha:secret_key' => 'required|string|max:255', + ]); + } elseif ($provider === 'recaptcha') { + $data['pterodactyl:captcha:turnstile:site_key'] = ''; + $data['pterodactyl:captcha:turnstile:secret_key'] = ''; + $data['pterodactyl:captcha:hcaptcha:site_key'] = ''; + $data['pterodactyl:captcha:hcaptcha:secret_key'] = ''; + $request->validate([ + 'pterodactyl:captcha:recaptcha:site_key' => 'required|string|max:255', + 'pterodactyl:captcha:recaptcha:secret_key' => 'required|string|max:255', + ]); + } + + foreach ($data as $key => $value) { + if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) { + $value = $this->encrypter->encrypt($value); + } + + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + + return new JsonResponse(['success' => true]); + } + + public function branding(): JsonResponse + { + return new JsonResponse([ + 'logoType' => $this->logoService->getCurrentType(), + 'logoUrl' => $this->logoService->getCurrentUrl(), + 'logoValue' => $this->logoService->getCurrentValue(), + 'history' => $this->logoService->getHistory(), + ]); + } + + public function updateBranding(Request $request): JsonResponse + { + $validated = $request->validate([ + 'logo_file' => 'nullable|file|mimes:png,jpg,jpeg,gif,webp,svg|max:2048', + 'logo_url' => 'nullable|url|max:2048', + 'remove' => 'nullable|boolean', + 'rewind' => 'nullable|integer|min:0', + ]); + + $this->logoService->handle($validated); + $this->kernel->call('queue:restart'); + + return new JsonResponse(['success' => true]); + } + + public function advanced(): JsonResponse + { + return new JsonResponse([ + 'pterodactyl:guzzle:connect_timeout' => (int) $this->config->get('pterodactyl.guzzle.connect_timeout', 5), + 'pterodactyl:guzzle:timeout' => (int) $this->config->get('pterodactyl.guzzle.timeout', 30), + 'pterodactyl:client_features:allocations:enabled' => $this->config->get('pterodactyl.client_features.allocations.enabled', false) ? 'true' : 'false', + 'pterodactyl:client_features:allocations:range_start' => $this->config->get('pterodactyl.client_features.allocations.range_start'), + 'pterodactyl:client_features:allocations:range_end' => $this->config->get('pterodactyl.client_features.allocations.range_end'), + ]); + } + + public function updateAdvanced(Request $request): JsonResponse + { + $data = $request->validate([ + 'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60', + 'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60', + 'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false', + 'pterodactyl:client_features:allocations:range_start' => [ + 'nullable', + 'required_if:pterodactyl:client_features:allocations:enabled,true', + 'integer', + 'between:1024,65535', + ], + 'pterodactyl:client_features:allocations:range_end' => [ + 'nullable', + 'required_if:pterodactyl:client_features:allocations:enabled,true', + 'integer', + 'between:1024,65535', + ], + ]); + + foreach ($data as $key => $value) { + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + + return new JsonResponse(['success' => true]); + } + + public function domains(): JsonResponse + { + $domains = Domain::withCount('serverSubdomains')->orderBy('created_at', 'desc')->get(); + $providers = Providers::allWithDescriptions(); + + return new JsonResponse([ + 'domains' => $domains, + 'providers' => $providers, + ]); + } + + public function storeDomain(Request $request): JsonResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:191|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/|unique:domains,name', + 'dns_provider' => 'required|string|in:cloudflare,hetzner,route53', + 'dns_config' => 'required|array', + 'is_active' => 'sometimes|boolean', + 'is_default' => 'sometimes|boolean', + ]); + + try { + $providerClass = $this->getProviderClass($data['dns_provider']); + $provider = new $providerClass($data['dns_config']); + $provider->testConnection(); + + \DB::transaction(function () use ($data) { + if (!empty($data['is_default'])) { + Domain::where('is_default', true)->update(['is_default' => false]); + } + + $domain = Domain::create([ + 'name' => strtolower(trim($data['name'])), + 'dns_provider' => $data['dns_provider'], + 'dns_config' => $data['dns_config'], + 'is_active' => $data['is_active'] ?? true, + 'is_default' => $data['is_default'] ?? false, + ]); + + return $domain; + }); + + return new JsonResponse(['success' => true]); + } catch (DnsProviderException $e) { + return new JsonResponse(['error' => $e->getMessage()], 400); + } + } + + public function updateDomain(Request $request, Domain $domain): JsonResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:191|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/|unique:domains,name,' . $domain->id, + 'dns_provider' => 'required|string|in:cloudflare,hetzner,route53', + 'dns_config' => 'required|array', + 'is_active' => 'sometimes|boolean', + 'is_default' => 'sometimes|boolean', + ]); + + try { + if ($data['dns_config'] !== $domain->dns_config || $data['dns_provider'] !== $domain->dns_provider) { + $providerClass = $this->getProviderClass($data['dns_provider']); + $provider = new $providerClass($data['dns_config']); + $provider->testConnection(); + } + + \DB::transaction(function () use ($data, $domain) { + $newIsDefault = $data['is_default'] ?? false; + if ($newIsDefault && !$domain->is_default) { + Domain::where('is_default', true)->update(['is_default' => false]); + } elseif (!$newIsDefault && $domain->is_default) { + $defaultCount = Domain::where('is_default', true)->count(); + if ($defaultCount <= 1) { + throw new \Exception('Cannot remove default status: At least one domain must be set as default.'); + } + } + + $domain->update([ + 'name' => strtolower(trim($data['name'])), + 'dns_provider' => $data['dns_provider'], + 'dns_config' => $data['dns_config'], + 'is_active' => $data['is_active'] ?? $domain->is_active, + 'is_default' => $newIsDefault, + ]); + }); + + return new JsonResponse(['success' => true]); + } catch (DnsProviderException $e) { + return new JsonResponse(['error' => $e->getMessage()], 400); + } + } + + public function deleteDomain(Domain $domain): JsonResponse + { + $activeSubdomains = $domain->activeSubdomains()->count(); + if ($activeSubdomains > 0) { + return new JsonResponse(['error' => "Cannot delete domain with {$activeSubdomains} active subdomains."], 400); + } + + if ($domain->is_default) { + $defaultCount = Domain::where('is_default', true)->count(); + if ($defaultCount <= 1) { + return new JsonResponse(['error' => 'Cannot delete the only default domain. Please set another domain as default first.'], 400); + } + } + + $domain->delete(); + return new JsonResponse(['success' => true]); + } + + public function testConnection(Request $request): JsonResponse + { + $request->validate([ + 'dns_provider' => 'required|string', + 'dns_config' => 'required|array', + ]); + + try { + $providerClass = $this->getProviderClass($request->input('dns_provider')); + $provider = new $providerClass($request->input('dns_config')); + $provider->testConnection(); + + return new JsonResponse(['success' => true, 'message' => 'Connection successful.']); + } catch (DnsProviderException $e) { + return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 400); + } + } + + public function getProviderSchema(string $provider): JsonResponse + { + try { + $providerClass = $this->getProviderClass($provider); + $providerInstance = new $providerClass([]); + $schema = $providerInstance->getConfigurationSchema(); + + return new JsonResponse(['success' => true, 'schema' => $schema]); + } catch (\Exception $e) { + return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 400); + } + } + + private function getProviderClass(string $provider): string + { + $providers = Providers::all(); + + if (!isset($providers[$provider])) { + throw new \Exception("Unsupported DNS provider: {$provider}"); + } + + return $providers[$provider]; + } +} diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 679ac87fc..aa8e5db1e 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -33,7 +33,7 @@ public function __construct( */ public function index(GetUsersRequest $request): array { - $users = QueryBuilder::for(User::query()) + $users = QueryBuilder::for(User::query()->withCount('servers')) ->allowedFilters(['email', 'uuid', 'username', 'external_id']) ->allowedSorts(['id', 'uuid']) ->paginate($request->query('per_page') ?? 50); @@ -48,6 +48,8 @@ public function index(GetUsersRequest $request): array */ public function view(GetUsersRequest $request, User $user): array { + $user->loadCount('servers'); + return $this->fractal->item($user) ->transformWith($this->getTransformer(UserTransformer::class)) ->toArray(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 47e8d72a6..0087f0b81 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -68,7 +68,7 @@ class Kernel extends HttpKernel EncryptCookies::class, AddQueuedCookiesToResponse::class, /* StartSession::class, */ - /* EnsureStatefulRequests::class, */ + EnsureStatefulRequests::class, 'auth:sanctum', IsValidJson::class, TrackAPIKey::class, diff --git a/app/Http/Requests/Api/Application/ApiKeys/DeleteApiKeyRequest.php b/app/Http/Requests/Api/Application/ApiKeys/DeleteApiKeyRequest.php new file mode 100644 index 000000000..bb507153b --- /dev/null +++ b/app/Http/Requests/Api/Application/ApiKeys/DeleteApiKeyRequest.php @@ -0,0 +1,13 @@ +only([ + 'memo', + 'allowed_ips', + 'expires_at', + ])->toArray(); + } + + public function permissions(): array + { + $resources = AdminAcl::getResourceList(); + $permissions = []; + + if ($this->has('permissions') && is_array($this->input('permissions'))) { + foreach ($this->input('permissions') as $resource => $value) { + $permissions['r_' . $resource] = (int) $value; + } + } + + foreach ($resources as $resource) { + $column = 'r_' . $resource; + if ($this->has($column)) { + $permissions[$column] = (int) $this->input($column); + } + } + + return $permissions; + } +} diff --git a/app/Http/Requests/Api/Application/ApiKeys/UpdateApiKeyRequest.php b/app/Http/Requests/Api/Application/ApiKeys/UpdateApiKeyRequest.php new file mode 100644 index 000000000..291356f52 --- /dev/null +++ b/app/Http/Requests/Api/Application/ApiKeys/UpdateApiKeyRequest.php @@ -0,0 +1,18 @@ +only([ + 'memo', + 'allowed_ips', + 'expires_at', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/DatabaseHosts/DeleteDatabaseHostRequest.php b/app/Http/Requests/Api/Application/DatabaseHosts/DeleteDatabaseHostRequest.php new file mode 100644 index 000000000..ebf2cfee5 --- /dev/null +++ b/app/Http/Requests/Api/Application/DatabaseHosts/DeleteDatabaseHostRequest.php @@ -0,0 +1,13 @@ +only([ + 'name', + 'host', + 'port', + 'username', + 'password', + 'node_id', + ])->toArray(); + } + + public function attributes(): array + { + return [ + 'name' => 'Database Host Name', + 'host' => 'Database Host Address', + 'port' => 'Database Host Port', + 'username' => 'Database Host Username', + 'password' => 'Database Host Password', + 'node_id' => 'Linked Node', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php b/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php new file mode 100644 index 000000000..a5b5c82ef --- /dev/null +++ b/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php @@ -0,0 +1,22 @@ +route()->parameter('host')->id; + + return collect(DatabaseHost::getRulesForUpdate($hostId))->only([ + 'name', + 'host', + 'port', + 'username', + 'password', + 'node_id', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php new file mode 100644 index 000000000..b51abf207 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php @@ -0,0 +1,13 @@ +only([ + 'name', + 'description', + 'source', + 'target', + 'read_only', + 'user_mountable', + ])->toArray(); + } + + public function attributes(): array + { + return [ + 'name' => 'Mount Name', + 'description' => 'Mount Description', + 'source' => 'Source Path', + 'target' => 'Target Path', + 'read_only' => 'Read Only', + 'user_mountable' => 'User Mountable', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php b/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php new file mode 100644 index 000000000..9f19ce374 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php @@ -0,0 +1,22 @@ +route()->parameter('mount')->id; + + return collect(Mount::getRulesForUpdate($mountId))->only([ + 'name', + 'description', + 'source', + 'target', + 'read_only', + 'user_mountable', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php b/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php new file mode 100644 index 000000000..9db49edd4 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php @@ -0,0 +1,13 @@ +only([ + 'nest_id', + 'name', + 'description', + 'features', + 'docker_images', + 'startup', + 'config_from', + 'config_stop', + 'config_startup', + 'config_logs', + 'config_files', + 'update_url', + 'force_outgoing_ip', + 'file_denylist', + ])->toArray(); + } + + public function attributes(): array + { + return [ + 'nest_id' => 'Nest', + 'name' => 'Egg Name', + 'description' => 'Egg Description', + 'features' => 'Features', + 'docker_images' => 'Docker Images', + 'startup' => 'Startup Command', + 'config_from' => 'Config From Egg', + 'config_stop' => 'Stop Command', + 'config_startup' => 'Startup Configuration', + 'config_logs' => 'Log Configuration', + 'config_files' => 'File Configuration', + 'update_url' => 'Update URL', + 'force_outgoing_ip' => 'Force Outgoing IP', + 'file_denylist' => 'File Denylist', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/UpdateEggRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/UpdateEggRequest.php new file mode 100644 index 000000000..1f62a6cbc --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/Eggs/UpdateEggRequest.php @@ -0,0 +1,30 @@ +route()->parameter('egg')->id; + + return collect(Egg::getRulesForUpdate($eggId))->only([ + 'nest_id', + 'name', + 'description', + 'features', + 'docker_images', + 'startup', + 'config_from', + 'config_stop', + 'config_startup', + 'config_logs', + 'config_files', + 'update_url', + 'force_outgoing_ip', + 'file_denylist', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/Variables/DeleteEggVariableRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/Variables/DeleteEggVariableRequest.php new file mode 100644 index 000000000..8872bb175 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/Eggs/Variables/DeleteEggVariableRequest.php @@ -0,0 +1,13 @@ +only([ + 'name', + 'description', + 'env_variable', + 'default_value', + 'user_viewable', + 'user_editable', + 'rules', + ])->toArray(); + } + + public function attributes(): array + { + return [ + 'name' => 'Variable Name', + 'description' => 'Variable Description', + 'env_variable' => 'Environment Variable', + 'default_value' => 'Default Value', + 'user_viewable' => 'User Viewable', + 'user_editable' => 'User Editable', + 'rules' => 'Validation Rules', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/Variables/UpdateEggVariableRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/Variables/UpdateEggVariableRequest.php new file mode 100644 index 000000000..df88c91e2 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/Eggs/Variables/UpdateEggVariableRequest.php @@ -0,0 +1,23 @@ +route()->parameter('variable')->id; + + return collect(EggVariable::getRulesForUpdate($variableId))->only([ + 'name', + 'description', + 'env_variable', + 'default_value', + 'user_viewable', + 'user_editable', + 'rules', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Nests/StoreNestRequest.php b/app/Http/Requests/Api/Application/Nests/StoreNestRequest.php new file mode 100644 index 000000000..0f1cceae3 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/StoreNestRequest.php @@ -0,0 +1,30 @@ +only([ + 'name', + 'description', + ])->toArray(); + } + + public function attributes(): array + { + return [ + 'name' => 'Nest Name', + 'description' => 'Nest Description', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Nests/UpdateNestRequest.php b/app/Http/Requests/Api/Application/Nests/UpdateNestRequest.php new file mode 100644 index 000000000..0d8c8ff78 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/UpdateNestRequest.php @@ -0,0 +1,18 @@ +route()->parameter('nest')->id; + + return collect(Nest::getRulesForUpdate($nestId))->only([ + 'name', + 'description', + ])->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/S3/DeleteBucketRequest.php b/app/Http/Requests/Api/Application/S3/DeleteBucketRequest.php new file mode 100644 index 000000000..080f3d72c --- /dev/null +++ b/app/Http/Requests/Api/Application/S3/DeleteBucketRequest.php @@ -0,0 +1,13 @@ + 'required|string|max:255|unique:s3,name', + 'description' => 'nullable|string|max:1000', + 'access_key' => 'required|string|max:255', + 'secret_key' => 'required|string|max:255', + 'endpoint' => 'nullable|url|max:255', + 'bucket_name' => 'required|string|max:255', + 'use_path_style_endpoint' => 'boolean', + 'enabled' => 'boolean', + 'is_local' => 'boolean', + 'minio_instance_url' => 'nullable|string|max:255', + ]; + } + + public function attributes(): array + { + return [ + 'name' => 'Bucket Name', + 'description' => 'Bucket Description', + 'access_key' => 'Access Key', + 'secret_key' => 'Secret Key', + 'endpoint' => 'Endpoint URL', + 'bucket_name' => 'S3 Bucket Name', + 'use_path_style_endpoint' => 'Use Path Style Endpoint', + 'enabled' => 'Enabled', + 'is_local' => 'Local Storage', + 'minio_instance_url' => 'MinIO Instance URL', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/S3/UpdateBucketRequest.php b/app/Http/Requests/Api/Application/S3/UpdateBucketRequest.php new file mode 100644 index 000000000..0a25f0857 --- /dev/null +++ b/app/Http/Requests/Api/Application/S3/UpdateBucketRequest.php @@ -0,0 +1,24 @@ +route()->parameter('bucket')->id; + + return [ + 'name' => "required|string|max:255|unique:s3,name,{$bucketId}", + 'description' => 'nullable|string|max:1000', + 'access_key' => 'required|string|max:255', + 'secret_key' => 'required|string|max:255', + 'endpoint' => 'nullable|url|max:255', + 'bucket_name' => 'required|string|max:255', + 'use_path_style_endpoint' => 'boolean', + 'enabled' => 'boolean', + 'is_local' => 'boolean', + 'minio_instance_url' => 'nullable|string|max:255', + ]; + } +} diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php index 0e98b20af..70e71707c 100644 --- a/app/Models/ApiKey.php +++ b/app/Models/ApiKey.php @@ -121,6 +121,8 @@ class ApiKey extends Model * Fields that are mass assignable. */ protected $fillable = [ + 'user_id', + 'key_type', 'identifier', 'token', 'allowed_ips', diff --git a/app/Models/Nest.php b/app/Models/Nest.php index 22582fdd4..c5eef5955 100644 --- a/app/Models/Nest.php +++ b/app/Models/Nest.php @@ -40,6 +40,8 @@ class Nest extends Model 'description', ]; + + public static array $validationRules = [ 'author' => 'required|string|email', 'name' => 'required|string|max:191', diff --git a/app/Models/S3.php b/app/Models/S3.php index bf037a3d3..742364404 100644 --- a/app/Models/S3.php +++ b/app/Models/S3.php @@ -55,6 +55,8 @@ class S3 extends Model 'bucket_name', 'use_path_style_endpoint', 'enabled', + 'is_local', + 'minio_instance_url', ]; protected static array $validationRules = [ @@ -67,11 +69,14 @@ class S3 extends Model 'bucket_name' => 'required|string|max:255', 'use_path_style_endpoint' => 'boolean', 'enabled' => 'boolean', + 'is_local' => 'boolean', + 'minio_instance_url' => 'nullable|string|max:255', ]; protected $casts = [ 'use_path_style_endpoint' => 'boolean', 'enabled' => 'boolean', + 'is_local' => 'boolean', ]; protected $hidden = [ diff --git a/app/Transformers/Api/Application/ApiKeyTransformer.php b/app/Transformers/Api/Application/ApiKeyTransformer.php new file mode 100644 index 000000000..9984901f2 --- /dev/null +++ b/app/Transformers/Api/Application/ApiKeyTransformer.php @@ -0,0 +1,37 @@ +{$column}; + } + + return [ + 'id' => $model->id, + 'user_id' => $model->user_id, + 'key_type' => $model->key_type, + 'identifier' => $model->identifier, + 'memo' => $model->memo, + 'allowed_ips' => $model->allowed_ips, + 'last_used_at' => $model->last_used_at?->toAtomString(), + 'expires_at' => $model->expires_at?->toAtomString(), + 'permissions' => $permissions, + $model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at), + $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), + ]; + } +} diff --git a/app/Transformers/Api/Application/MountTransformer.php b/app/Transformers/Api/Application/MountTransformer.php new file mode 100644 index 000000000..6bc475514 --- /dev/null +++ b/app/Transformers/Api/Application/MountTransformer.php @@ -0,0 +1,72 @@ + $model->id, + 'uuid' => $model->uuid, + 'name' => $model->name, + 'description' => $model->description, + 'source' => $model->source, + 'target' => $model->target, + 'read_only' => $model->read_only, + 'user_mountable' => $model->user_mountable, + ]; + } + + public function includeEggs(Mount $model): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + $model->loadMissing('eggs'); + + return $this->collection($model->getRelation('eggs'), $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME); + } + + public function includeNodes(Mount $model): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + $model->loadMissing('nodes'); + + return $this->collection($model->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME); + } + + public function includeServers(Mount $model): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $model->loadMissing('servers'); + + return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME); + } +} diff --git a/app/Transformers/Api/Application/S3Transformer.php b/app/Transformers/Api/Application/S3Transformer.php new file mode 100644 index 000000000..61a033446 --- /dev/null +++ b/app/Transformers/Api/Application/S3Transformer.php @@ -0,0 +1,31 @@ + $model->id, + 'name' => $model->name, + 'description' => $model->description, + 'bucket_name' => $model->bucket_name, + 'endpoint' => $model->endpoint, + 'use_path_style_endpoint' => $model->use_path_style_endpoint, + 'is_local' => $model->is_local, + 'enabled' => $model->enabled, + 'minio_instance_url' => $model->minio_instance_url, + $model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at), + $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), + ]; + } +} diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index 14e354b45..becc24e3d 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -38,6 +38,7 @@ public function transform(User $user): array 'language' => $user->language, 'root_admin' => (bool) $user->root_admin, '2fa' => (bool) $user->use_totp, + 'servers_count' => $user->servers_count ?? 0, 'created_at' => $this->formatTimestamp($user->created_at), 'updated_at' => $this->formatTimestamp($user->updated_at), ]; diff --git a/database/Seeders/DatabaseSeeder.php b/database/Seeders/DatabaseSeeder.php index ae534fe6a..8ba2f2f5d 100644 --- a/database/Seeders/DatabaseSeeder.php +++ b/database/Seeders/DatabaseSeeder.php @@ -13,5 +13,6 @@ public function run() { $this->call(NestSeeder::class); $this->call(EggSeeder::class); + $this->call(TestUserSeeder::class); } } diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index 360641298..084649805 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -20,6 +20,7 @@ class EggSeeder extends Seeder */ public static array $import = [ 'Minecraft', + 'Minecraft (itzg)', 'Hytale', 'Source Engine', 'Voice Servers', @@ -59,6 +60,8 @@ protected function parseEggFiles(Nest $nest) $files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($nest->name))); $this->command->alert('Updating Eggs for Nest: ' . $nest->name); + + $managedIds = []; /** @var \DirectoryIterator $file */ foreach ($files as $file) { if (!$file->isFile() || !$file->isReadable()) { @@ -66,7 +69,7 @@ protected function parseEggFiles(Nest $nest) } $decoded = json_decode(file_get_contents($file->getRealPath()), true, 512, JSON_THROW_ON_ERROR); - $file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json'); + $eggFile = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json'); $egg = $nest->eggs() ->where('author', $decoded['author']) @@ -74,12 +77,19 @@ protected function parseEggFiles(Nest $nest) ->first(); if ($egg instanceof Egg) { - $this->updateImporterService->handle($egg, $file); + $this->updateImporterService->handle($egg, $eggFile); $this->command->info('Updated ' . $decoded['name']); } else { - $this->importerService->handle($file, $nest->id); + $egg = $this->importerService->handle($eggFile, $nest->id); $this->command->comment('Created ' . $decoded['name']); } + + $managedIds[] = $egg->id; + } + + $removed = $nest->eggs()->whereNotIn('id', $managedIds)->delete(); + if ($removed > 0) { + $this->command->warn("Removed {$removed} stale egg(s) from nest '{$nest->name}'"); } $this->command->line(''); diff --git a/database/Seeders/NestSeeder.php b/database/Seeders/NestSeeder.php index 339118679..033cf1d99 100644 --- a/database/Seeders/NestSeeder.php +++ b/database/Seeders/NestSeeder.php @@ -41,6 +41,7 @@ public function run() ])->keyBy('name')->toArray(); $this->createMinecraftNest(array_get($items, 'Minecraft')); + $this->createMinecraftIzzgNest(array_get($items, 'Minecraft (itzg)')); $this->createHytaleNest(array_get($items, 'Hytale')); $this->createSourceEngineNest(array_get($items, 'Source Engine')); $this->createVoiceServersNest(array_get($items, 'Voice Servers')); @@ -63,6 +64,21 @@ private function createMinecraftNest(?array $nest = null) } } + /** + * Create the Minecraft (itzg) nest for itzg-powered Docker images. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + private function createMinecraftIzzgNest(?array $nest = null) + { + if (is_null($nest)) { + $this->creationService->handle([ + 'name' => 'Minecraft (itzg)', + 'description' => 'Minecraft servers powered by itzg Docker images, with extensive configuration options.', + ], 'support@pterodactyl.io'); + } + } + /** * Create the Hytale nest to be used later on. * diff --git a/database/Seeders/TestUserSeeder.php b/database/Seeders/TestUserSeeder.php index c0eecc320..86d176f22 100644 --- a/database/Seeders/TestUserSeeder.php +++ b/database/Seeders/TestUserSeeder.php @@ -15,7 +15,10 @@ public function run(): void // while seeding these fixture accounts. \Illuminate\Database\Eloquent\Model::unguard(); - User::query()->updateOrCreate(['email' => 'admin@hydrodactyl.dev'], [ + // Delete all existing users (dev/test mode only) + User::query()->forceDelete(); + + User::create([ 'uuid' => Uuid::uuid4()->toString(), 'username' => 'admin', 'email' => 'admin@hydrodactyl.dev', @@ -29,7 +32,21 @@ public function run(): void 'updated_at' => Carbon::now(), ]); - User::query()->updateOrCreate(['email' => 'test@hydrodactyl.dev'], [ + User::create([ + 'uuid' => Uuid::uuid4()->toString(), + 'username' => 'dev', + 'email' => 'dev@hydrodactyl.dev', + 'name_first' => 'Dev', + 'name_last' => 'User', + 'password' => bcrypt('dev'), + 'language' => 'en', + 'root_admin' => true, + 'use_totp' => false, + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]); + + User::create([ 'uuid' => Uuid::uuid4()->toString(), 'username' => 'test', 'email' => 'test@hydrodactyl.dev', diff --git a/database/Seeders/eggs/hytale/egg-hytale.json b/database/Seeders/eggs/hytale/egg-hytale.json index c3c8410f4..fc99c1a7f 100644 --- a/database/Seeders/eggs/hytale/egg-hytale.json +++ b/database/Seeders/eggs/hytale/egg-hytale.json @@ -6,9 +6,11 @@ }, "exported_at": "2026-01-13T05:17:28+00:00", "name": "Hytale", - "author": "hello@pterodactyl.io", + "author": "support@pterodactyl.io", "description": "Set out on an adventure built for both creation and play. Hytale blends the freedom of a sandbox with the momentum of an RPG: explore a procedurally generated world full of dungeons, secrets, and a variety of creatures, then shape it block by block.", - "features": ["java_version"], + "features": [ + "java_version" + ], "docker_images": { "ghcr.io/pterodactyl/games:hytale": "ghcr.io/pterodactyl/games:hytale" }, diff --git a/database/Seeders/eggs/minecraft-itzg/egg-bedrock.json b/database/Seeders/eggs/minecraft-itzg/egg-bedrock.json new file mode 100644 index 000000000..1453c321e --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-bedrock.json @@ -0,0 +1,117 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Minecraft Bedrock", + "author": "support@pterodactyl.io", + "description": "Bedrock Dedicated Server allows you to run a Minecraft Bedrock Edition server. Uses the itzg/minecraft-bedrock-server Docker image.", + "features": [ + "eula", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Bedrock Server": "itzg/minecraft-bedrock-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \"Server started.\"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Bedrock installation script\n# itzg/minecraft-bedrock-server handles server binary download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Minecraft Version", + "description": "The version of Bedrock to run. Set to LATEST for the latest release.", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "10", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:1000", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The name displayed in the Minecraft server list.", + "env_variable": "SERVER_NAME", + "default_value": "Bedrock Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Level Name", + "description": "The name of the world/level.", + "env_variable": "LEVEL_NAME", + "default_value": "Bedrock Level", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:50", + "field_type": "text" + }, + { + "name": "Allow Cheats", + "description": "Enable cheats on the server.", + "env_variable": "ALLOW_CHEATS", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Tick Distance", + "description": "The radius (in chunks) around the player that the server will tick.", + "env_variable": "TICK_DISTANCE", + "default_value": "4", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:12", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-bungeecord.json b/database/Seeders/eggs/minecraft-itzg/egg-bungeecord.json new file mode 100644 index 000000000..cba1cf901 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-bungeecord.json @@ -0,0 +1,88 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Bungeecord", + "author": "support@pterodactyl.io", + "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. Uses the itzg/mc-proxy Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg MC Proxy": "itzg/mc-proxy:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \"Listening on \"}", + "logs": "{}", + "stop": "end" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Bungeecord installation script\n# itzg/mc-proxy handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of proxy to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "BUNGEECORD", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Proxy Version", + "description": "The version of the proxy to run. Set to LATEST for the latest release.", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 1G, 512M).", + "env_variable": "MEMORY", + "default_value": "1G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can connect through the proxy.", + "env_variable": "MAX_PLAYERS", + "default_value": "100", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-fabric.json b/database/Seeders/eggs/minecraft-itzg/egg-fabric.json new file mode 100644 index 000000000..e03daf248 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-fabric.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Fabric", + "author": "support@pterodactyl.io", + "description": "Fabric is a lightweight, experimental modding toolchain for Minecraft. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Fabric installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "FABRIC", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Fabric Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-folia.json b/database/Seeders/eggs/minecraft-itzg/egg-folia.json new file mode 100644 index 000000000..106ac3f56 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-folia.json @@ -0,0 +1,149 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Folia", + "author": "support@pterodactyl.io", + "description": "Folia is a fork of Paper that adds regionized multithreading to the server scheduler. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft", + "plugin/paper" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Folia installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "FOLIA", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Folia Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-forge.json b/database/Seeders/eggs/minecraft-itzg/egg-forge.json new file mode 100644 index 000000000..46ffd6f72 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-forge.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Forge", + "author": "support@pterodactyl.io", + "description": "Forge is a modding API for Minecraft that allows players to install and run mods. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Forge installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "FORGE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "4G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Forge Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-neoforge.json b/database/Seeders/eggs/minecraft-itzg/egg-neoforge.json new file mode 100644 index 000000000..00fd75810 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-neoforge.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "NeoForge", + "author": "support@pterodactyl.io", + "description": "NeoForge is a fork of Forge focused on separating the Minecraft modding ecosystem from the Forge name and legacy. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# NeoForge installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "NEOFORGE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "4G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "NeoForge Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-paper.json b/database/Seeders/eggs/minecraft-itzg/egg-paper.json new file mode 100644 index 000000000..a4f1e571b --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-paper.json @@ -0,0 +1,149 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Paper", + "author": "support@pterodactyl.io", + "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies. Uses the itzg/minecraft-server Docker image which auto-downloads the server jar and manages configuration via environment variables.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft", + "plugin/paper" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Paper installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "PAPER", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Paper Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-purpur.json b/database/Seeders/eggs/minecraft-itzg/egg-purpur.json new file mode 100644 index 000000000..08e901045 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-purpur.json @@ -0,0 +1,149 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Purpur", + "author": "support@pterodactyl.io", + "description": "Purpur is a fork of Paper and Tuinity with the goal of providing new and exciting configuration options for server owners. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft", + "plugin/paper" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Purpur installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "PURPUR", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Purpur Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-quilt.json b/database/Seeders/eggs/minecraft-itzg/egg-quilt.json new file mode 100644 index 000000000..63b7801c4 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-quilt.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Quilt", + "author": "support@pterodactyl.io", + "description": "Quilt is a community-driven modding toolchain built from the ground up for Minecraft. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Quilt installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "QUILT", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Quilt Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-sponge-vanilla.json b/database/Seeders/eggs/minecraft-itzg/egg-sponge-vanilla.json new file mode 100644 index 000000000..83c446355 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-sponge-vanilla.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "SpongeVanilla", + "author": "support@pterodactyl.io", + "description": "SpongeVanilla is the SpongeAPI implementation for vanilla Minecraft servers, providing plugin support without Forge. Uses the itzg/minecraft-server Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# SpongeVanilla installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "SPONGEVANILLA", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "SpongeVanilla Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft-itzg/egg-vanilla-minecraft.json new file mode 100644 index 000000000..15cee7acd --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-vanilla-minecraft.json @@ -0,0 +1,148 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Vanilla Minecraft", + "author": "support@pterodactyl.io", + "description": "Minecraft is a game about placing blocks and going on adventures. Uses the itzg/minecraft-server Docker image for pure vanilla gameplay.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg Minecraft Server": "itzg/docker-minecraft-server:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \")! For help, type \"}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Vanilla Minecraft installation script\n# itzg/docker-minecraft-server handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of Minecraft server to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "VANILLA", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Minecraft Version", + "description": "The version of Minecraft to run. Set to LATEST for the latest release, or specify a version number (e.g., 1.20.4).", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 2G, 4096M).", + "env_variable": "MEMORY", + "default_value": "2G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Difficulty", + "description": "The difficulty level of the server.", + "env_variable": "DIFFICULTY", + "default_value": "easy", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:peaceful,easy,normal,hard", + "field_type": "text" + }, + { + "name": "Game Mode", + "description": "Default game mode for new players.", + "env_variable": "MODE", + "default_value": "survival", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:survival,creative,adventure,spectator", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can join the server.", + "env_variable": "MAX_PLAYERS", + "default_value": "20", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + }, + { + "name": "Message of the Day", + "description": "The MOTD displayed in the server list.", + "env_variable": "MOTD", + "default_value": "A Minecraft Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Server Name", + "description": "The internal server name used for the world folder and level-name property.", + "env_variable": "SERVER_NAME", + "default_value": "Vanilla Server", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:100", + "field_type": "text" + }, + { + "name": "Enable RCON", + "description": "Enable RCON for remote server administration.", + "env_variable": "ENABLE_RCON", + "default_value": "FALSE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Override Server Properties", + "description": "Allow environment variables to override server.properties values.", + "env_variable": "OVERRIDE_SERVER_PROPERTIES", + "default_value": "TRUE", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-velocity.json b/database/Seeders/eggs/minecraft-itzg/egg-velocity.json new file mode 100644 index 000000000..d7b9a2168 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-velocity.json @@ -0,0 +1,88 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Velocity", + "author": "support@pterodactyl.io", + "description": "Velocity is a Minecraft server proxy with unparalleled server support, scalability, and flexibility. Uses the itzg/mc-proxy Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg MC Proxy": "itzg/mc-proxy:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \"Done (\"}", + "logs": "{}", + "stop": "end" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Velocity installation script\n# itzg/mc-proxy handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of proxy to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "VELOCITY", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Proxy Version", + "description": "The version of the proxy to run. Set to LATEST for the latest release.", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 1G, 512M).", + "env_variable": "MEMORY", + "default_value": "1G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can connect through the proxy.", + "env_variable": "MAX_PLAYERS", + "default_value": "100", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft-itzg/egg-waterfall.json b/database/Seeders/eggs/minecraft-itzg/egg-waterfall.json new file mode 100644 index 000000000..7f659a205 --- /dev/null +++ b/database/Seeders/eggs/minecraft-itzg/egg-waterfall.json @@ -0,0 +1,88 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY HYDRODACTYL PANEL", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-09T00:00:00+00:00", + "name": "Waterfall", + "author": "support@pterodactyl.io", + "description": "Waterfall is a fork of the well-known BungeeCord server proxy with improved performance and stability. Uses the itzg/mc-proxy Docker image.", + "features": [ + "eula", + "java_version", + "pid_limit", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "itzg MC Proxy": "itzg/mc-proxy:latest" + }, + "file_denylist": [], + "startup": "/start", + "config": { + "files": "{}", + "startup": "{\"done\": \"Listening on \"}", + "logs": "{}", + "stop": "end" + }, + "scripts": { + "installation": { + "script": "#!/bin/ash\n# Waterfall installation script\n# itzg/mc-proxy handles server jar download at container start\nmkdir -p /mnt/server\n", + "container": "ghcr.io/pterodactyl/installers:alpine", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Server Type", + "description": "The type of proxy to run. Fixed for this egg.", + "env_variable": "TYPE", + "default_value": "WATERFALL", + "user_viewable": false, + "user_editable": false, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Proxy Version", + "description": "The version of the proxy to run. Set to LATEST for the latest release.", + "env_variable": "VERSION", + "default_value": "LATEST", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:20", + "field_type": "text" + }, + { + "name": "Max Memory", + "description": "Maximum memory allocated to the Java heap (e.g., 1G, 512M).", + "env_variable": "MEMORY", + "default_value": "1G", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:10", + "field_type": "text" + }, + { + "name": "Online Mode", + "description": "Whether to authenticate players with Mojang servers. Set FALSE for offline/cracked servers.", + "env_variable": "ONLINE_MODE", + "default_value": "TRUE", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|in:TRUE,FALSE", + "field_type": "text" + }, + { + "name": "Max Players", + "description": "Maximum number of players that can connect through the proxy.", + "env_variable": "MAX_PLAYERS", + "default_value": "100", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:1|max:10000", + "field_type": "text" + } + ] +} diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json index 8cfcaf873..2b7f8d4f0 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json @@ -8,7 +8,14 @@ "name": "Bungeecord", "author": "support@pterodactyl.io", "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", - "features": ["eula", "java_version", "pid_limit", "plugin/bungeecord", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/bungeecord", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-fabric.json b/database/Seeders/eggs/minecraft/egg-fabric.json index d185ed970..39e902a9c 100644 --- a/database/Seeders/eggs/minecraft/egg-fabric.json +++ b/database/Seeders/eggs/minecraft/egg-fabric.json @@ -6,9 +6,16 @@ }, "exported_at": "2025-01-07T23:17:29+00:00", "name": "Fabric", - "author": "contact@awakenedredstone.com", + "author": "support@pterodactyl.io", "description": "Fabric Server. Fabric is a lightweight mod loader for Minecraft, with support for snapshots and fast updates to new versions.", - "features": ["eula", "java_version", "pid_limit", "mod/fabric", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "mod/fabric", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-folia.json b/database/Seeders/eggs/minecraft/egg-folia.json index f663d9281..848e5fe0e 100644 --- a/database/Seeders/eggs/minecraft/egg-folia.json +++ b/database/Seeders/eggs/minecraft/egg-folia.json @@ -6,9 +6,16 @@ }, "exported_at": "2026-03-27T14:36:34+01:00", "name": "Folia", - "author": "Hosting@NeverStopGaming.net", + "author": "support@pterodactyl.io", "description": "Fork of Paper which adds recognised multithreading to the dedicated server.", - "features": ["eula", "java_version", "pid_limit", "plugin/paper", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/paper", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json index d6d88b84e..e2e7ebeab 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json @@ -8,7 +8,14 @@ "name": "Forge Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", - "features": ["eula", "java_version", "pid_limit", "mod/forge", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "mod/forge", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-limbo.json b/database/Seeders/eggs/minecraft/egg-limbo.json index a02600a66..2280359f7 100644 --- a/database/Seeders/eggs/minecraft/egg-limbo.json +++ b/database/Seeders/eggs/minecraft/egg-limbo.json @@ -8,7 +8,14 @@ "name": "Limbo", "author": "xEfinax@protonmail.com", "description": "Standalone server program Limbo.", - "features": ["eula", "java_version", "pid_limit", "plugin/paper", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/paper", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 17": "ghcr.io/pterodactyl/yolks:java_17" diff --git a/database/Seeders/eggs/minecraft/egg-neoforge.json b/database/Seeders/eggs/minecraft/egg-neoforge.json index 36a5c4ce2..b773c18c7 100644 --- a/database/Seeders/eggs/minecraft/egg-neoforge.json +++ b/database/Seeders/eggs/minecraft/egg-neoforge.json @@ -6,9 +6,16 @@ }, "exported_at": "2024-05-07T12:55:56+00:00", "name": "Neoforge", - "author": "support@phosphor.host", + "author": "support@pterodactyl.io", "description": "Minecraft Neoforge Server. Minecraft Neoforge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", - "features": ["eula", "java_version", "pid_limit", "mod/neoforge", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "mod/neoforge", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-paper.json b/database/Seeders/eggs/minecraft/egg-paper.json index 465494cc2..2cbd04c76 100644 --- a/database/Seeders/eggs/minecraft/egg-paper.json +++ b/database/Seeders/eggs/minecraft/egg-paper.json @@ -6,9 +6,16 @@ }, "exported_at": "2024-05-07T12:55:55+00:00", "name": "Paper", - "author": "parker@pterodactyl.io", + "author": "support@pterodactyl.io", "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", - "features": ["eula", "java_version", "pid_limit", "plugin/paper", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/paper", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-purpur.json b/database/Seeders/eggs/minecraft/egg-purpur.json index 51b60b069..51b9bc872 100644 --- a/database/Seeders/eggs/minecraft/egg-purpur.json +++ b/database/Seeders/eggs/minecraft/egg-purpur.json @@ -6,9 +6,16 @@ }, "exported_at": "2024-06-01T19:39:58+00:00", "name": "Purpur", - "author": "purpur@birdflop.com", + "author": "support@pterodactyl.io", "description": "A drop-in replacement for Paper servers designed for configurability, and new fun and exciting gameplay features.", - "features": ["eula", "java_version", "pid_limit", "plugin/purpur", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/purpur", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io/pterodactyl/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-quilt.json b/database/Seeders/eggs/minecraft/egg-quilt.json index 808190c90..e1ed89119 100644 --- a/database/Seeders/eggs/minecraft/egg-quilt.json +++ b/database/Seeders/eggs/minecraft/egg-quilt.json @@ -6,9 +6,16 @@ }, "exported_at": "2025-01-07T23:17:29+00:00", "name": "Quilt", - "author": "support@phosphor.host", + "author": "support@pterodactyl.io", "description": "Quilt Server. Quilt is a lightweight mod loader for Minecraft based on Fabric Loader, with support for snapshots and fast updates to new versions.", - "features": ["eula", "java_version", "pid_limit", "mod/quilt", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "mod/quilt", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-forge.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-forge.json index 2255956d4..505d28d31 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-forge.json +++ b/database/Seeders/eggs/minecraft/egg-sponge--sponge-forge.json @@ -1,57 +1,65 @@ { - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", - "meta": { - "version": "PTDL_v2", - "update_url": null - }, - "exported_at": "2024-05-07T12:55:54+00:00", - "name": "Sponge (SpongeForge)", - "author": "support@phosphor.host", - "description": "SpongeForge is the SpongeAPI implementation for Forge Loader.", - "features": ["eula", "java_version", "pid_limit", "plugin/sponge", "mod/forge", "mclogs", "subdomain_minecraft"], - "docker_images": { - "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", - "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", - "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", - "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", - "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", - "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!/bin/bash\n# SpongeForge Installation Script\n#\n# SpongeForge is a Forge mod, not a standalone server, so a matching Forge\n# server is installed and the SpongeForge jar is dropped into the mods folder.\n# Everything else mirrors the SpongeVanilla egg: same install image\n# (ghcr.io/ptero-eggs/installers:debian), same variables, same v2 download API.\n#\n# Server Files: /mnt/server\napt update\n# jq/curl ship with the image; openjdk is only needed to run the Forge installer\n# (the installer and its processors are Java 8 bytecode, so any JRE works).\napt install -y jq curl openjdk-17-jdk-headless\n\nmkdir -p /mnt/server\ncd /mnt/server\n\nAPI=\"https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/spongeforge\"\n\n# Resolve the SpongeForge version to install\nif [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"latest\" ] || [ \"${SPONGE_VERSION}\" == \"recommended\" ]; then\n echo -e \"Getting recommended SpongeForge version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?recommended=true&limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n if [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"null\" ]; then\n echo -e \"No recommended build available, falling back to the newest version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n fi\nfi\n\necho -e \"Using SpongeForge version ${SPONGE_VERSION}\"\n\n# Pull the version metadata: Minecraft version, required Forge version and the\n# download URL of the runnable mod jar (the \"universal\" classifier).\nVERSION_DATA=$(curl -s \"${API}/versions/${SPONGE_VERSION}\")\nMC_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.minecraft')\nFORGE_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.forge')\nSV_DL_LINK=$(echo \"${VERSION_DATA}\" | jq -r '.assets[]? | select(.classifier == \"universal\" and .extension == \"jar\") | .downloadUrl')\n\nif [ -z \"${SV_DL_LINK}\" ] || [ \"${SV_DL_LINK}\" == \"null\" ] || [ -z \"${FORGE_VERSION}\" ] || [ \"${FORGE_VERSION}\" == \"null\" ]; then\n echo -e \"ERROR: could not resolve SpongeForge version '${SPONGE_VERSION}'.\"\n echo -e \"Check the version string against https://spongepowered.org/downloads/spongeforge\"\n exit 1\nfi\n\necho -e \"Minecraft ${MC_VERSION}, Forge ${FORGE_VERSION}\"\n\n# --- Install the matching Forge server ---\nFORGE_FULL=\"${MC_VERSION}-${FORGE_VERSION}\"\nDOWNLOAD_LINK=\"https://maven.minecraftforge.net/net/minecraftforge/forge/${FORGE_FULL}/forge-${FORGE_FULL}-installer.jar\"\n\necho -e \"Downloading Forge installer from ${DOWNLOAD_LINK}\"\nif ! curl --output /dev/null --silent --head --fail \"${DOWNLOAD_LINK}\"; then\n echo -e \"ERROR: Forge installer for ${FORGE_FULL} is not available.\"\n exit 2\nfi\ncurl -s -o installer.jar -sS \"${DOWNLOAD_LINK}\"\n\n# Clean any previous Forge args/libraries so up/downgrades work\nrm -rf libraries/net/minecraftforge/forge\nrm -f unix_args.txt\n\necho -e \"Installing Forge server\"\njava -jar installer.jar --installServer || { echo -e \"\\nForge install failed for ${FORGE_FULL}. If memory is set to 0/unlimited, give the install some real memory.\"; exit 4; }\nrm -f installer.jar\n\n# Forge 1.17+ launches through generated JPMS args; symlink them for the egg startup\nif ls libraries/net/minecraftforge/forge/*/unix_args.txt >/dev/null 2>&1; then\n ln -sf libraries/net/minecraftforge/forge/*/unix_args.txt unix_args.txt\nelse\n echo -e \"ERROR: Forge unix_args.txt was not produced; SpongeForge requires Forge 1.17+.\"\n exit 5\nfi\n\n# --- Drop SpongeForge into the mods folder ---\nmkdir -p mods\necho -e \"Downloading SpongeForge ${SPONGE_VERSION} into mods/\"\ncurl -s -o \"mods/spongeforge-${SPONGE_VERSION}.jar\" \"${SV_DL_LINK}\"\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading default server.properties\"\n curl -s -o server.properties https://raw.githubusercontent.com/ptero-eggs/game-eggs/main/minecraft/java/server.properties\nfi\n\necho -e \"Install for SpongeForge is complete\"\n", - "container": "ghcr.io/ptero-eggs/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Sponge Version", - "description": "SpongeForge version to install (e.g. 1.21.10-60.0.1-17.0.0), or \"latest\"/\"recommended\". Also determines which Forge version is installed.", - "env_variable": "SPONGE_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": "required|regex:/^([a-zA-Z0-9.\\-_]+)$/", - "field_type": "text" + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v2", + "update_url": null }, - { - "name": "Server Jar File", - "description": "The name of the Jarfile to use when running SpongeVanilla.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": "required|regex:/^([\\w\\d._-]+)(\\.jar)$/", - "field_type": "text" - } - ] + "exported_at": "2024-05-07T12:55:54+00:00", + "name": "Sponge (SpongeForge)", + "author": "support@phosphor.host", + "description": "SpongeForge is the SpongeAPI implementation for Forge Loader.", + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/sponge", + "mod/forge", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", + "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, + "file_denylist": [], + "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", + "config": { + "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", + "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/bash\n# SpongeForge Installation Script\n#\n# SpongeForge is a Forge mod, not a standalone server, so a matching Forge\n# server is installed and the SpongeForge jar is dropped into the mods folder.\n# Everything else mirrors the SpongeVanilla egg: same install image\n# (ghcr.io/ptero-eggs/installers:debian), same variables, same v2 download API.\n#\n# Server Files: /mnt/server\napt update\n# jq/curl ship with the image; openjdk is only needed to run the Forge installer\n# (the installer and its processors are Java 8 bytecode, so any JRE works).\napt install -y jq curl openjdk-17-jdk-headless\n\nmkdir -p /mnt/server\ncd /mnt/server\n\nAPI=\"https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/spongeforge\"\n\n# Resolve the SpongeForge version to install\nif [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"latest\" ] || [ \"${SPONGE_VERSION}\" == \"recommended\" ]; then\n echo -e \"Getting recommended SpongeForge version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?recommended=true&limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n if [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"null\" ]; then\n echo -e \"No recommended build available, falling back to the newest version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n fi\nfi\n\necho -e \"Using SpongeForge version ${SPONGE_VERSION}\"\n\n# Pull the version metadata: Minecraft version, required Forge version and the\n# download URL of the runnable mod jar (the \"universal\" classifier).\nVERSION_DATA=$(curl -s \"${API}/versions/${SPONGE_VERSION}\")\nMC_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.minecraft')\nFORGE_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.forge')\nSV_DL_LINK=$(echo \"${VERSION_DATA}\" | jq -r '.assets[]? | select(.classifier == \"universal\" and .extension == \"jar\") | .downloadUrl')\n\nif [ -z \"${SV_DL_LINK}\" ] || [ \"${SV_DL_LINK}\" == \"null\" ] || [ -z \"${FORGE_VERSION}\" ] || [ \"${FORGE_VERSION}\" == \"null\" ]; then\n echo -e \"ERROR: could not resolve SpongeForge version '${SPONGE_VERSION}'.\"\n echo -e \"Check the version string against https://spongepowered.org/downloads/spongeforge\"\n exit 1\nfi\n\necho -e \"Minecraft ${MC_VERSION}, Forge ${FORGE_VERSION}\"\n\n# --- Install the matching Forge server ---\nFORGE_FULL=\"${MC_VERSION}-${FORGE_VERSION}\"\nDOWNLOAD_LINK=\"https://maven.minecraftforge.net/net/minecraftforge/forge/${FORGE_FULL}/forge-${FORGE_FULL}-installer.jar\"\n\necho -e \"Downloading Forge installer from ${DOWNLOAD_LINK}\"\nif ! curl --output /dev/null --silent --head --fail \"${DOWNLOAD_LINK}\"; then\n echo -e \"ERROR: Forge installer for ${FORGE_FULL} is not available.\"\n exit 2\nfi\ncurl -s -o installer.jar -sS \"${DOWNLOAD_LINK}\"\n\n# Clean any previous Forge args/libraries so up/downgrades work\nrm -rf libraries/net/minecraftforge/forge\nrm -f unix_args.txt\n\necho -e \"Installing Forge server\"\njava -jar installer.jar --installServer || { echo -e \"\\nForge install failed for ${FORGE_FULL}. If memory is set to 0/unlimited, give the install some real memory.\"; exit 4; }\nrm -f installer.jar\n\n# Forge 1.17+ launches through generated JPMS args; symlink them for the egg startup\nif ls libraries/net/minecraftforge/forge/*/unix_args.txt >/dev/null 2>&1; then\n ln -sf libraries/net/minecraftforge/forge/*/unix_args.txt unix_args.txt\nelse\n echo -e \"ERROR: Forge unix_args.txt was not produced; SpongeForge requires Forge 1.17+.\"\n exit 5\nfi\n\n# --- Drop SpongeForge into the mods folder ---\nmkdir -p mods\necho -e \"Downloading SpongeForge ${SPONGE_VERSION} into mods/\"\ncurl -s -o \"mods/spongeforge-${SPONGE_VERSION}.jar\" \"${SV_DL_LINK}\"\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading default server.properties\"\n curl -s -o server.properties https://raw.githubusercontent.com/ptero-eggs/game-eggs/main/minecraft/java/server.properties\nfi\n\necho -e \"Install for SpongeForge is complete\"\n", + "container": "ghcr.io/ptero-eggs/installers:debian", + "entrypoint": "bash" + } + }, + "variables": [ + { + "name": "Sponge Version", + "description": "SpongeForge version to install (e.g. 1.21.10-60.0.1-17.0.0), or \"latest\"/\"recommended\". Also determines which Forge version is installed.", + "env_variable": "SPONGE_VERSION", + "default_value": "latest", + "user_viewable": true, + "user_editable": true, + "rules": "required|regex:/^([a-zA-Z0-9.\\-_]+)$/", + "field_type": "text" + }, + { + "name": "Server Jar File", + "description": "The name of the Jarfile to use when running SpongeVanilla.", + "env_variable": "SERVER_JARFILE", + "default_value": "server.jar", + "user_viewable": true, + "user_editable": true, + "rules": "required|regex:/^([\\w\\d._-]+)(\\.jar)$/", + "field_type": "text" + } + ] } diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-neo.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-neo.json index 17380216e..aa697a403 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-neo.json +++ b/database/Seeders/eggs/minecraft/egg-sponge--sponge-neo.json @@ -1,57 +1,65 @@ { - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", - "meta": { - "version": "PTDL_v2", - "update_url": null - }, - "exported_at": "2024-05-07T12:55:54+00:00", - "name": "Sponge (SpongeNeo)", - "author": "support@phosphor.host", - "description": "SpongeNeo is the SpongeAPI implementation for Neoforge Loader.", - "features": ["eula", "java_version", "pid_limit", "plugin/sponge", "mod/neoforge", "mclogs", "subdomain_minecraft"], - "docker_images": { - "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", - "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", - "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", - "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", - "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", - "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!/bin/bash\n# SpongeNeo Installation Script\n#\n# SpongeNeo is a NeoForge mod, not a standalone server, so a matching NeoForge\n# server is installed and the SpongeNeo jar is dropped into the mods folder.\n# Everything else mirrors the SpongeVanilla egg: same install image\n# (ghcr.io/ptero-eggs/installers:debian), same variables, same v2 download API.\n#\n# Server Files: /mnt/server\napt update\n# jq/curl ship with the image; openjdk is only needed to run the NeoForge\n# installer (installer + processors are Java 8 bytecode, so any JRE works).\napt install -y jq curl openjdk-17-jdk-headless\n\nmkdir -p /mnt/server\ncd /mnt/server\n\nAPI=\"https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/spongeneo\"\n\n# Resolve the SpongeNeo version to install\nif [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"latest\" ] || [ \"${SPONGE_VERSION}\" == \"recommended\" ]; then\n echo -e \"Getting recommended SpongeNeo version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?recommended=true&limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n if [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"null\" ]; then\n echo -e \"No recommended build available, falling back to the newest version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n fi\nfi\n\necho -e \"Using SpongeNeo version ${SPONGE_VERSION}\"\n\n# Pull the version metadata: Minecraft version, required NeoForge version and the\n# download URL of the runnable mod jar (the \"universal\" classifier).\nVERSION_DATA=$(curl -s \"${API}/versions/${SPONGE_VERSION}\")\nMC_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.minecraft')\nNEO_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.neoforge')\nSV_DL_LINK=$(echo \"${VERSION_DATA}\" | jq -r '.assets[]? | select(.classifier == \"universal\" and .extension == \"jar\") | .downloadUrl')\n\nif [ -z \"${SV_DL_LINK}\" ] || [ \"${SV_DL_LINK}\" == \"null\" ] || [ -z \"${NEO_VERSION}\" ] || [ \"${NEO_VERSION}\" == \"null\" ]; then\n echo -e \"ERROR: could not resolve SpongeNeo version '${SPONGE_VERSION}'.\"\n echo -e \"Check the version string against https://spongepowered.org/downloads/spongeneo\"\n exit 1\nfi\n\necho -e \"Minecraft ${MC_VERSION}, NeoForge ${NEO_VERSION}\"\n\n# --- Install the matching NeoForge server ---\nDOWNLOAD_LINK=\"https://maven.neoforged.net/releases/net/neoforged/neoforge/${NEO_VERSION}/neoforge-${NEO_VERSION}-installer.jar\"\n\necho -e \"Downloading NeoForge installer from ${DOWNLOAD_LINK}\"\nif ! curl --output /dev/null --silent --head --fail \"${DOWNLOAD_LINK}\"; then\n echo -e \"ERROR: NeoForge installer for ${NEO_VERSION} is not available.\"\n exit 2\nfi\ncurl -s -o installer.jar -sS \"${DOWNLOAD_LINK}\"\n\n# Clean any previous NeoForge args/libraries so up/downgrades work\nrm -rf libraries/net/neoforged/neoforge\nrm -f unix_args.txt\n\necho -e \"Installing NeoForge server\"\njava -jar installer.jar --installServer || { echo -e \"\\nNeoForge install failed for ${NEO_VERSION}. If memory is set to 0/unlimited, give the install some real memory.\"; exit 4; }\nrm -f installer.jar\n\n# NeoForge launches through generated JPMS args; symlink them for the egg startup\nif ls libraries/net/neoforged/neoforge/*/unix_args.txt >/dev/null 2>&1; then\n ln -sf libraries/net/neoforged/neoforge/*/unix_args.txt unix_args.txt\nelse\n echo -e \"ERROR: NeoForge unix_args.txt was not produced.\"\n exit 5\nfi\n\n# --- Drop SpongeNeo into the mods folder ---\nmkdir -p mods\necho -e \"Downloading SpongeNeo ${SPONGE_VERSION} into mods/\"\ncurl -s -o \"mods/spongeneo-${SPONGE_VERSION}.jar\" \"${SV_DL_LINK}\"\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading default server.properties\"\n curl -s -o server.properties https://raw.githubusercontent.com/ptero-eggs/game-eggs/main/minecraft/java/server.properties\nfi\n\necho -e \"Install for SpongeNeo is complete\"\n", - "container": "ghcr.io/ptero-eggs/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Sponge Version", - "description": "SpongeNeo version to install (e.g. 1.21.10-21.10.64-17.0.0), or \"latest\"/\"recommended\". Also determines which NeoForge version is installed.", - "env_variable": "SPONGE_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": "required|regex:/^([a-zA-Z0-9.\\-_]+)$/", - "field_type": "text" + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v2", + "update_url": null }, - { - "name": "Server Jar File", - "description": "The name of the Jarfile to use when running SpongeVanilla.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": "required|regex:/^([\\w\\d._-]+)(\\.jar)$/", - "field_type": "text" - } - ] + "exported_at": "2024-05-07T12:55:54+00:00", + "name": "Sponge (SpongeNeo)", + "author": "support@phosphor.host", + "description": "SpongeNeo is the SpongeAPI implementation for Neoforge Loader.", + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/sponge", + "mod/neoforge", + "mclogs", + "subdomain_minecraft" + ], + "docker_images": { + "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", + "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, + "file_denylist": [], + "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", + "config": { + "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", + "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!/bin/bash\n# SpongeNeo Installation Script\n#\n# SpongeNeo is a NeoForge mod, not a standalone server, so a matching NeoForge\n# server is installed and the SpongeNeo jar is dropped into the mods folder.\n# Everything else mirrors the SpongeVanilla egg: same install image\n# (ghcr.io/ptero-eggs/installers:debian), same variables, same v2 download API.\n#\n# Server Files: /mnt/server\napt update\n# jq/curl ship with the image; openjdk is only needed to run the NeoForge\n# installer (installer + processors are Java 8 bytecode, so any JRE works).\napt install -y jq curl openjdk-17-jdk-headless\n\nmkdir -p /mnt/server\ncd /mnt/server\n\nAPI=\"https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/spongeneo\"\n\n# Resolve the SpongeNeo version to install\nif [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"latest\" ] || [ \"${SPONGE_VERSION}\" == \"recommended\" ]; then\n echo -e \"Getting recommended SpongeNeo version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?recommended=true&limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n if [ -z \"${SPONGE_VERSION}\" ] || [ \"${SPONGE_VERSION}\" == \"null\" ]; then\n echo -e \"No recommended build available, falling back to the newest version\"\n SPONGE_VERSION=$(curl -s \"${API}/versions?limit=1&offset=0\" | jq -r '.artifacts | keys[0]')\n fi\nfi\n\necho -e \"Using SpongeNeo version ${SPONGE_VERSION}\"\n\n# Pull the version metadata: Minecraft version, required NeoForge version and the\n# download URL of the runnable mod jar (the \"universal\" classifier).\nVERSION_DATA=$(curl -s \"${API}/versions/${SPONGE_VERSION}\")\nMC_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.minecraft')\nNEO_VERSION=$(echo \"${VERSION_DATA}\" | jq -r '.tags.neoforge')\nSV_DL_LINK=$(echo \"${VERSION_DATA}\" | jq -r '.assets[]? | select(.classifier == \"universal\" and .extension == \"jar\") | .downloadUrl')\n\nif [ -z \"${SV_DL_LINK}\" ] || [ \"${SV_DL_LINK}\" == \"null\" ] || [ -z \"${NEO_VERSION}\" ] || [ \"${NEO_VERSION}\" == \"null\" ]; then\n echo -e \"ERROR: could not resolve SpongeNeo version '${SPONGE_VERSION}'.\"\n echo -e \"Check the version string against https://spongepowered.org/downloads/spongeneo\"\n exit 1\nfi\n\necho -e \"Minecraft ${MC_VERSION}, NeoForge ${NEO_VERSION}\"\n\n# --- Install the matching NeoForge server ---\nDOWNLOAD_LINK=\"https://maven.neoforged.net/releases/net/neoforged/neoforge/${NEO_VERSION}/neoforge-${NEO_VERSION}-installer.jar\"\n\necho -e \"Downloading NeoForge installer from ${DOWNLOAD_LINK}\"\nif ! curl --output /dev/null --silent --head --fail \"${DOWNLOAD_LINK}\"; then\n echo -e \"ERROR: NeoForge installer for ${NEO_VERSION} is not available.\"\n exit 2\nfi\ncurl -s -o installer.jar -sS \"${DOWNLOAD_LINK}\"\n\n# Clean any previous NeoForge args/libraries so up/downgrades work\nrm -rf libraries/net/neoforged/neoforge\nrm -f unix_args.txt\n\necho -e \"Installing NeoForge server\"\njava -jar installer.jar --installServer || { echo -e \"\\nNeoForge install failed for ${NEO_VERSION}. If memory is set to 0/unlimited, give the install some real memory.\"; exit 4; }\nrm -f installer.jar\n\n# NeoForge launches through generated JPMS args; symlink them for the egg startup\nif ls libraries/net/neoforged/neoforge/*/unix_args.txt >/dev/null 2>&1; then\n ln -sf libraries/net/neoforged/neoforge/*/unix_args.txt unix_args.txt\nelse\n echo -e \"ERROR: NeoForge unix_args.txt was not produced.\"\n exit 5\nfi\n\n# --- Drop SpongeNeo into the mods folder ---\nmkdir -p mods\necho -e \"Downloading SpongeNeo ${SPONGE_VERSION} into mods/\"\ncurl -s -o \"mods/spongeneo-${SPONGE_VERSION}.jar\" \"${SV_DL_LINK}\"\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading default server.properties\"\n curl -s -o server.properties https://raw.githubusercontent.com/ptero-eggs/game-eggs/main/minecraft/java/server.properties\nfi\n\necho -e \"Install for SpongeNeo is complete\"\n", + "container": "ghcr.io/ptero-eggs/installers:debian", + "entrypoint": "bash" + } + }, + "variables": [ + { + "name": "Sponge Version", + "description": "SpongeNeo version to install (e.g. 1.21.10-21.10.64-17.0.0), or \"latest\"/\"recommended\". Also determines which NeoForge version is installed.", + "env_variable": "SPONGE_VERSION", + "default_value": "latest", + "user_viewable": true, + "user_editable": true, + "rules": "required|regex:/^([a-zA-Z0-9.\\-_]+)$/", + "field_type": "text" + }, + { + "name": "Server Jar File", + "description": "The name of the Jarfile to use when running SpongeVanilla.", + "env_variable": "SERVER_JARFILE", + "default_value": "server.jar", + "user_viewable": true, + "user_editable": true, + "rules": "required|regex:/^([\\w\\d._-]+)(\\.jar)$/", + "field_type": "text" + } + ] } diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json index b5811d07a..daed0a56e 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json +++ b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json @@ -8,7 +8,14 @@ "name": "Sponge (SpongeVanilla)", "author": "support@pterodactyl.io", "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", - "features": ["eula", "java_version", "pid_limit", "plugin/sponge", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/sponge", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json index 3ac29bea8..994c95bfd 100644 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json @@ -8,7 +8,14 @@ "name": "Vanilla Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", - "features": ["eula", "java_version", "pid_limit", "vanilla", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "vanilla", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io/pyrodactyl-oss/images:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/minecraft/egg-velocity.json b/database/Seeders/eggs/minecraft/egg-velocity.json index d6232f466..0c25071c7 100644 --- a/database/Seeders/eggs/minecraft/egg-velocity.json +++ b/database/Seeders/eggs/minecraft/egg-velocity.json @@ -6,9 +6,16 @@ }, "exported_at": "2026-03-07T23:02:39+00:00", "name": "Velocity", - "author": "hostmaster@waterfallgaming.net", + "author": "support@pterodactyl.io", "description": "Velocity is a Minecraft server proxy with unparalleled server support, scalability, and flexibility.", - "features": ["eula", "java_version", "pid_limit", "plugin/velocity", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/velocity", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", "Java 22": "ghcr.io\/pterodactyl\/yolks:java_22", diff --git a/database/Seeders/eggs/minecraft/egg-waterfall.json b/database/Seeders/eggs/minecraft/egg-waterfall.json index a542ede0c..d23cfa6bf 100644 --- a/database/Seeders/eggs/minecraft/egg-waterfall.json +++ b/database/Seeders/eggs/minecraft/egg-waterfall.json @@ -8,7 +8,14 @@ "name": "Waterfall", "author": "support@phosphor.host", "description": "Waterfall is a fork of the well-known BungeeCord server proxy.", - "features": ["eula", "java_version", "pid_limit", "plugin/waterfall", "mclogs", "subdomain_minecraft"], + "features": [ + "eula", + "java_version", + "pid_limit", + "plugin/waterfall", + "mclogs", + "subdomain_minecraft" + ], "docker_images": { "Java 25": "ghcr.io\/pterodactyl\/yolks:java_25", "Java 21": "ghcr.io\/pterodactyl\/yolks:java_21", diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json index 4f151d370..2e0631ccf 100644 --- a/database/Seeders/eggs/rust/egg-rust.json +++ b/database/Seeders/eggs/rust/egg-rust.json @@ -8,7 +8,10 @@ "name": "Rust", "author": "support@pterodactyl.io", "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", - "features": ["steam_disk_space", "subdomain_rust"], + "features": [ + "steam_disk_space", + "subdomain_rust" + ], "docker_images": { "ghcr.io\/pterodactyl\/games:rust": "ghcr.io\/pterodactyl\/games:rust" }, diff --git a/database/Seeders/eggs/source-engine/egg-ark--survival-evolved.json b/database/Seeders/eggs/source-engine/egg-ark--survival-evolved.json index 8613609a5..660ebd14e 100644 --- a/database/Seeders/eggs/source-engine/egg-ark--survival-evolved.json +++ b/database/Seeders/eggs/source-engine/egg-ark--survival-evolved.json @@ -8,8 +8,12 @@ "name": "Ark: Survival Evolved", "author": "dev@shepper.fr", "description": "As a man or woman stranded, naked, freezing, and starving on the unforgiving shores of a mysterious island called ARK, use your skill and cunning to kill or tame and ride the plethora of leviathan dinosaurs and other primeval creatures roaming the land. Hunt, harvest resources, craft items, grow crops, research technologies, and build shelters to withstand the elements and store valuables, all while teaming up with (or preying upon) hundreds of other players to survive, dominate... and escape! \u2014 Gamepedia: ARK", - "features": ["steam_disk_space"], - "images": ["quay.io\/parkervcp\/pterodactyl-images:debian_source"], + "features": [ + "steam_disk_space" + ], + "images": [ + "quay.io\/parkervcp\/pterodactyl-images:debian_source" + ], "file_denylist": [], "startup": "rmv() { echo -e \"stopping server\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c saveworld && rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c DoExit; }; trap rmv 15; cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName=\"{{SESSION_NAME}}\"?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled=True$( [ \"$BATTLE_EYE\" == \"1\" ] || printf %s ' -NoBattlEye' ) -server {{ARGS}} -log & until echo \"waiting for rcon connection...\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD}; do sleep 5; done", "config": { diff --git a/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json b/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json index 99386245b..7c9a9779f 100644 --- a/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json +++ b/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json @@ -8,8 +8,13 @@ "name": "Counter-Strike: Global Offensive", "author": "support@pterodactyl.io", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", - "features": ["gsl_token", "steam_disk_space"], - "images": ["ghcr.io\/pterodactyl\/games:source"], + "features": [ + "gsl_token", + "steam_disk_space" + ], + "images": [ + "ghcr.io\/pterodactyl\/games:source" + ], "file_denylist": [], "startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", "config": { diff --git a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json index 1a9f32f38..6c93175f5 100644 --- a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json +++ b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json @@ -8,8 +8,12 @@ "name": "Custom Source Engine Game", "author": "support@pterodactyl.io", "description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.", - "features": ["steam_disk_space"], - "images": ["ghcr.io\/pterodactyl\/games:source"], + "features": [ + "steam_disk_space" + ], + "images": [ + "ghcr.io\/pterodactyl\/games:source" + ], "file_denylist": [], "startup": ".\/srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart", "config": { diff --git a/database/Seeders/eggs/source-engine/egg-garrys-mod.json b/database/Seeders/eggs/source-engine/egg-garrys-mod.json index b70951927..32829174d 100644 --- a/database/Seeders/eggs/source-engine/egg-garrys-mod.json +++ b/database/Seeders/eggs/source-engine/egg-garrys-mod.json @@ -8,8 +8,13 @@ "name": "Garrys Mod", "author": "support@pterodactyl.io", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", - "features": ["gsl_token", "steam_disk_space"], - "images": ["ghcr.io\/pterodactyl\/games:source"], + "features": [ + "gsl_token", + "steam_disk_space" + ], + "images": [ + "ghcr.io\/pterodactyl\/games:source" + ], "file_denylist": [], "startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ \"$LUA_REFRESH\" == \"1\" ] || printf %s '-disableluarefresh' )", "config": { diff --git a/database/Seeders/eggs/source-engine/egg-insurgency.json b/database/Seeders/eggs/source-engine/egg-insurgency.json index 55e588f08..69d182f3d 100644 --- a/database/Seeders/eggs/source-engine/egg-insurgency.json +++ b/database/Seeders/eggs/source-engine/egg-insurgency.json @@ -8,8 +8,12 @@ "name": "Insurgency", "author": "support@pterodactyl.io", "description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.", - "features": ["steam_disk_space"], - "images": ["ghcr.io\/pterodactyl\/games:source"], + "features": [ + "steam_disk_space" + ], + "images": [ + "ghcr.io\/pterodactyl\/games:source" + ], "file_denylist": [], "startup": ".\/srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart", "config": { diff --git a/database/Seeders/eggs/source-engine/egg-team-fortress2.json b/database/Seeders/eggs/source-engine/egg-team-fortress2.json index 6864a8f54..6785984ed 100644 --- a/database/Seeders/eggs/source-engine/egg-team-fortress2.json +++ b/database/Seeders/eggs/source-engine/egg-team-fortress2.json @@ -8,8 +8,13 @@ "name": "Team Fortress 2", "author": "support@pterodactyl.io", "description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.", - "features": ["gsl_token", "steam_disk_space"], - "images": ["ghcr.io\/pterodactyl\/games:source"], + "features": [ + "gsl_token", + "steam_disk_space" + ], + "images": [ + "ghcr.io\/pterodactyl\/games:source" + ], "file_denylist": [], "startup": ".\/srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", "config": { diff --git a/database/Seeders/eggs/vintage-story/egg-vintage-story.json b/database/Seeders/eggs/vintage-story/egg-vintage-story.json index 2b95581a6..277a67b78 100644 --- a/database/Seeders/eggs/vintage-story/egg-vintage-story.json +++ b/database/Seeders/eggs/vintage-story/egg-vintage-story.json @@ -8,7 +8,9 @@ "name": "Vintage Story", "author": "mail@wuffy.eu", "description": "Vintage Story is an uncompromising wilderness survival sandbox game inspired by lovecraftian horror themes. Find yourself in a ruined world reclaimed by nature and permeated by unnerving temporal disturbances. Relive the advent of human civilization, or take your own path.", - "features": ["subdomain_vintagestory"], + "features": [ + "subdomain_vintagestory" + ], "docker_images": { "Dotnet 7": "ghcr.io/ptero-eggs/yolks:dotnet_7", "Dotnet 8": "ghcr.io/ptero-eggs/yolks:dotnet_8" diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json index 0c2c080e5..9aa6e25e3 100644 --- a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json +++ b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json @@ -8,7 +8,9 @@ "name": "Teamspeak3 Server", "author": "support@pterodactyl.io", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", - "features": ["subdomain_teamspeak"], + "features": [ + "subdomain_teamspeak" + ], "docker_images": { "ghcr.io\/pterodactyl\/yolks:debian": "ghcr.io\/pterodactyl\/yolks:debian" }, diff --git a/database/migrations/2026_07_09_000001_add_is_local_to_s3_table.php b/database/migrations/2026_07_09_000001_add_is_local_to_s3_table.php new file mode 100644 index 000000000..af5755bc3 --- /dev/null +++ b/database/migrations/2026_07_09_000001_add_is_local_to_s3_table.php @@ -0,0 +1,23 @@ +boolean('is_local')->default(false)->after('enabled'); + $table->string('minio_instance_url')->nullable()->after('is_local'); + }); + } + + public function down(): void + { + Schema::table('s3', function (Blueprint $table) { + $table->dropColumn(['is_local', 'minio_instance_url']); + }); + } +}; diff --git a/package.json b/package.json index e574741ca..0ccd2aa35 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@hugeicons/react": "^1.1.1", "@lezer/highlight": "^1.2.1", "@marsidev/react-turnstile": "^1.2.0", + "@nivo/bar": "^0.99.0", "@preact/signals-react": "^3.2.1", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-context-menu": "^2.2.15", @@ -58,6 +59,7 @@ "laravel-vite-plugin": "^2.0.0", "million": "^3.1.11", "motion": "^12.23.12", + "nuqs": "^2.9.0", "pathe": "^2.0.3", "qrcode.react": "^4.2.0", "radix-ui": "^1.4.2", @@ -66,7 +68,6 @@ "react-dom": "^19.1.1", "react-fast-compare": "^3.2.2", "react-router-dom": "^7.7.1", - "recharts": "^3.9.1", "sockette": "^2.0.6", "sonner": "^2.0.7", "styled-components": "^6.1.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4afb6508a..94ab82865 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@marsidev/react-turnstile': specifier: ^1.2.0 version: 1.5.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/bar': + specifier: ^0.99.0 + version: 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@preact/signals-react': specifier: ^3.2.1 version: 3.10.2(react@19.2.7) @@ -161,6 +164,9 @@ importers: motion: specifier: ^12.23.12 version: 12.42.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + nuqs: + specifier: ^2.9.0 + version: 2.9.0(react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) pathe: specifier: ^2.0.3 version: 2.0.3 @@ -185,9 +191,6 @@ importers: react-router-dom: specifier: ^7.7.1 version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - recharts: - specifier: ^3.9.1 - version: 3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) sockette: specifier: ^2.0.6 version: 2.0.6 @@ -861,6 +864,57 @@ packages: react: ^17.0.2 || ^18.0.0 || ^19.0 react-dom: ^17.0.2 || ^18.0.0 || ^19.0 + '@nivo/annotations@0.99.0': + resolution: {integrity: sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/axes@0.99.0': + resolution: {integrity: sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/bar@0.99.0': + resolution: {integrity: sha512-9yfMn7H6UF/TqtCwVZ/vihVAXUff9wWvSaeF2Z1DCfgr5S07qs31Qb2p0LZA+YgCWpaU7zqkeb3VZ4WCpZbrDA==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/canvas@0.99.0': + resolution: {integrity: sha512-UxA8zb+NPwqmNm81hoyUZSMAikgjU1ukLf4KybVNyV8ejcJM+BUFXsb8DxTcLdt4nmCFHqM56GaJQv2hnAHmzg==} + + '@nivo/colors@0.99.0': + resolution: {integrity: sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/core@0.99.0': + resolution: {integrity: sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/legends@0.99.0': + resolution: {integrity: sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/scales@0.99.0': + resolution: {integrity: sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w==} + + '@nivo/text@0.99.0': + resolution: {integrity: sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/theming@0.99.0': + resolution: {integrity: sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + + '@nivo/tooltip@0.99.0': + resolution: {integrity: sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg==} + peerDependencies: + react: ^16.14 || ^17.0 || ^18.0 || ^19.0 + '@preact/signals-core@1.14.3': resolution: {integrity: sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw==} @@ -1571,22 +1625,38 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@react-spring/animated@10.1.2': + resolution: {integrity: sha512-yAsQ/bbp6+vko7WNCI1M00c6KLE9XKTGCrgRhQqS4JcK3oF5qBV4rHYrQEprvEvYXxt+1H5FsLS2eVValEPfFw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/core@10.1.2': + resolution: {integrity: sha512-lPGOAg0V+PV3ucopOajD+YCxoe8twALqAeG4c/+sqVjBsyUNNdx8qfz/DcNSPKA8PV3+AyQELlCxlDfap9cmBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/rafz@10.1.2': + resolution: {integrity: sha512-KC6vSFZyPnRJ2rXqipV9QqR4SaYbYXjvKfdYKWipNK2mWuV79gr20WmpVUOsTiEHGRY3WSOdWCHN+P9Pdaqb7Q==} + + '@react-spring/shared@10.1.2': + resolution: {integrity: sha512-47/8bNQ/o0uEmxEnPBuERlC29VSqiTn5P9ln9tUMSVkYKYxBtouYE686F5kAGj+eHRPoNCw7drxWE9nv2d1LMw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/types@10.1.2': + resolution: {integrity: sha512-G4CWowmVPz+rDG1y9QRVq/prZNxiNwQaHC0kgT/MgY6jAGgdRgTV4VThpvJDwWvUitk3xZB4soP4d36fjeQ09g==} + + '@react-spring/web@10.1.2': + resolution: {integrity: sha512-KxDB3zaDqy9qFsu7fdxjyraAxweHH4k5TW5WGT/OuMK6hKaxSDfhrQfRBzfFaSVvMBf+NghbJFanllV4ia6i7A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@react-types/shared@3.36.0': resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@reduxjs/toolkit@2.12.0': - resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1740,11 +1810,8 @@ packages: '@sinclair/typebox@0.34.52': resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} '@swc/core-darwin-arm64@1.15.43': resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} @@ -1976,14 +2043,11 @@ packages: cpu: [arm64] os: [win32] - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-format@1.4.5': + resolution: {integrity: sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==} '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} @@ -1991,18 +2055,27 @@ packages: '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + '@types/d3-scale@4.0.9': resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} '@types/d3-shape@3.1.8': resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + '@types/d3-time-format@2.3.4': + resolution: {integrity: sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==} + + '@types/d3-time-format@3.0.4': + resolution: {integrity: sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==} + + '@types/d3-time@1.1.4': + resolution: {integrity: sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==} + '@types/d3-time@3.0.4': resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/debounce@1.2.4': resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==} @@ -2052,9 +2125,6 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -2207,6 +2277,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -2215,9 +2288,8 @@ packages: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} + d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} d3-format@3.1.2: resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} @@ -2231,6 +2303,10 @@ packages: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + d3-scale@4.0.2: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} @@ -2239,18 +2315,23 @@ packages: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} + d3-time-format@3.0.0: + resolution: {integrity: sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==} + d3-time-format@4.1.0: resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} engines: {node: '>=12'} + d3-time@1.1.0: + resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} + + d3-time@2.1.1: + resolution: {integrity: sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==} + d3-time@3.1.0: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - date-fns@4.4.0: resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} @@ -2267,9 +2348,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - deepmerge-ts@7.1.5: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} @@ -2345,9 +2423,6 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.49.0: - resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -2364,9 +2439,6 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2476,9 +2548,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - immer@11.1.9: - resolution: {integrity: sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==} - immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -2486,6 +2555,9 @@ packages: resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} engines: {node: '>= 0.10'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -2700,6 +2772,27 @@ packages: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} + nuqs@2.9.0: + resolution: {integrity: sha512-1ckQBhVHaQYjLZ3kWhhH40A32lPJ7TNTQMa2T+SUvl5azyVt7swCQfUXt9xOXJV3YidWq8VHIHcMzVAJ0knRsw==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^5 || ^6 || ^7 || ^8 + react-router-dom: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2814,18 +2907,6 @@ packages: react-is@19.2.7: resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} - react-redux@9.3.0: - resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2878,6 +2959,12 @@ packages: '@types/react': optional: true + react-virtualized-auto-sizer@1.0.26: + resolution: {integrity: sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -2885,14 +2972,6 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - recharts@3.9.1: - resolution: {integrity: sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -2905,9 +2984,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - reselect@5.2.0: - resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -3039,9 +3115,6 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} @@ -3137,9 +3210,6 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite-plugin-full-reload@1.2.0: resolution: {integrity: sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==} @@ -3928,6 +3998,138 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + '@nivo/annotations@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/colors': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + lodash: 4.18.1 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/axes@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/scales': 0.99.0 + '@nivo/text': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/d3-format': 1.4.5 + '@types/d3-time-format': 2.3.4 + d3-format: 1.4.5 + d3-time-format: 3.0.0 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/bar@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/annotations': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/axes': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/canvas': 0.99.0 + '@nivo/colors': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/legends': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/scales': 0.99.0 + '@nivo/text': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@nivo/tooltip': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + lodash: 4.18.1 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/canvas@0.99.0': {} + + '@nivo/colors@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@types/d3-color': 3.1.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + d3-color: 3.1.0 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + lodash: 4.18.1 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/core@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/theming': 0.99.0(react@19.2.7) + '@nivo/tooltip': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/d3-shape': 3.1.8 + d3-color: 3.1.0 + d3-format: 1.4.5 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-shape: 3.2.0 + d3-time-format: 3.0.0 + lodash: 4.18.1 + react: 19.2.7 + react-virtualized-auto-sizer: 1.0.26(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + use-debounce: 10.1.1(react@19.2.7) + transitivePeerDependencies: + - react-dom + + '@nivo/legends@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/colors': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/text': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@types/d3-scale': 4.0.9 + d3-scale: 4.0.2 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/scales@0.99.0': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-time': 1.1.4 + '@types/d3-time-format': 3.0.4 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-time: 1.1.0 + d3-time-format: 3.0.0 + lodash: 4.18.1 + + '@nivo/text@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@nivo/theming@0.99.0(react@19.2.7)': + dependencies: + lodash: 4.18.1 + react: 19.2.7 + + '@nivo/tooltip@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@nivo/core': 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@nivo/theming': 0.99.0(react@19.2.7) + '@react-spring/web': 10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + transitivePeerDependencies: + - react-dom + '@preact/signals-core@1.14.3': {} '@preact/signals-react@3.10.2(react@19.2.7)': @@ -4695,21 +4897,42 @@ snapshots: react-aria: 3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-dom: 19.2.7(react@19.2.7) - '@react-types/shared@3.36.0(react@19.2.7)': + '@react-spring/animated@10.1.2(react@19.2.7)': dependencies: + '@react-spring/shared': 10.1.2(react@19.2.7) + '@react-spring/types': 10.1.2 react: 19.2.7 - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': + '@react-spring/core@10.1.2(react@19.2.7)': + dependencies: + '@react-spring/animated': 10.1.2(react@19.2.7) + '@react-spring/shared': 10.1.2(react@19.2.7) + '@react-spring/types': 10.1.2 + react: 19.2.7 + + '@react-spring/rafz@10.1.2': {} + + '@react-spring/shared@10.1.2(react@19.2.7)': + dependencies: + '@react-spring/rafz': 10.1.2 + '@react-spring/types': 10.1.2 + react: 19.2.7 + + '@react-spring/types@10.1.2': {} + + '@react-spring/web@10.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@react-spring/animated': 10.1.2(react@19.2.7) + '@react-spring/core': 10.1.2(react@19.2.7) + '@react-spring/shared': 10.1.2(react@19.2.7) + '@react-spring/types': 10.1.2 + csstype: 3.2.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@react-types/shared@3.36.0(react@19.2.7)': dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.9 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.2.0 - optionalDependencies: react: 19.2.7 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -4798,9 +5021,7 @@ snapshots: '@sinclair/typebox@0.34.52': {} - '@standard-schema/spec@1.1.0': {} - - '@standard-schema/utils@0.3.0': {} + '@standard-schema/spec@1.0.0': {} '@swc/core-darwin-arm64@1.15.43': optional: true @@ -4966,11 +5187,9 @@ snapshots: '@turbo/windows-arm64@2.10.2': optional: true - '@types/d3-array@3.2.2': {} - '@types/d3-color@3.1.3': {} - '@types/d3-ease@3.0.2': {} + '@types/d3-format@1.4.5': {} '@types/d3-interpolate@3.0.4': dependencies: @@ -4978,6 +5197,8 @@ snapshots: '@types/d3-path@3.1.1': {} + '@types/d3-scale-chromatic@3.1.0': {} + '@types/d3-scale@4.0.9': dependencies: '@types/d3-time': 3.0.4 @@ -4986,9 +5207,13 @@ snapshots: dependencies: '@types/d3-path': 3.1.1 - '@types/d3-time@3.0.4': {} + '@types/d3-time-format@2.3.4': {} + + '@types/d3-time-format@3.0.4': {} - '@types/d3-timer@3.0.2': {} + '@types/d3-time@1.1.4': {} + + '@types/d3-time@3.0.4': {} '@types/debounce@1.2.4': {} @@ -5038,8 +5263,6 @@ snapshots: '@types/stack-utils@2.0.3': {} - '@types/use-sync-external-store@0.0.6': {} - '@types/uuid@10.0.0': {} '@types/yargs-parser@21.0.3': {} @@ -5190,13 +5413,17 @@ snapshots: csstype@3.2.3: {} + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + d3-array@3.2.4: dependencies: internmap: 2.0.3 d3-color@3.1.0: {} - d3-ease@3.0.1: {} + d3-format@1.4.5: {} d3-format@3.1.2: {} @@ -5206,6 +5433,11 @@ snapshots: d3-path@3.1.0: {} + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + d3-scale@4.0.2: dependencies: d3-array: 3.2.4 @@ -5218,16 +5450,24 @@ snapshots: dependencies: d3-path: 3.1.0 + d3-time-format@3.0.0: + dependencies: + d3-time: 2.1.1 + d3-time-format@4.1.0: dependencies: d3-time: 3.1.0 + d3-time@1.1.0: {} + + d3-time@2.1.1: + dependencies: + d3-array: 2.12.1 + d3-time@3.1.0: dependencies: d3-array: 3.2.4 - d3-timer@3.0.1: {} - date-fns@4.4.0: {} debounce@2.2.0: {} @@ -5236,8 +5476,6 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js-light@2.5.1: {} - deepmerge-ts@7.1.5: {} deepmerge@2.2.1: {} @@ -5297,8 +5535,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - es-toolkit@1.49.0: {} - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -5334,8 +5570,6 @@ snapshots: estree-walker@2.0.2: {} - eventemitter3@5.0.4: {} - events@3.3.0: {} expect@30.4.1: @@ -5438,12 +5672,12 @@ snapshots: dependencies: react-is: 16.13.1 - immer@11.1.9: {} - immer@9.0.21: {} install@0.13.0: {} + internmap@1.0.1: {} + internmap@2.0.3: {} is-core-module@2.16.2: @@ -5626,6 +5860,14 @@ snapshots: node-releases@2.0.50: {} + nuqs@2.9.0(react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 19.2.7 + optionalDependencies: + react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-router-dom: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + object-assign@4.1.1: {} open@8.4.2: @@ -5787,15 +6029,6 @@ snapshots: react-is@19.2.7: {} - react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.2.7 - use-sync-external-store: 1.6.0(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - redux: 5.0.1 - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): dependencies: react: 19.2.7 @@ -5847,32 +6080,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + react-virtualized-auto-sizer@1.0.26(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react@19.2.7: {} read-cache@1.0.0: dependencies: pify: 2.3.0 - recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.49.0 - eventemitter3: 5.0.4 - immer: 11.1.9 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-is: 19.2.7 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) - reselect: 5.2.0 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.7) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -5881,8 +6099,6 @@ snapshots: require-directory@2.1.1: {} - reselect@5.2.0: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -6011,8 +6227,6 @@ snapshots: tiny-case@1.0.3: {} - tiny-invariant@1.3.3: {} - tiny-warning@1.0.3: {} tinyglobby@0.2.17: @@ -6087,23 +6301,6 @@ snapshots: uuid@11.1.1: {} - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - vite-plugin-full-reload@1.2.0: dependencies: picocolors: 1.1.1 diff --git a/resources/scripts/admin/admin.css b/resources/scripts/admin/admin.css new file mode 100644 index 000000000..7c43e3189 --- /dev/null +++ b/resources/scripts/admin/admin.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "../assets/tailwindcss-animate.css"; diff --git a/resources/scripts/admin/index.tsx b/resources/scripts/admin/index.tsx deleted file mode 100644 index a6a5ea1b2..000000000 --- a/resources/scripts/admin/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis } from 'recharts'; - -interface SystemMetrics { - status: string; - timestamp: string; - metrics: { - uptime: number; - memory: { total: number; used: number; free: number }; - cpu: number; - disk: { total: number; free: number; used: number }; - }; - system: { - php_version: string; - os: string; - hostname: string; - load_average: number[]; - }; -} - -function formatBytes(bytes: number): string { - if (!bytes) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`; -} - -function formatUptime(seconds: number): string { - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return `${days}d ${hours}h ${minutes}m`; -} - -const COLORS = { used: '#52A9FF', free: '#2D5A8A', bg: '#1E3A5A' }; - -function UsageBar({ used, total, label, unit }: { used: number; total: number; label: string; unit?: string }) { - const pct = total > 0 ? (used / total) * 100 : 0; - const data = [{ name: label, used, free: total - used }]; - const isPercent = unit === '%'; - const fmt = (v: number) => (isPercent ? `${v.toFixed(1)}%` : formatBytes(v)); - - return ( -
-
- {label} - - {fmt(used)} / {fmt(total)} - -
- - - - fmt(v)} - /> - - - - -
{pct.toFixed(1)}% used
-
- ); -} - -function LoadGraph({ loads }: { loads: number[] }) { - const data = loads.map((v, i) => ({ name: `${i + 1}m`, value: parseFloat(v.toFixed(2)) })); - - return ( -
-
Load Average
- - - - - - - -
- ); -} - -function AdminDashboard() { - const [data, setData] = useState(null); - - const fetchMetrics = useCallback(() => { - fetch('/api/application/panel/status') - .then((r) => r.json()) - .then(setData) - .catch(() => undefined); - }, []); - - useEffect(() => { - fetchMetrics(); - const interval = setInterval(fetchMetrics, 30000); - return () => clearInterval(interval); - }, [fetchMetrics]); - - if (!data) { - return ( -
- -

Loading system metrics...

-
- ); - } - - const { metrics, system } = data; - - return ( -
-
-
-
- -
-
-
-
-
-
- {formatUptime(metrics.uptime)} -
-
Uptime
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
-

System Info

-
-
- - - {[ - ['Hostname', system.hostname], - ['OS', system.os], - ['PHP Version', system.php_version], - ['CPU Cores', navigator.hardwareConcurrency?.toString() || 'N/A'], - ].map(([k, v]) => ( - - - - - ))} - -
{k}{v}
-
-
-
-
-
-
-

Load Average

-
-
- -
-
-
-
-
- ); -} - -const container = document.getElementById('admin-dashboard'); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/resources/scripts/api/admin/apiKeys.ts b/resources/scripts/api/admin/apiKeys.ts new file mode 100644 index 000000000..145e33d6a --- /dev/null +++ b/resources/scripts/api/admin/apiKeys.ts @@ -0,0 +1,91 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface ApiKeyPermissions { + [key: string]: number; +} + +export interface AdminApiKey { + id: number; + userId: number; + keyType: number; + identifier: string; + memo: string; + allowedIps: string[] | null; + lastUsedAt: string | null; + expiresAt: string | null; + permissions: ApiKeyPermissions; + createdAt: string; + updatedAt: string; +} + +const rawToApiKey = (data: FractalResponseData): AdminApiKey => { + const attrs = data.attributes; + return { + id: attrs.id as number, + userId: attrs.user_id as number, + keyType: attrs.key_type as number, + identifier: attrs.identifier as string, + memo: attrs.memo as string, + allowedIps: attrs.allowed_ips as string[] | null, + lastUsedAt: attrs.last_used_at as string | null, + expiresAt: attrs.expires_at as string | null, + permissions: (attrs.permissions as ApiKeyPermissions) || {}, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getApiKeys = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/api-keys', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToApiKey), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getApiKey = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/api-keys/${id}`) + .then(({ data }) => resolve(rawToApiKey(data))) + .catch(reject); + }); + +export interface CreateApiKeyData { + memo: string; + allowed_ips?: string[]; + expires_at?: string; + [key: string]: unknown; +} + +export const createApiKey = (data: CreateApiKeyData): Promise<{ key: AdminApiKey; token: string }> => + new Promise((resolve, reject) => { + http.post('/api/application/api-keys', data) + .then(({ data: resp }) => + resolve({ + key: rawToApiKey(resp), + token: resp.meta?.token as string, + }), + ) + .catch(reject); + }); + +export const updateApiKey = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/api-keys/${id}`, data) + .then(({ data: resp }) => resolve(rawToApiKey(resp))) + .catch(reject); + }); + +export const deleteApiKey = (id: number): Promise => http.delete(`/api/application/api-keys/${id}`); diff --git a/resources/scripts/api/admin/buckets.ts b/resources/scripts/api/admin/buckets.ts new file mode 100644 index 000000000..8e7847566 --- /dev/null +++ b/resources/scripts/api/admin/buckets.ts @@ -0,0 +1,88 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminBucket { + id: number; + name: string; + description: string | null; + bucketName: string; + endpoint: string | null; + usePathStyleEndpoint: boolean; + isLocal: boolean; + enabled: boolean; + minioInstanceUrl: string | null; + createdAt: string; + updatedAt: string; +} + +const rawToBucket = (data: FractalResponseData): AdminBucket => { + const attrs = data.attributes; + return { + id: attrs.id as number, + name: attrs.name as string, + description: attrs.description as string | null, + bucketName: attrs.bucket_name as string, + endpoint: attrs.endpoint as string | null, + usePathStyleEndpoint: attrs.use_path_style_endpoint as boolean, + isLocal: attrs.is_local as boolean, + enabled: attrs.enabled as boolean, + minioInstanceUrl: attrs.minio_instance_url as string | null, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getBuckets = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/buckets', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToBucket), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getBucket = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/buckets/${id}`) + .then(({ data }) => resolve(rawToBucket(data))) + .catch(reject); + }); + +export interface CreateBucketData { + name: string; + description?: string | null; + access_key: string; + secret_key: string; + endpoint?: string | null; + bucket_name: string; + use_path_style_endpoint?: boolean; + enabled?: boolean; + is_local?: boolean; + minio_instance_url?: string | null; +} + +export const createBucket = (data: CreateBucketData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/buckets', data) + .then(({ data: resp }) => resolve(rawToBucket(resp))) + .catch(reject); + }); + +export const updateBucket = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/buckets/${id}`, data) + .then(({ data: resp }) => resolve(rawToBucket(resp))) + .catch(reject); + }); + +export const deleteBucket = (id: number): Promise => http.delete(`/api/application/buckets/${id}`); diff --git a/resources/scripts/api/admin/databaseHosts.ts b/resources/scripts/api/admin/databaseHosts.ts new file mode 100644 index 000000000..b208ce3e4 --- /dev/null +++ b/resources/scripts/api/admin/databaseHosts.ts @@ -0,0 +1,81 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminDatabaseHost { + id: number; + name: string; + host: string; + port: number; + username: string; + node: number | null; + containerImage?: string; + createdAt: string; + updatedAt: string; +} + +const rawToDatabaseHost = (data: FractalResponseData): AdminDatabaseHost => { + const attrs = data.attributes; + return { + id: attrs.id as number, + name: attrs.name as string, + host: attrs.host as string, + port: attrs.port as number, + username: attrs.username as string, + node: attrs.node as number | null, + containerImage: attrs.container_image as string | undefined, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getDatabaseHosts = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/database-hosts', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToDatabaseHost), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getDatabaseHost = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/database-hosts/${id}`) + .then(({ data }) => resolve(rawToDatabaseHost(data))) + .catch(reject); + }); + +export interface CreateDatabaseHostData { + name: string; + host: string; + port: number; + username: string; + password: string; + container_image?: string; + node_id?: number | null; +} + +export const createDatabaseHost = (data: CreateDatabaseHostData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/database-hosts', data) + .then(({ data: resp }) => resolve(rawToDatabaseHost(resp))) + .catch(reject); + }); + +export const updateDatabaseHost = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/database-hosts/${id}`, data) + .then(({ data: resp }) => resolve(rawToDatabaseHost(resp))) + .catch(reject); + }); + +export const deleteDatabaseHost = (id: number): Promise => http.delete(`/api/application/database-hosts/${id}`); diff --git a/resources/scripts/api/admin/index.ts b/resources/scripts/api/admin/index.ts new file mode 100644 index 000000000..b790e540f --- /dev/null +++ b/resources/scripts/api/admin/index.ts @@ -0,0 +1,32 @@ +import http from '@/api/http'; + +export interface PanelStatus { + status: string; + timestamp: string; + metrics: { + uptime: number; + memory: { total: number; used: number; free: number }; + cpu: number; + disk: { total: number; free: number; used: number }; + }; + system: { + php_version: string; + os: string; + hostname: string; + load_average: number[]; + }; +} + +export const getPanelStatus = (): Promise => + http.get('/api/application/panel/status').then(({ data }) => data); + +export interface Counts { + servers: number; + nodes: number; + users: number; + locations: number; + nests: number; + buckets: number; +} + +export const getAdminCounts = (): Promise => http.get('/api/application/panel/counts').then(({ data }) => data); diff --git a/resources/scripts/api/admin/locations.ts b/resources/scripts/api/admin/locations.ts new file mode 100644 index 000000000..82d03f2d3 --- /dev/null +++ b/resources/scripts/api/admin/locations.ts @@ -0,0 +1,74 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminLocation { + id: number; + uuid: string; + short: string; + long: string; + nodesCount: number; + serversCount: number; + createdAt: string; + updatedAt: string; +} + +const rawToLocation = (data: FractalResponseData): AdminLocation => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + short: attrs.short as string, + long: attrs.long as string, + nodesCount: (attrs.nodes_count as number) || 0, + serversCount: (attrs.servers_count as number) || 0, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getLocations = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/locations', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToLocation), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getLocation = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/locations/${id}`) + .then(({ data }) => resolve(rawToLocation(data))) + .catch(reject); + }); + +export const deleteLocation = (id: number): Promise => http.delete(`/api/application/locations/${id}`); + +export interface CreateLocationData { + short: string; + long: string; +} + +export const createLocation = (data: CreateLocationData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/locations', data) + .then(({ data: resp }) => resolve(rawToLocation(resp))) + .catch(reject); + }); + +export const updateLocation = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/locations/${id}`, data) + .then(({ data: resp }) => resolve(rawToLocation(resp))) + .catch(reject); + }); diff --git a/resources/scripts/api/admin/mounts.ts b/resources/scripts/api/admin/mounts.ts new file mode 100644 index 000000000..0cd0a9c7a --- /dev/null +++ b/resources/scripts/api/admin/mounts.ts @@ -0,0 +1,77 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminMount { + id: number; + uuid: string; + name: string; + description: string | null; + source: string; + target: string; + readOnly: boolean; + userMountable: boolean; +} + +const rawToMount = (data: FractalResponseData): AdminMount => { + return { + id: data.attributes.id as number, + uuid: data.attributes.uuid as string, + name: data.attributes.name as string, + description: data.attributes.description as string | null, + source: data.attributes.source as string, + target: data.attributes.target as string, + readOnly: data.attributes.read_only as boolean, + userMountable: data.attributes.user_mountable as boolean, + }; +}; + +export const getMounts = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/mounts', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToMount), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getMount = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/mounts/${id}`) + .then(({ data }) => resolve(rawToMount(data))) + .catch(reject); + }); + +export interface CreateMountData { + name: string; + description?: string | null; + source: string; + target: string; + read_only?: boolean; + user_mountable?: boolean; +} + +export const createMount = (data: CreateMountData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/mounts', data) + .then(({ data: resp }) => resolve(rawToMount(resp))) + .catch(reject); + }); + +export const updateMount = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/mounts/${id}`, data) + .then(({ data: resp }) => resolve(rawToMount(resp))) + .catch(reject); + }); + +export const deleteMount = (id: number): Promise => http.delete(`/api/application/mounts/${id}`); diff --git a/resources/scripts/api/admin/nests.ts b/resources/scripts/api/admin/nests.ts new file mode 100644 index 000000000..6b979c3c7 --- /dev/null +++ b/resources/scripts/api/admin/nests.ts @@ -0,0 +1,284 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminEggVariable { + id: number; + name: string; + description: string; + envVariable: string; + defaultValue: string; + userViewable: boolean; + userEditable: boolean; + rules: string; + createdAt: string; + updatedAt: string; +} + +export interface AdminEggConfig { + files: Record | null; + startup: string | null; + stop: string | null; + logs: string | null; + fileDenylist: string[] | null; + extends: number | null; +} + +export interface AdminEggScript { + privileged: boolean; + install: string | null; + entry: string; + container: string; + extends: number | null; +} + +export interface AdminEgg { + id: number; + uuid: string; + name: string; + description: string; + nest: number; + author: string; + dockerImage: string; + dockerImages: Record; + config: AdminEggConfig; + startup: string; + script: AdminEggScript; + features: string[] | null; + forceOutgoingIp: boolean; + fileDenylist: string[] | null; + createdAt: string; + updatedAt: string; + variables?: AdminEggVariable[]; +} + +export interface AdminNest { + id: number; + uuid: string; + author: string; + name: string; + description: string; + eggsCount: number; + serversCount: number; + createdAt: string; + updatedAt: string; + eggs?: AdminEgg[]; +} + +const rawToNest = (data: FractalResponseData): AdminNest => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + author: attrs.author as string, + name: attrs.name as string, + description: (attrs.description as string) || '', + eggsCount: + (attrs.eggs_count as number) || (attrs.relationships?.eggs?.data as FractalResponseData[])?.length || 0, + serversCount: (attrs.servers_count as number) || 0, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +const rawToEgg = (data: FractalResponseData): AdminEgg => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + name: attrs.name as string, + description: (attrs.description as string) || '', + nest: attrs.nest as number, + author: attrs.author as string, + dockerImage: attrs.docker_image as string, + dockerImages: attrs.docker_images as Record, + config: { + files: attrs.config?.files as Record | null, + startup: attrs.config?.startup as string | null, + stop: attrs.config?.stop as string | null, + logs: attrs.config?.logs as string | null, + fileDenylist: attrs.config?.file_denylist as string[] | null, + extends: attrs.config?.extends as number | null, + }, + startup: attrs.startup as string, + script: { + privileged: attrs.script?.privileged as boolean, + install: attrs.script?.install as string | null, + entry: attrs.script?.entry as string, + container: attrs.script?.container as string, + extends: attrs.script?.extends as number | null, + }, + features: attrs.features as string[] | null, + forceOutgoingIp: attrs.force_outgoing_ip as boolean, + fileDenylist: attrs.file_denylist as string[] | null, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + variables: (attrs.relationships?.variables?.data as FractalResponseData[])?.map(rawToEggVariable), + }; +}; + +const rawToEggVariable = (data: FractalResponseData): AdminEggVariable => { + const attrs = data.attributes; + return { + id: attrs.id as number, + name: attrs.name as string, + description: attrs.description as string, + envVariable: attrs.env_variable as string, + defaultValue: attrs.default_value as string, + userViewable: attrs.user_viewable as boolean, + userEditable: attrs.user_editable as boolean, + rules: attrs.rules as string, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getNests = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/nests', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToNest), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getNest = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nests/${id}`) + .then(({ data }) => resolve(rawToNest(data))) + .catch(reject); + }); + +export interface CreateNestData { + name: string; + description?: string; +} + +export const createNest = (data: CreateNestData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/nests', data) + .then(({ data: resp }) => resolve(rawToNest(resp))) + .catch(reject); + }); + +export const updateNest = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/nests/${id}`, data) + .then(({ data: resp }) => resolve(rawToNest(resp))) + .catch(reject); + }); + +export const deleteNest = (id: number): Promise => http.delete(`/api/application/nests/${id}`); + +export const getNestEggs = (nestId: number, params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get(`/api/application/nests/${nestId}/eggs`, { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToEgg), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getEgg = (nestId: number, eggId: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nests/${nestId}/eggs/${eggId}`) + .then(({ data }) => resolve(rawToEgg(data))) + .catch(reject); + }); + +export interface CreateEggData { + name: string; + description?: string | null; + features?: string[] | null; + docker_images: Record; + startup: string; + config_from?: number | null; + config_stop?: string | null; + config_startup?: string | null; + config_logs?: string | null; + config_files?: string | null; + update_url?: string | null; + force_outgoing_ip?: boolean; + file_denylist?: string[] | null; +} + +export const createEgg = (nestId: number, data: CreateEggData): Promise => + new Promise((resolve, reject) => { + http.post(`/api/application/nests/${nestId}/eggs`, data) + .then(({ data: resp }) => resolve(rawToEgg(resp))) + .catch(reject); + }); + +export const updateEgg = (nestId: number, eggId: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/nests/${nestId}/eggs/${eggId}`, data) + .then(({ data: resp }) => resolve(rawToEgg(resp))) + .catch(reject); + }); + +export const deleteEgg = (nestId: number, eggId: number): Promise => + http.delete(`/api/application/nests/${nestId}/eggs/${eggId}`); + +export interface CreateEggVariableData { + name: string; + description?: string; + env_variable: string; + default_value?: string; + user_viewable?: boolean; + user_editable?: boolean; + rules?: string; +} + +export const getEggVariables = (nestId: number, eggId: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nests/${nestId}/eggs/${eggId}/variables`) + .then(({ data }) => resolve((data.data || []).map(rawToEggVariable))) + .catch(reject); + }); + +export const getEggVariable = (nestId: number, eggId: number, variableId: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nests/${nestId}/eggs/${eggId}/variables/${variableId}`) + .then(({ data }) => resolve(rawToEggVariable(data))) + .catch(reject); + }); + +export const createEggVariable = ( + nestId: number, + eggId: number, + data: CreateEggVariableData, +): Promise => + new Promise((resolve, reject) => { + http.post(`/api/application/nests/${nestId}/eggs/${eggId}/variables`, data) + .then(({ data: resp }) => resolve(rawToEggVariable(resp))) + .catch(reject); + }); + +export const updateEggVariable = ( + nestId: number, + eggId: number, + variableId: number, + data: Partial, +): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/nests/${nestId}/eggs/${eggId}/variables/${variableId}`, data) + .then(({ data: resp }) => resolve(rawToEggVariable(resp))) + .catch(reject); + }); + +export const deleteEggVariable = (nestId: number, eggId: number, variableId: number): Promise => + http.delete(`/api/application/nests/${nestId}/eggs/${eggId}/variables/${variableId}`); diff --git a/resources/scripts/api/admin/nodes.ts b/resources/scripts/api/admin/nodes.ts new file mode 100644 index 000000000..7fc11af0b --- /dev/null +++ b/resources/scripts/api/admin/nodes.ts @@ -0,0 +1,196 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminNode { + id: number; + uuid: string; + name: string; + description: string; + locationId: number; + locationName?: string; + public: boolean; + fqdn: string; + scheme: string; + behindProxy: boolean; + maintenanceMode: boolean; + memory: number; + memoryOverallocate: number; + disk: number; + diskOverallocate: number; + daemonBase: string; + daemonListen: number; + daemonSftp: number; + serversCount: number; + createdAt: string; + updatedAt: string; +} + +const rawToNode = (data: FractalResponseData): AdminNode => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + name: attrs.name as string, + description: (attrs.description as string) || '', + locationId: attrs.location_id as number, + public: attrs.public as boolean, + fqdn: attrs.fqdn as string, + scheme: attrs.scheme as string, + behindProxy: attrs.behind_proxy as boolean, + maintenanceMode: attrs.maintenance_mode as boolean, + memory: attrs.memory as number, + memoryOverallocate: attrs.memory_overallocate as number, + disk: attrs.disk as number, + diskOverallocate: attrs.disk_overallocate as number, + daemonBase: attrs.daemon_base as string, + daemonListen: attrs.daemon_listen as number, + daemonSftp: attrs.daemon_sftp as number, + serversCount: 0, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getNodes = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/nodes', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToNode), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getNode = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nodes/${id}`) + .then(({ data }) => resolve(rawToNode(data))) + .catch(reject); + }); + +export const deleteNode = (id: number): Promise => http.delete(`/api/application/nodes/${id}`); + +export interface CreateNodeData { + name: string; + location_id: number; + fqdn: string; + scheme?: string; + behind_proxy?: boolean; + memory?: number; + memory_overallocate?: number; + disk?: number; + disk_overallocate?: number; + daemon_base?: string; + daemon_listen?: number; + daemon_sftp?: number; + public?: boolean; + description?: string; +} + +export const createNode = (data: CreateNodeData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/nodes', data) + .then(({ data: resp }) => resolve(rawToNode(resp))) + .catch(reject); + }); + +export interface UpdateNodeData extends Partial { + maintenance_mode?: boolean; + location_id?: number; +} + +export const updateNode = (id: number, data: UpdateNodeData): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/nodes/${id}`, data) + .then(({ data: resp }) => resolve(rawToNode(resp))) + .catch(reject); + }); + +export interface AdminAllocation { + id: number; + nodeId: number; + ip: string; + port: number; + alias: string | null; + serverId: number | null; + createdAt: string; + updatedAt: string; +} + +const rawToAllocation = (data: FractalResponseData): AdminAllocation => { + const attrs = data.attributes; + return { + id: attrs.id as number, + nodeId: attrs.node_id as number, + ip: attrs.ip as string, + port: attrs.port as number, + alias: (attrs.alias as string) || null, + serverId: (attrs.server_id as number) || null, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getNodeAllocations = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/nodes/${id}/allocations`) + .then(({ data }) => resolve((data.data || []).map(rawToAllocation))) + .catch(reject); + }); + +export interface CreateAllocationData { + ip: string; + port: number; + alias?: string; +} + +export const createAllocation = (nodeId: number, data: CreateAllocationData): Promise => + new Promise((resolve, reject) => { + http.post(`/api/application/nodes/${nodeId}/allocations`, data) + .then(({ data: resp }) => resolve(rawToAllocation(resp))) + .catch(reject); + }); + +export const deleteAllocation = (nodeId: number, allocationId: number): Promise => + http.delete(`/api/application/nodes/${nodeId}/allocations/${allocationId}`); + +export const setAllocationAlias = (nodeId: number, allocationId: number, alias: string): Promise => + http.put(`/api/application/nodes/${nodeId}/allocations/${allocationId}`, { alias }); + +export const removeAllocationBlock = (nodeId: number, allocationId: number): Promise => + http.post(`/api/application/nodes/${nodeId}/allocations/${allocationId}/block`); + +export interface NodeConfiguration { + uuid: string; + daemonToken: string; + daemonBase: string; + config: Record; +} + +export const getNodeConfiguration = (id: number): Promise => + http.get(`/api/application/nodes/${id}/configuration`).then(({ data }) => data); + +export interface DeployableNode { + id: number; + name: string; + fqdn: string; + memory: number; + disk: number; + locations: { id: number; short: string; long: string }[]; +} + +export const getDeployableNodes = (eggId?: number): Promise => + new Promise((resolve, reject) => { + http.get('/api/application/nodes/deployable', { params: eggId ? { egg_id: eggId } : undefined }) + .then(({ data }) => resolve(data.data || [])) + .catch(reject); + }); diff --git a/resources/scripts/api/admin/servers.ts b/resources/scripts/api/admin/servers.ts new file mode 100644 index 000000000..e3e7f71db --- /dev/null +++ b/resources/scripts/api/admin/servers.ts @@ -0,0 +1,259 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + type QueryBuilderParams, + withQueryBuilderParams, +} from '@/api/http'; + +export interface AdminServer { + id: number; + uuid: string; + shortUuid: string; + name: string; + description: string; + node: number; + nodeName?: string; + egg: number; + eggName?: string; + user: number; + userName?: string; + memory: number; + swap: number; + disk: number; + cpu: number; + io: number; + threads: string | null; + databases: number; + backups: number; + allocations: number; + status: string | null; + suspended: boolean; + installed: boolean; + createdAt: string; + updatedAt: string; +} + +const rawToServer = (data: FractalResponseData): AdminServer => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + shortUuid: attrs.short_uuid as string, + name: attrs.name as string, + description: (attrs.description as string) || '', + node: attrs.node as number, + egg: attrs.egg as number, + user: attrs.user as number, + memory: (attrs.limits?.memory as number) || 0, + swap: (attrs.limits?.swap as number) || 0, + disk: (attrs.limits?.disk as number) || 0, + cpu: (attrs.limits?.cpu as number) || 0, + io: (attrs.limits?.io as number) || 500, + threads: (attrs.limits?.threads as string) || null, + databases: attrs.relationships?.databases?.data?.length || 0, + backups: attrs.relationships?.backups?.data?.length || 0, + allocations: attrs.relationships?.allocations?.data?.length || 0, + status: attrs.status as string | null, + suspended: attrs.suspended as boolean, + installed: attrs.installed as boolean, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getServers = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/servers', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToServer), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getServer = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/servers/${id}`) + .then(({ data }) => resolve(rawToServer(data))) + .catch(reject); + }); + +export const deleteServer = (id: number): Promise => http.delete(`/api/application/servers/${id}`); + +export const suspendServer = (id: number): Promise => http.post(`/api/application/servers/${id}/suspend`); + +export const unsuspendServer = (id: number): Promise => http.post(`/api/application/servers/${id}/unsuspend`); + +export const reinstallServer = (id: number): Promise => http.post(`/api/application/servers/${id}/reinstall`); + +export interface ServerDetails { + id: number; + uuid: string; + name: string; + description: string; + userId: number; + nodeId: number; + eggId: number; + nestId: number; + allocationId: number; + image: string; + createdAt: string; + updatedAt: string; + user?: { id: number; username: string; email: string }; + node?: { id: number; name: string }; + egg?: { id: number; name: string }; + allocatons?: { id: number; ip: string; port: number }[]; +} + +const rawToServerDetails = (data: FractalResponseData): ServerDetails => { + const attrs = data.attributes; + return { + id: attrs.id as number, + uuid: attrs.uuid as string, + name: attrs.name as string, + description: (attrs.description as string) || '', + userId: attrs.user as number, + nodeId: attrs.node as number, + eggId: attrs.egg as number, + nestId: attrs.nest as number, + allocationId: attrs.allocation as number, + image: (attrs.docker_image as string) || '', + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + user: attrs.relationships?.user as ServerDetails['user'], + node: attrs.relationships?.node as ServerDetails['node'], + egg: attrs.relationships?.egg as ServerDetails['egg'], + }; +}; + +export const getServerDetails = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/servers/${id}`) + .then(({ data }) => resolve(rawToServerDetails(data))) + .catch(reject); + }); + +export interface UpdateServerDetailsData { + name?: string; + description?: string; + user?: number; + allocation_id?: number; + docker_image?: string; + startup?: string; + environment?: Record; + egg_id?: number; + swap?: number; + io?: number; + threads?: string | null; + oom_kill?: boolean; + databases?: number; + backups?: number; +} + +export const updateServerDetails = (id: number, data: UpdateServerDetailsData): Promise => + http.patch(`/api/application/servers/${id}/details`, data); + +export interface UpdateServerBuildData { + memory?: number; + swap?: number; + disk?: number; + io?: number; + cpu?: number; + threads?: string | null; + oom_kill?: boolean; +} + +export const updateServerBuild = (id: number, data: UpdateServerBuildData): Promise => + http.patch(`/api/application/servers/${id}/build`, data); + +export interface UpdateServerStartupData { + startup?: string | null; + env?: Record; + image?: string; + egg_id?: number; +} + +export const updateServerStartup = (id: number, data: UpdateServerStartupData): Promise => + http.patch(`/api/application/servers/${id}/startup`, data); + +export interface ServerDatabase { + id: number; + serverId: number; + hostId: number; + database: string; + username: string; + createdAt: string; + updatedAt: string; +} + +const rawToServerDatabase = (data: FractalResponseData): ServerDatabase => { + const attrs = data.attributes; + return { + id: attrs.id as number, + serverId: attrs.server_id as number, + hostId: attrs.host_id as number, + database: attrs.database as string, + username: attrs.username as string, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +export const getServerDatabases = (id: number): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/servers/${id}/databases`) + .then(({ data }) => resolve((data.data || []).map(rawToServerDatabase))) + .catch(reject); + }); + +export interface CreateServerDatabaseData { + database_host_id: number; + database?: string; + username?: string; +} + +export const createServerDatabase = (id: number, data: CreateServerDatabaseData): Promise => + new Promise((resolve, reject) => { + http.post(`/api/application/servers/${id}/databases`, data) + .then(({ data: resp }) => resolve(rawToServerDatabase(resp))) + .catch(reject); + }); + +export const deleteServerDatabase = (id: number, databaseId: number): Promise => + http.delete(`/api/application/servers/${id}/databases/${databaseId}`); + +export interface CreateServerData { + name: string; + user: number; + egg: number; + docker_image?: string; + startup_command?: string; + environment?: Record; + allocation_id?: number; + allocation_ids?: number[]; + memory?: number; + swap?: number; + disk?: number; + io?: number; + cpu?: number; + threads?: string | null; + feature_limits?: { + databases?: number; + allocations?: number; + backups?: number; + }; + skip_scripts?: boolean; + start_on_completion?: boolean; +} + +export const createServer = (data: CreateServerData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/servers', data) + .then(({ data: resp }) => resolve(rawToServer(resp))) + .catch(reject); + }); diff --git a/resources/scripts/api/admin/settings.ts b/resources/scripts/api/admin/settings.ts new file mode 100644 index 000000000..7ee20c149 --- /dev/null +++ b/resources/scripts/api/admin/settings.ts @@ -0,0 +1,117 @@ +import http from '@/api/http'; + +export interface GeneralSettings { + 'app:name': string; + 'app:locale': string; + 'pterodactyl:auth:2fa_required': number; + available_languages: Record; +} + +export interface MailSettings { + disabled: boolean; + 'mail:mailers:smtp:host': string; + 'mail:mailers:smtp:port': number; + 'mail:mailers:smtp:encryption': string; + 'mail:mailers:smtp:username': string; + 'mail:from:address': string; + 'mail:from:name': string; +} + +export interface CaptchaSettings { + providers: Record; + 'pterodactyl:captcha:provider': string; + 'pterodactyl:captcha:turnstile:site_key': string; + 'pterodactyl:captcha:hcaptcha:site_key': string; + 'pterodactyl:captcha:recaptcha:site_key': string; +} + +export interface BrandingSettings { + logoType: string | null; + logoUrl: string | null; + logoValue: string | null; + history: BrandingHistoryEntry[]; +} + +export interface BrandingHistoryEntry { + type: 'upload' | 'link'; + value: string; + timestamp: string; +} + +export interface AdvancedSettings { + 'pterodactyl:guzzle:connect_timeout': number; + 'pterodactyl:guzzle:timeout': number; + 'pterodactyl:client_features:allocations:enabled': string; + 'pterodactyl:client_features:allocations:range_start': number | null; + 'pterodactyl:client_features:allocations:range_end': number | null; +} + +export interface DomainData { + id: number; + name: string; + dns_provider: string; + dns_config: Record; + is_active: boolean; + is_default: boolean; + server_subdomains_count?: number; + created_at: string; + updated_at: string; +} + +export interface DomainsResponse { + domains: DomainData[]; + providers: Record; +} + +export const getGeneralSettings = (): Promise => + http.get('/api/application/settings/general').then(({ data }) => data); + +export const updateGeneralSettings = (settings: Partial): Promise => + http.patch('/api/application/settings/general', settings); + +export const getMailSettings = (): Promise => + http.get('/api/application/settings/mail').then(({ data }) => data); + +export const updateMailSettings = (settings: Partial): Promise => + http.patch('/api/application/settings/mail', settings); + +export const testMailSettings = (): Promise => + http.post('/api/application/settings/mail/test'); + +export const getCaptchaSettings = (): Promise => + http.get('/api/application/settings/captcha').then(({ data }) => data); + +export const updateCaptchaSettings = (settings: Record): Promise => + http.patch('/api/application/settings/captcha', settings); + +export const getBrandingSettings = (): Promise => + http.get('/api/application/settings/branding').then(({ data }) => data); + +export const updateBrandingSettings = (formData: FormData): Promise => + http.post('/api/application/settings/branding', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + +export const getAdvancedSettings = (): Promise => + http.get('/api/application/settings/advanced').then(({ data }) => data); + +export const updateAdvancedSettings = (settings: Partial): Promise => + http.patch('/api/application/settings/advanced', settings); + +export const getDomains = (): Promise => + http.get('/api/application/settings/domains').then(({ data }) => data); + +export const createDomain = (data: Record): Promise => + http.post('/api/application/settings/domains', data); + +export const updateDomain = (id: number, data: Record): Promise => + http.patch(`/api/application/settings/domains/${id}`, data); + +export const deleteDomain = (id: number): Promise => + http.delete(`/api/application/settings/domains/${id}`); + +export const testDnsConnection = (data: { dns_provider: string; dns_config: Record }): Promise<{ success: boolean; message: string }> => + http.post('/api/application/settings/domains/test-connection', data).then(({ data: resp }) => resp); + +export const getProviderSchema = (provider: string): Promise<{ success: boolean; schema: Record }> => + http.get(`/api/application/settings/domains/provider-schema/${provider}`).then(({ data: resp }) => resp); diff --git a/resources/scripts/api/admin/users.ts b/resources/scripts/api/admin/users.ts new file mode 100644 index 000000000..a889e90b1 --- /dev/null +++ b/resources/scripts/api/admin/users.ts @@ -0,0 +1,131 @@ +import http, { + type FractalResponseData, + getPaginationSet, + type PaginatedResult, + withQueryBuilderParams, + type QueryBuilderParams, +} from '@/api/http'; + +export interface AdminServerSummary { + id: number; + name: string; + uuid: string; + identifier: string; + nodeId: number; + status: string[]; + suspended: boolean; + createdAt: string; + updatedAt: string; +} + +export interface AdminUser { + id: number; + uuid: string; + externalId: string | null; + username: string; + email: string; + nameFirst: string; + nameLast: string; + language: string; + rootAdmin: boolean; + useTotp: boolean; + serversCount: number; + createdAt: string; + updatedAt: string; + servers?: AdminServerSummary[]; +} + +const rawToServer = (data: FractalResponseData): AdminServerSummary => { + const attrs = data.attributes; + return { + id: attrs.id as number, + name: attrs.name as string, + uuid: attrs.uuid as string, + identifier: attrs.identifier as string, + nodeId: attrs.node as number, + status: (attrs.status as string[]) || [], + suspended: attrs.suspended as boolean, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; +}; + +const rawToUser = (data: FractalResponseData): AdminUser => { + const attrs = data.attributes; + const relationships = data.relationships as Record | undefined; + + const user: AdminUser = { + id: attrs.id as number, + uuid: attrs.uuid as string, + externalId: (attrs.external_id as string) || null, + username: attrs.username as string, + email: attrs.email as string, + nameFirst: attrs.first_name as string, + nameLast: attrs.last_name as string, + language: attrs.language as string, + rootAdmin: attrs.root_admin as boolean, + useTotp: attrs['2fa'] as boolean, + serversCount: (attrs.servers_count as number) || 0, + createdAt: attrs.created_at as string, + updatedAt: attrs.updated_at as string, + }; + + if (relationships?.server) { + const serverList = relationships.server as { data: FractalResponseData[] }; + if (Array.isArray(serverList.data)) { + user.servers = serverList.data.map(rawToServer); + } + } + + return user; +}; + +export const getUsers = (params?: QueryBuilderParams): Promise> => + new Promise((resolve, reject) => { + http.get('/api/application/users', { + params: withQueryBuilderParams(params), + }) + .then(({ data }) => + resolve({ + items: (data.data || []).map(rawToUser), + pagination: getPaginationSet(data.meta.pagination), + }), + ) + .catch(reject); + }); + +export const getUser = (id: number, include?: string): Promise => + new Promise((resolve, reject) => { + http.get(`/api/application/users/${id}`, { + params: include ? { include } : undefined, + }) + .then(({ data }) => resolve(rawToUser(data))) + .catch(reject); + }); + +export const deleteUser = (id: number): Promise => + http.delete(`/api/application/users/${id}`); + +export interface CreateUserData { + username: string; + email: string; + name_first: string; + name_last: string; + password?: string; + root_admin?: boolean; + language?: string; +} + +export const createUser = (data: CreateUserData): Promise => + new Promise((resolve, reject) => { + http.post('/api/application/users', data) + .then(({ data: resp }) => resolve(rawToUser(resp))) + .catch(reject); + }); + +export const updateUser = (id: number, data: Partial): Promise => + new Promise((resolve, reject) => { + http.patch(`/api/application/users/${id}`, data) + .then(({ data: resp }) => resolve(rawToUser(resp))) + .catch(reject); + }); diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index 6ca3a2d3d..8df3ffc86 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -22,6 +22,7 @@ import HydrodactylProvider from './HydrodactylProvider'; // const DashboardRouter = lazy(() => import('@/routers/DashboardRouter')); // const ServerRouter = lazy(() => import('@/routers/ServerRouter')); +const AdminRouter = lazy(() => import('@/routers/AdminRouter')); const UnifiedRouter = lazy(() => import('@/routers/UnifiedRouter')); const AuthenticationRouter = lazy(() => import('@/routers/AuthenticationRouter')); const SetupRouter = lazy(() => import('@/routers/SetupRouter')); @@ -100,6 +101,17 @@ const App = () => { } /> + + + + + + } + /> + { } /> - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* } */} - {/* /> */} - } /> diff --git a/resources/scripts/components/admin/AdminDashboardContainer.tsx b/resources/scripts/components/admin/AdminDashboardContainer.tsx new file mode 100644 index 000000000..c31d73e81 --- /dev/null +++ b/resources/scripts/components/admin/AdminDashboardContainer.tsx @@ -0,0 +1,678 @@ +import { + Activity02Icon, + Archive01Icon, + CubeIcon, + Database02Icon, + GlobalIcon, + ServerStack02Icon, + UserMultiple02Icon, +} from '@hugeicons/core-free-icons'; +import { HugeiconsIcon, type IconSvgElement } from '@hugeicons/react'; +import { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import useSWR from 'swr'; + +import { getAdminCounts, getPanelStatus, type PanelStatus } from '@/api/admin'; +import { getServers, type AdminServer } from '@/api/admin/servers'; +import { SkeletonCards } from '@/components/admin/shared/AdminSkeleton'; + +/* ─────────────────────────── helpers ─────────────────────────── */ + +function formatBytes(bytes: number): string { + if (!bytes) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`; +} + +function formatUptime(seconds: number): string { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}d ${h}h ${m}m`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} + +function formatRelativeTime(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return `${Math.floor(hrs / 24)}d ago`; +} + +/* ──────────────────────── count-up hook ──────────────────────── */ + +function useCountUp(target: number, duration = 900): number { + const [value, setValue] = useState(0); + const frameRef = useRef(0); + + useEffect(() => { + if (target === 0) { + setValue(0); + return; + } + const start = performance.now(); + const tick = (now: number) => { + const progress = Math.min((now - start) / duration, 1); + // ease-out cubic + const eased = 1 - (1 - progress) ** 3; + setValue(Math.floor(eased * target)); + if (progress < 1) frameRef.current = requestAnimationFrame(tick); + }; + frameRef.current = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frameRef.current); + }, [target, duration]); + + return value; +} + +/* ──────────────────────── stat card ──────────────────────── */ + +const CARD_ACCENT: Record = { + servers: 'from-blue-500/20 to-blue-500/5 border-blue-500/30', + nodes: 'from-teal-500/20 to-teal-500/5 border-teal-500/30', + users: 'from-violet-500/20 to-violet-500/5 border-violet-500/30', + locations: 'from-amber-500/20 to-amber-500/5 border-amber-500/30', + nests: 'from-emerald-500/20 to-emerald-500/5 border-emerald-500/30', + buckets: 'from-rose-500/20 to-rose-500/5 border-rose-500/30', +}; + +const ICON_RING: Record = { + servers: 'bg-blue-500/15 text-blue-400', + nodes: 'bg-teal-500/15 text-teal-400', + users: 'bg-violet-500/15 text-violet-400', + locations: 'bg-amber-500/15 text-amber-400', + nests: 'bg-emerald-500/15 text-emerald-400', + buckets: 'bg-rose-500/15 text-rose-400', +}; + +interface StatCardProps { + id: string; + label: string; + value: number; + sub: string; + to: string; + icon: IconSvgElement; +} + +function StatCard({ id, label, value, sub, to, icon: Icon }: StatCardProps) { + const displayed = useCountUp(value); + const accent = CARD_ACCENT[id] ?? 'from-mocha-400/20 to-mocha-400/5 border-mocha-400/30'; + const ring = ICON_RING[id] ?? 'bg-mocha-400 text-cream-400'; + + return ( + + {/* top row */} +
+ {label} +
+ +
+
+ + {/* value */} +
+ {displayed.toLocaleString()} +
+ + {/* sub */} +

{sub}

+ + ); +} + +/* ──────────────────────── SVG arc gauge ──────────────────────── */ + +interface ArcGaugeProps { + percent: number; + label: string; + sub: string; +} + +function gaugeColor(pct: number): string { + if (pct >= 80) return '#f87171'; // red-400 + if (pct >= 60) return '#fbbf24'; // amber-400 + return '#34d399'; // emerald-400 +} + +function ArcGauge({ percent, label, sub }: ArcGaugeProps) { + const [animPct, setAnimPct] = useState(0); + const frameRef = useRef(0); + + useEffect(() => { + const duration = 1000; + const start = performance.now(); + const tick = (now: number) => { + const progress = Math.min((now - start) / duration, 1); + const eased = 1 - (1 - progress) ** 3; + setAnimPct(eased * percent); + if (progress < 1) frameRef.current = requestAnimationFrame(tick); + }; + frameRef.current = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frameRef.current); + }, [percent]); + + const size = 100; + const strokeW = 8; + const r = (size - strokeW) / 2; + const cx = size / 2; + const cy = size / 2; + // arc from 135° to 405° (270° sweep) + const sweepDeg = 270; + const color = gaugeColor(percent); + + // Convert degrees to radians for path + const toRad = (deg: number) => (deg * Math.PI) / 180; + const polarToXY = (angle: number) => ({ + x: cx + r * Math.cos(toRad(angle)), + y: cy + r * Math.sin(toRad(angle)), + }); + + const startA = -135; // -135° from 3 o'clock = 7:30 position + const endA = startA + sweepDeg; // 135° + + const start = polarToXY(startA); + const end = polarToXY(endA); + const largeArc = sweepDeg > 180 ? 1 : 0; + + const trackPath = `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y}`; + + // filled arc + const fillEndA = startA + (animPct / 100) * sweepDeg; + const fillEnd = polarToXY(fillEndA); + const fillLargeArc = (animPct / 100) * sweepDeg > 180 ? 1 : 0; + const fillPath = + animPct <= 0 ? '' : `M ${start.x} ${start.y} A ${r} ${r} 0 ${fillLargeArc} 1 ${fillEnd.x} ${fillEnd.y}`; + + return ( +
+
+ + {/* track */} + + {/* fill */} + {fillPath && ( + + )} + + {/* center text */} +
+ + {Math.round(animPct)}% + +
+
+
+

{label}

+

{sub}

+
+
+ ); +} + +/* ──────────────────────── load sparkline ──────────────────────── */ + +function LoadSparkline({ loads }: { loads: number[] }) { + const max = Math.max(...loads, 1); + return ( +
+ {loads.map((v, i) => { + const pct = (v / max) * 100; + const color = v > 2 ? '#f87171' : v > 1 ? '#fbbf24' : '#34d399'; + return ( +
+
+ {i + 1}m +
+ ); + })} +
+ ); +} + +/* ──────────────────────── quick action card ──────────────────────── */ + +interface ActionCardProps { + to: string; + icon: IconSvgElement; + label: string; + description: string; + id: string; +} + +function ActionCard({ to, icon: Icon, label, description, id }: ActionCardProps) { + return ( + +
+ +
+
+

{label}

+

{description}

+
+ + ); +} + +/* ──────────────────────── status badge ──────────────────────── */ + +function ServerStatusBadge({ server }: { server: AdminServer }) { + if (server.suspended) { + return ( + + + Suspended + + ); + } + if (!server.installed) { + return ( + + + Installing + + ); + } + return ( + + + Active + + ); +} + +/* ──────────────────────── info row ──────────────────────── */ + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} + +/* ──────────────────────── live uptime ticker ──────────────────────── */ + +function UptimeTicker({ uptimeSeconds }: { uptimeSeconds: number }) { + const [live, setLive] = useState(uptimeSeconds); + useEffect(() => { + const id = setInterval(() => setLive((s) => s + 1), 1000); + return () => clearInterval(id); + }, []); + return {formatUptime(live)}; +} + +/* ──────────────────────── section header ──────────────────────── */ + +function SectionHeader({ title, sub, action }: { title: string; sub?: string; action?: React.ReactNode }) { + return ( +
+
+

{title}

+ {sub &&

{sub}

} +
+ {action} +
+ ); +} + +/* ──────────────────────── skeleton for gauges ──────────────────────── */ + +function GaugeSkeleton() { + return ( +
+
+
+
+
+ ); +} + +/* ──────────────────────── main component ──────────────────────── */ + +const AdminDashboardContainer = () => { + const { data: status } = useSWR('admin:status', getPanelStatus, { refreshInterval: 30000 }); + const { data: counts } = useSWR('admin:counts', getAdminCounts, { refreshInterval: 60000 }); + const { data: serversPage } = useSWR('admin:dashboard:servers', () => getServers({ page: 1 })); + + const serverCount = counts?.servers ?? 0; + const nodeCount = counts?.nodes ?? 0; + const userCount = counts?.users ?? 0; + const locationCount = counts?.locations ?? 0; + const nestCount = counts?.nests ?? 0; + const bucketCount = counts?.buckets ?? 0; + + const items = serversPage?.items ?? []; + const activeServers = items.filter((s) => !s.suspended && s.installed).length; + const suspendedServers = items.filter((s) => s.suspended).length; + const recentServers = [...items] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 5); + + const stats: StatCardProps[] = [ + { + id: 'servers', + label: 'Servers', + value: serverCount, + sub: `${activeServers} active · ${suspendedServers} suspended`, + to: '/admin/servers', + icon: ServerStack02Icon, + }, + { + id: 'nodes', + label: 'Nodes', + value: nodeCount, + sub: 'Active infrastructure', + to: '/admin/nodes', + icon: Activity02Icon, + }, + { + id: 'users', + label: 'Users', + value: userCount, + sub: 'Registered accounts', + to: '/admin/users', + icon: UserMultiple02Icon, + }, + { + id: 'locations', + label: 'Locations', + value: locationCount, + sub: 'Deployment regions', + to: '/admin/locations', + icon: GlobalIcon, + }, + { + id: 'nests', + label: 'Nests', + value: nestCount, + sub: 'Egg containers', + to: '/admin/nests', + icon: CubeIcon, + }, + { + id: 'buckets', + label: 'S3 Buckets', + value: bucketCount, + sub: 'Object storage', + to: '/admin/buckets', + icon: Archive01Icon, + }, + ]; + + const quickActions: ActionCardProps[] = [ + { + id: 'create-server', + to: '/admin/servers/new', + icon: ServerStack02Icon, + label: 'Create Server', + description: 'Deploy a new game server', + }, + { + id: 'create-user', + to: '/admin/users/new', + icon: UserMultiple02Icon, + label: 'Create User', + description: 'Add a new panel account', + }, + { + id: 'add-node', + to: '/admin/nodes/new', + icon: Activity02Icon, + label: 'Add Node', + description: 'Register a Wings daemon', + }, + { + id: 'add-bucket', + to: '/admin/buckets/new', + icon: Archive01Icon, + label: 'Add S3 Bucket', + description: 'Connect object storage', + }, + { + id: 'manage-databases', + to: '/admin/databases', + icon: Database02Icon, + label: 'Databases', + description: 'Manage database hosts', + }, + { + id: 'manage-locations', + to: '/admin/locations', + icon: GlobalIcon, + label: 'Locations', + description: 'Configure regions', + }, + ]; + + const cpuPct = status ? status.metrics.cpu : 0; + const memPct = + status && status.metrics.memory.total > 0 + ? (status.metrics.memory.used / status.metrics.memory.total) * 100 + : 0; + const diskPct = + status && status.metrics.disk.total > 0 ? (status.metrics.disk.used / status.metrics.disk.total) * 100 : 0; + + return ( +
+ {/* ── page header ── */} +
+

Overview

+

Panel health, resources, and quick access

+
+ + {/* ── stat cards ── */} + {!counts ? ( + + ) : ( +
+ {stats.map((s) => ( + + ))} +
+ )} + + {/* ── resource health + system info ── */} +
+ {/* gauges — 3/5 */} +
+ + {!status ? ( +
+ + + +
+ ) : ( +
+ +
+ +
+ +
+ )} +
+ + {/* system info — 2/5 */} +
+ + {!status ? ( +
+ {[80, 56, 64, 72].map((w, i) => ( +
+
+
60 ? 24 : 16} rounded bg-mocha-400/30`} /> +
+ ))} +
+ ) : ( + <> +
+ + + +
+ + Uptime + + + + +
+
+
+

+ Load Average +

+ +
+ + )} +
+
+ + {/* ── quick actions ── */} +
+ +
+ {quickActions.map((a) => ( + + ))} +
+
+ + {/* ── recent servers ── */} +
+ + View all → + + } + /> +
+ {!serversPage ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+
+
+ ))} +
+ ) : recentServers.length === 0 ? ( +
+ No servers yet.{' '} + + Create one + +
+ ) : ( + + + + + + + + + + + + {recentServers.map((server) => ( + + + + + + + + ))} + +
+ Name + + Owner + + Resources + + Status + + Created +
+ + {server.name} + +

+ {server.shortUuid} +

+
+ {server.userName ?? `UID ${server.user}`} + + {server.memory === 0 ? '∞' : `${server.memory} MB`} RAM ·{' '} + {server.disk === 0 ? '∞' : `${server.disk} MB`} Disk + + + + {formatRelativeTime(server.createdAt)} +
+ )} +
+
+
+ ); +}; + +export default AdminDashboardContainer; diff --git a/resources/scripts/components/admin/api/AdminApiContainer.tsx b/resources/scripts/components/admin/api/AdminApiContainer.tsx new file mode 100644 index 000000000..bb5044296 --- /dev/null +++ b/resources/scripts/components/admin/api/AdminApiContainer.tsx @@ -0,0 +1,187 @@ +import { useState } from 'react'; +import useSWR from 'swr'; +import { type AdminApiKey, type CreateApiKeyData, createApiKey, deleteApiKey, getApiKeys } from '@/api/admin/apiKeys'; +import { httpErrorToHuman } from '@/api/http'; +import { Button } from '@/components/ui/button'; +import { MainPageHeader } from '@/components/elements/MainPageHeader'; +import Pagination from '@/components/elements/Pagination'; +import Spinner from '@/components/elements/Spinner'; + +const AdminApiContainer = () => { + const [page, setPage] = useState(1); + const [showCreate, setShowCreate] = useState(false); + const [memo, setMemo] = useState(''); + const [allowedIps, setAllowedIps] = useState(''); + const [creating, setCreating] = useState(false); + const [createdToken, setCreatedToken] = useState(null); + const { data, error, mutate } = useSWR(['admin:api-keys', page], () => getApiKeys({ page })); + + const handleCreate = async () => { + if (!memo.trim()) return; + setCreating(true); + try { + const payload: CreateApiKeyData = { + memo: memo.trim(), + allowedIps: allowedIps + ? allowedIps + .split(',') + .map((ip) => ip.trim()) + .filter(Boolean) + : null, + }; + const result = await createApiKey(payload); + setCreatedToken(result.token); + setMemo(''); + setAllowedIps(''); + setShowCreate(false); + mutate(); + } catch (e: any) { + alert(httpErrorToHuman(e)); + } finally { + setCreating(false); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm('Are you sure you want to delete this API key? This cannot be undone.')) return; + try { + await deleteApiKey(id); + mutate(); + } catch (e: any) { + alert(httpErrorToHuman(e)); + } + }; + + const handleCopyToken = () => { + if (createdToken) { + navigator.clipboard.writeText(createdToken); + } + }; + + return ( +
+ + + + + {error &&
Error: {httpErrorToHuman(error)}
} + + {createdToken && ( +
+
+
+

+ API Key Created — Copy This Token Now +

+

+ This token will not be shown again. Store it securely. +

+ + {createdToken} + +
+ +
+
+ )} + + {showCreate && !createdToken && ( +
+

New API Key

+
+
+ + setMemo(e.target.value)} + placeholder='e.g. Production server key' + className='w-full bg-mocha-600 border border-mocha-400 rounded px-3 py-2 text-sm text-cream-400 placeholder-mocha-200/40 focus:outline-none focus:border-mocha-400' + /> +
+
+ + -
+
+ +
- -
- -
-
-
-
- -
- -

A unique ID used for identification of this nest internally and through the API.

-
+ +
+ +
+
+
+

Nest Info

+
+
+
+ + +

Used for identification internally and through the API.

-
- -
- -

The author of this service option. Please direct questions and issues to them unless this is an official option authored by support@pterodactyl.io.

-
+
+ + +

Direct questions and issues to the author.

-
- -
- -

A UUID that all servers using this option are assigned for identification purposes.

-
+
+ + +

Assigned to all servers using this option.

-
-
-
-
-

Nest Eggs

-
-
- - - - - - - + +
+
+
+

Nest Eggs

+ + + +
+
+
IDNameDescriptionServers
+ + + + + + + - @foreach($nest->eggs as $egg) + + + @forelse($nest->eggs as $egg) + + + + + + + + @empty - - - - - - @endforeach -
IDNameDescriptionServers
{{ $egg->id }} + + {{ $egg->name }} + + {{ $egg->description }}{{ $egg->servers->count() }} + + + +
{{ $egg->id }}{{ $egg->name }}{{ $egg->description }}{{ $egg->servers->count() }} - + +

No eggs in this nest

+

Create a new egg to get started.

-
- + @endforelse + +
@@ -109,9 +143,9 @@ @parent @endsection diff --git a/resources/views/admin/nodes/index.blade.php b/resources/views/admin/nodes/index.blade.php index 68623d286..3b625ced8 100644 --- a/resources/views/admin/nodes/index.blade.php +++ b/resources/views/admin/nodes/index.blade.php @@ -25,12 +25,12 @@

Node List

-
+
@@ -56,7 +56,7 @@ @foreach ($nodes as $node) - {!! $node->maintenance_mode ? ' ' : '' !!}{{ $node->name }} + {!! $node->maintenance_mode ? ' ' : '' !!}{{ $node->name }} {{ $node->location->short }} {{ $node->memory_percent }}% {{ $node->allocated_memory }} diff --git a/resources/views/admin/nodes/new.blade.php b/resources/views/admin/nodes/new.blade.php index d09248ce1..f37ffa281 100644 --- a/resources/views/admin/nodes/new.blade.php +++ b/resources/views/admin/nodes/new.blade.php @@ -8,13 +8,13 @@

New NodeCreate a new local or remote node for servers to be installed to.

@endsection @section('content') -
+
diff --git a/resources/views/admin/nodes/view/allocation.blade.php b/resources/views/admin/nodes/view/allocation.blade.php index 396428b61..167ac5812 100644 --- a/resources/views/admin/nodes/view/allocation.blade.php +++ b/resources/views/admin/nodes/view/allocation.blade.php @@ -8,8 +8,8 @@

{{ $node->name }}Control allocations available for servers on this node.

@endsection @@ -19,11 +19,11 @@ @@ -72,7 +72,7 @@ {{ $allocation->port }} @if(! is_null($allocation->server)) - {{ $allocation->server->name }} + {{ $allocation->server->name }} @endif @@ -92,7 +92,7 @@
- +

Assign New Allocations

@@ -139,7 +139,7 @@
- +