Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7635da3
chore: update application version to v1.4.0 and enhance changelog wit…
NaysKutzu May 22, 2026
6dd6551
feat: add configurable login page settings for default method, displa…
NaysKutzu May 22, 2026
e46d90d
feat: enhance Proxmox user management and admin updates functionality
NaysKutzu May 22, 2026
33e0e80
chore: update dependencies and improve code organization across compo…
NaysKutzu May 22, 2026
a77fbc7
chore: downgrade application version to v1.3.7.4 across multiple comp…
NaysKutzu Jun 7, 2026
03b6078
fix: Discord account linking functionality and improve error handling
NaysKutzu Jun 7, 2026
6e064c9
feat: enhance ticket management and notifications for admins and users
NaysKutzu Jun 7, 2026
d7acab1
feat: add email resend functionality for users and admins
NaysKutzu Jun 7, 2026
a43c783
refactor: update AdminOpenTicketsBanner component for improved visibi…
NaysKutzu Jun 7, 2026
b0ebec8
feat: enhance role management and permissions handling
NaysKutzu Jun 7, 2026
b14efdd
feat: add terms of service acceptance to login and registration forms
NaysKutzu Jun 7, 2026
b6fa080
feat: update
NaysKutzu Jun 7, 2026
5470819
feat: improve Wings integration and timezone handling
NaysKutzu Jun 7, 2026
d906c79
feat: enhance server transfer functionality and improve health checks
NaysKutzu Jun 7, 2026
21045cb
feat: enhance plugin validation and server filtering capabilities
NaysKutzu Jun 8, 2026
5a69e83
feat: improve plugin behavior and error reporting
NaysKutzu Jun 8, 2026
da8283d
feat: add custom badge feature to role management
NaysKutzu Jun 8, 2026
add8396
refactor: remove Cloudflare challenge handling from various components
NaysKutzu Jun 8, 2026
451ef6d
feat: enhance phpMyAdmin integration and optimize performance
NaysKutzu Jun 9, 2026
69fd8ae
refactor: reorganize and enhance code structure across multiple compo…
NaysKutzu Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@ For installation instructions, system requirements, and complete guides, please

<!-- COUNT-STATS:START -->

_Last updated: 2026-05-20T18:10:33.759Z_
_Last updated: 2026-06-09T14:28:24.443Z_

| Extension | Files | Lines |
| --- | ---: | ---: |
| `.php` | 518 | 132,170 |
| `.tsx` | 368 | 118,043 |
| `.ts` | 77 | 9,333 |
| `.yaml` | 3 | 5,950 |
| `.php` | 534 | 135,333 |
| `.tsx` | 379 | 120,491 |
| `.ts` | 87 | 10,391 |
| `.yaml` | 3 | 5,911 |
| `.rs` | 16 | 3,395 |
| `.sql` | 140 | 2,403 |
| `.sql` | 145 | 2,479 |
| `.yml` | 18 | 1,877 |
| `.css` | 7 | 445 |
| **Total** | 1,147 | 273,616 |
| `.css` | 7 | 459 |
| **Total** | 1,189 | 280,336 |

<!-- COUNT-STATS:END -->

Expand Down
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
{
"typescript.tsserver.maxTsServerMemory": 2048,
"files.watcherExclude": {
"**/.next/**": true,
"**/node_modules/**": true
},
"search.exclude": {
"**/.next": true,
"**/node_modules": true
},
"json.maxItemsComputed": 10000,
"editor.largeFileOptimizations": false,
"editor.renderWhitespace": "all",
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
# Changelog

## v1.3.7.4 STABLE

### Added

- Configurable default avatar provider in admin settings (Gravatar by default, with panel logo, UI Avatars, RoboHash, DiceBear, and custom URL template support). by @nayskutzu
- Admin SSO token settings: configure the default token lifetime in App settings, and optionally pass `expires_in` (minutes, 1–1440) when calling `POST /api/admin/users/{uuid}/sso-token`. by @nayskutzu
- Admin list pages (users, servers, VDS nodes, nodes, VM instances, spells, roles, tickets, and others) now remember search, filters, sort, and pagination in localStorage. by @nayskutzu
- Spells now support a configurable default Docker image (star icon on the spell Docker tab) so new servers use the correct runtime on first install when game images are updated. by @nayskutzu
- Server startup Docker image selection is locked to spell-configured images by default; admins can enable custom Docker image input under Settings → Servers (`server_allow_custom_docker_image`, default off). by @nayskutzu
- Admin server edit now configures Docker image on the Startup tab; create validates the selected image on submit so spell defaults (e.g. Java 25) persist correctly. by @nayskutzu
- Admin settings to configure the login page default sign-in method, display order of authentication options, and hidden methods (local, LDAP, passkey, email code, Discord, OIDC). by @nayskutzu
- Admins now get clearer notifications (including email notifications) when there are open support tickets; the ticket section can show all open tickets if the user is an admin. by @nayskutzu
- Users now receive an email notification when their support ticket is replied to, closed, or reopened. by @nayskutzu
- Added Discord to the links section in settings. displaying these links in a visible area, such as a footer, so users can easily access them.
- Added a button so you can retry sending an email to a user. by @nayskutzu
- Added the you accept our terms of service checkbox to the login page. by @nayskutzu
- Added a custom badge system to roles so you can set a custom badge for each role. by @nayskutzu

### Improved

- Login page CAPTCHA is shown after username/password (before Sign in), full-width in a bordered block, instead of between the two fields. by @nayskutzu
- Styles for the auth LDAP button were improved. by @nayskutzu
- Improved the amazing new role editor page that allows you to edit roles and permissions and thats idiot proof (I HOPE). by @nayskutzu
- Timezones were missmatched into the server backups page. by @nayskutzu
- Now you can sort users by newest and oldest and by username. by @nayskutzu
- Now you can click on the owner's name in the server details page to view the user's profile. by @nayskutzu
- Admin user edit page now includes a Potential Alts tab that finds other accounts sharing IP addresses with the viewed user. by @nayskutzu
- Potential alt detection now also compares server activity IPs and hidden browser sync identifiers (localStorage + cookie) collected across the panel. by @nayskutzu
- Registration can block new accounts when a browser/device already has the maximum allowed panel accounts, pointing users to their main account or support. by @nayskutzu
- Smarter component rendering for improved performance and reliability. by @nayskutzu
- Admins can clear device fingerprint records per user or globally from the Users admin area. by @nayskutzu
- Hopefully fixed some issues with the server console. by @nayskutzu
- Improved phpMyAdmin integration for seamless database management within the panel. by @nayskutzu

### Fixed

- Plugin uploads with mismatched conf.yml identifier/name and PHP entry class namespace now fail validation at install time instead of bricking the panel into maintenance mode. by @nayskutzu
- User server startup page now shows admin-assigned Docker images (e.g. Java 25) even when they are not in the spell image list, matching admin server edit behavior. by @nayskutzu
- Admin spell export now downloads the PTDL_v2-compatible export file instead of the raw database record, and legacy raw exports can still be imported. by @nayskutzu
- Admin Updates page plugin bulk updates now call the correct online install API (`/api/admin/plugins/online/install`) instead of a non-existent route. by @nayskutzu
- Issues related to Discord OAuth2 account linking and registration were fixed. by @nayskutzu
- Small issues regarding spells export behavior were fixed. by @nayskutzu
- Node connections now actually respect the behind proxy setting. by @nayskutzu
- Timezones were missmatched into the admin pages.by @nayskutzu
- Search bar in the server navbar was not working. by @nayskutzu
- Plugin behaviors and errors reporting was improved. by @nayskutzu
- Multiple issues with the server transfer dialog were fixed. by @nayskutzu
- Multiple issues with connections to the nodes were fixed. by @nayskutzu

## v1.3.7.3 STABLE

### Added
Expand Down Expand Up @@ -33,6 +82,7 @@
- Further enhanced the plugin installer for improved performance and reliability. by @nayskutzu
- Resolved a significant issue that previously prevented searching for allocations within the admin area. by @nayskutzu


### Improved

- Tickets were pretty cramped and didn't look good on big screens. by @nayskutzu
Expand Down
2 changes: 1 addition & 1 deletion app
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ define('APP_ADDONS_DIR', APP_STORAGE_DIR . 'addons');
define('APP_SOURCECODE_DIR', APP_DIR . 'app');
define('APP_ROUTES_DIR', APP_SOURCECODE_DIR . '/Api');
define('SYSTEM_KERNEL_NAME', php_uname('s'));
define('APP_VERSION', 'v1.3.7.3');
define('APP_VERSION', 'v1.3.7.4');
define('APP_UPSTREAM', 'stable');
define('TELEMETRY', true);
define('IS_CLI', true);
Expand Down
2 changes: 1 addition & 1 deletion backend/app/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public function registerApiRoutes(RouteCollection $routes): void
if (is_callable($register)) {
$register($routes);
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
self::getLogger()->error('Failed to load plugin routes from ' . $file->getPathname() . ': ' . $e->getMessage());
}
}
Expand Down
15 changes: 15 additions & 0 deletions backend/app/Chat/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ public static function getActivitiesByContextLikeAndNameIn(string $contextLike,
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}

/**
* @return string[]
*/
public static function getDistinctIpsByUserUuid(string $userUuid): array
{
$pdo = Database::getPdoConnection();
$stmt = $pdo->prepare(
'SELECT DISTINCT ip_address FROM ' . self::$table
. ' WHERE user_uuid = :user_uuid AND ip_address IS NOT NULL AND ip_address != \'\' ORDER BY ip_address'
);
$stmt->execute(['user_uuid' => $userUuid]);

return array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'ip_address');
}

public static function getCountByUserUuid(string $userUuid, ?string $search = null): int
{
$pdo = Database::getPdoConnection();
Expand Down
59 changes: 59 additions & 0 deletions backend/app/Chat/Allocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,43 @@ public static function getFreeCountByNodeId(int $nodeId): int
*
* @return array<int>
*/
/**
* Pick free allocation IDs that match specific IP/port slots (in order).
*
* @param array<int, array{ip: string, port: int}> $slots
* @param array<int> $excludeIds
*
* @return array<int>
*/
public static function pickFreeAllocationIdsForSlots(int $nodeId, array $slots, array $excludeIds = []): array
{
if ($nodeId <= 0 || empty($slots)) {
return [];
}

$excludeIds = array_values(array_filter(
array_map('intval', $excludeIds),
fn (int $id) => $id > 0
));

$picked = [];
foreach ($slots as $slot) {
$allocation = self::getByNodeIpPort($nodeId, (string) $slot['ip'], (int) $slot['port']);
if ($allocation === null || $allocation['server_id'] !== null) {
return [];
}

$id = (int) $allocation['id'];
if (in_array($id, $excludeIds, true) || in_array($id, $picked, true)) {
return [];
}

$picked[] = $id;
}

return $picked;
}

public static function pickFreeAllocationIdsForNode(int $nodeId, int $count, array $excludeIds = []): array
{
if ($nodeId <= 0 || $count <= 0) {
Expand Down Expand Up @@ -604,6 +641,28 @@ public static function deleteUnused(?int $nodeId = null, ?string $ip = null): in
return $stmt->rowCount();
}

/**
* Get allocation by node ID, IP, and port.
*/
public static function getByNodeIpPort(int $nodeId, string $ip, int $port): ?array
{
if ($nodeId <= 0) {
return null;
}

$pdo = Database::getPdoConnection();
$stmt = $pdo->prepare(
'SELECT * FROM ' . self::$table . ' WHERE node_id = :node_id AND ip = :ip AND port = :port LIMIT 1'
);
$stmt->execute([
'node_id' => $nodeId,
'ip' => $ip,
'port' => $port,
]);

return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}

/**
* Check if IP and port combination is unique for a node.
*/
Expand Down
27 changes: 27 additions & 0 deletions backend/app/Chat/MailQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,31 @@ public static function deleteAllMailQueueByUserId(string $userUuid): bool

return $stmt->execute(['user_uuid' => $userUuid]);
}

/**
* Re-queue a failed mail for delivery.
*/
public static function retry(int $id): bool
{
if ($id <= 0) {
return false;
}

$mail = self::getById($id);
if (!$mail || ($mail['status'] ?? '') !== 'failed' || ($mail['deleted'] ?? 'false') === 'true') {
return false;
}

$updated = self::update($id, [
'status' => 'pending',
'locked' => 'false',
]);
if (!$updated) {
return false;
}

\App\Helpers\IAsyncRunnerService::notifyMailPending($id);

return true;
}
}
19 changes: 19 additions & 0 deletions backend/app/Chat/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,23 @@ public static function getCountByRoleId(int $roleId): int

return (int) $stmt->fetchColumn();
}

/**
* Get distinct role IDs that have a specific permission or admin.root.
*
* @return int[]
*/
public static function getRoleIdsWithPermission(string $permission): array
{
$pdo = Database::getPdoConnection();
$stmt = $pdo->prepare(
'SELECT DISTINCT role_id FROM ' . self::$table . ' WHERE permission = :permission OR permission = :root'
);
$stmt->execute([
'permission' => $permission,
'root' => 'admin.root',
]);

return array_map(static fn (array $row): int => (int) $row['role_id'], $stmt->fetchAll(\PDO::FETCH_ASSOC));
}
}
2 changes: 1 addition & 1 deletion backend/app/Chat/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static function getAll(?string $search = null, int $limit = 10, int $offs
$params['search'] = '%' . $search . '%';
}

$sql .= ' LIMIT :limit OFFSET :offset';
$sql .= ' ORDER BY id DESC LIMIT :limit OFFSET :offset';
$stmt = $pdo->prepare($sql);
if (!empty($params)) {
foreach ($params as $key => $value) {
Expand Down
14 changes: 14 additions & 0 deletions backend/app/Chat/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ public static function getServerByAllocationId(int $allocationId): ?array
* @param string|null $uuid Filter by UUID (optional)
* @param string|null $uuidShort Filter by short UUID (optional)
* @param string|null $externalId Filter by external ID (optional)
* @param string|null $status Filter by server status (optional)
*/
public static function searchServers(
int $page = 1,
Expand All @@ -433,6 +434,7 @@ public static function searchServers(
?string $uuid = null,
?string $uuidShort = null,
?string $externalId = null,
?string $status = null,
): array {
$pdo = Database::getPdoConnection();

Expand Down Expand Up @@ -496,6 +498,11 @@ public static function searchServers(
$params['external_id'] = $externalId;
}

if ($status !== null && $status !== '') {
$where[] = 'status = :status';
$params['status'] = $status;
}

if (!empty($where)) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
Expand Down Expand Up @@ -716,6 +723,7 @@ public static function getAllServersWithRelations(): array
* Get the total number of servers.
*
* @param int|null $excludeOwnerId Exclude servers owned by this user ID (optional)
* @param string|null $status Filter by server status (optional)
*/
public static function getCount(
string $search = '',
Expand All @@ -728,6 +736,7 @@ public static function getCount(
?string $uuid = null,
?string $uuidShort = null,
?string $externalId = null,
?string $status = null,
): int {
$pdo = Database::getPdoConnection();
$sql = 'SELECT COUNT(*) FROM ' . self::$table;
Expand Down Expand Up @@ -784,6 +793,11 @@ public static function getCount(
$params['external_id'] = $externalId;
}

if ($status !== null && $status !== '') {
$where[] = 'status = :status';
$params['status'] = $status;
}

if (!empty($where)) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
Expand Down
19 changes: 19 additions & 0 deletions backend/app/Chat/ServerActivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ public static function getActivitiesByEvent(string $event, int $limit = 100): ar
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}

/**
* @return string[]
*/
public static function getDistinctIpsByUserId(int $userId): array
{
if ($userId <= 0) {
return [];
}

$pdo = Database::getPdoConnection();
$stmt = $pdo->prepare(
'SELECT DISTINCT ip FROM ' . self::$table
. ' WHERE user_id = :user_id AND ip IS NOT NULL AND ip != \'\' ORDER BY ip'
);
$stmt->execute(['user_id' => $userId]);

return array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'ip');
}

/**
* Get activities by user ID.
*
Expand Down
Loading
Loading