Merge branch 'main' into charles/rework-server

This commit is contained in:
notCharles 2024-06-01 15:54:03 -04:00
commit 118977c8c5
75 changed files with 468 additions and 433 deletions

View File

@ -17,9 +17,6 @@ CACHE_STORE=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
HASHIDS_SALT=
HASHIDS_LENGTH=8
MAIL_MAILER=log
MAIL_HOST=smtp.example.com
MAIL_PORT=25

View File

@ -34,7 +34,6 @@ jobs:
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: alittlebitofsalt1234
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
@ -97,7 +96,6 @@ jobs:
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: alittlebitofsalt1234
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
steps:

View File

@ -32,7 +32,6 @@ class AppSettingsCommand extends Command
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup
{--new-salt : Whether or not to generate a new salt for Hashids.}
{--url= : The URL that this Panel is running on.}
{--cache= : The cache driver backend to use.}
{--session= : The session driver backend to use.}
@ -61,10 +60,6 @@ class AppSettingsCommand extends Command
{
$this->variables['APP_TIMEZONE'] = 'UTC';
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
$this->variables['HASHIDS_SALT'] = str_random(20);
}
$this->output->comment(__('commands.appsettings.comment.url'));
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',

View File

@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
'id' => $schedule->id,
]));
} catch (\Throwable|\Exception $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);

View File

@ -1,15 +0,0 @@
<?php
namespace App\Contracts\Extensions;
use Hashids\HashidsInterface as VendorHashidsInterface;
interface HashidsInterface extends VendorHashidsInterface
{
/**
* Decode an encoded hashid and return the first result.
*
* @throws \InvalidArgumentException
*/
public function decodeFirst(string $encoded, string $default = null): mixed;
}

View File

@ -48,7 +48,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
*/
public function render(Request $request)
{
if (str($request->url())->contains('livewire')) {
if ($request->is('livewire/update')) {
Notification::make()
->title(static::class)
->body($this->getMessage())

View File

@ -25,7 +25,7 @@ class DynamicDatabaseConnection
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => decrypt($host->password),
'password' => $host->password,
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);

View File

@ -1,22 +0,0 @@
<?php
namespace App\Extensions;
use Hashids\Hashids as VendorHashids;
use App\Contracts\Extensions\HashidsInterface;
class Hashids extends VendorHashids implements HashidsInterface
{
/**
* {@inheritdoc}
*/
public function decodeFirst(string $encoded, string $default = null): mixed
{
$result = $this->decode($encoded);
if (!is_array($result)) {
return $default;
}
return array_first($result, null, $default);
}
}

View File

@ -4,9 +4,7 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Components\Tab;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class ApiKeyResource extends Resource
{
@ -16,7 +14,7 @@ class ApiKeyResource extends Resource
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
return static::getModel()::where('key_type', '2')->count() ?: null;
}
public static function canEdit($record): bool
@ -24,20 +22,6 @@ class ApiKeyResource extends Resource
return false;
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
public static function getRelations(): array
{
return [

View File

@ -19,30 +19,16 @@ class CreateApiKey extends CreateRecord
return $form
->schema([
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))),
Forms\Components\Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
Forms\Components\Hidden::make('user_id')
->default(auth()->user()->id)
->required(),
Forms\Components\Select::make('key_type')
Forms\Components\Hidden::make('key_type')
->inlineLabel()
->options(function (ApiKey $apiKey) {
$originalOptions = [
//ApiKey::TYPE_NONE => 'None',
ApiKey::TYPE_ACCOUNT => 'Account',
ApiKey::TYPE_APPLICATION => 'Application',
//ApiKey::TYPE_DAEMON_USER => 'Daemon User',
//ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application',
];
return collect($originalOptions)
->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key)
->all();
})
->selectablePlaceholder(false)
->required()
->default(ApiKey::TYPE_APPLICATION),
->default(ApiKey::TYPE_APPLICATION)
->required(),
Forms\Components\Fieldset::make('Permissions')
->columns([

View File

@ -5,10 +5,8 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Filament\Tables;
class ListApiKeys extends ListRecords
@ -19,16 +17,12 @@ class ListApiKeys extends ListRecords
{
return $table
->searchable(false)
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
->columns([
Tables\Columns\TextColumn::make('user.username')
->hidden()
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('key')
->copyable()
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)),
->state(fn (ApiKey $key) => $key->identifier . $key->token),
Tables\Columns\TextColumn::make('memo')
->label('Description')
@ -41,6 +35,7 @@ class ListApiKeys extends ListRecords
Tables\Columns\TextColumn::make('last_used_at')
->label('Last Used')
->placeholder('Not Used')
->dateTime()
->sortable(),
@ -48,13 +43,13 @@ class ListApiKeys extends ListRecords
->label('Created')
->dateTime()
->sortable(),
])
->filters([
//
Tables\Columns\TextColumn::make('user.username')
->label('Created By')
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
])
->actions([
Tables\Actions\DeleteAction::make(),
//Tables\Actions\EditAction::make()
]);
}
@ -64,22 +59,4 @@ class ListApiKeys extends ListRecords
Actions\CreateAction::make(),
];
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)
),
'account' => Tab::make('Account Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_ACCOUNT)
),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
}

View File

@ -74,15 +74,6 @@ class CreateDatabaseHost extends CreateRecord
]);
}
protected function mutateFormDataBeforeCreate(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getHeaderActions(): array
{
return [

View File

@ -76,15 +76,6 @@ class EditDatabaseHost extends EditRecord
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getFormActions(): array
{
return [];

View File

@ -28,13 +28,13 @@ class DatabasesRelationManager extends RelationManager
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
)
->formatStateUsing(fn (Database $database) => decrypt($database->password)),
->formatStateUsing(fn (Database $database) => $database->password),
Forms\Components\TextInput::make('remote')->label('Connections From'),
Forms\Components\TextInput::make('max_connections'),
Forms\Components\TextInput::make('JDBC')
->label('JDBC Connection String')
->columnSpanFull()
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode(decrypt($database->password)) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table

View File

@ -337,6 +337,7 @@ class CreateNode extends CreateRecord
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
@ -346,6 +347,7 @@ class CreateNode extends CreateRecord
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%'),

View File

@ -6,9 +6,11 @@ use App\Filament\Resources\NodeResource;
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
use App\Models\Node;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@ -374,6 +376,18 @@ class EditNode extends EditRecord
->rows(19)
->hintAction(CopyAction::make())
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(fn (NodeUpdateService $nodeUpdateService, Node $node) => $nodeUpdateService->handle($node, [], true)
&& Notification::make()->success()->title('Daemon Key Reset')->send()
&& $this->fillForm()
),
]),
]),
]),
]);

View File

@ -687,7 +687,7 @@ class CreateServer extends CreateRecord
->label('Container Labels')
->keyLabel('Title')
->valueLabel('Description')
->columnSpan(1),
->columnSpan(3),
]),
]),
]);

View File

@ -2,10 +2,12 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
use App\Facades\Activity;
use App\Models\ActivityLog;
use App\Models\ApiKey;
use App\Models\User;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
@ -20,8 +22,10 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
@ -99,12 +103,26 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
if ($this->getUser()->use_totp) {
return [
Placeholder::make('2FA already enabled!'),
Placeholder::make('2fa-already-enabled')
->label('Two Factor Authentication is currently enabled!'),
Textarea::make('backup-tokens')
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->rows(10)
->readOnly()
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->helperText('These will not be shown again!')
->label('Backup Tokens:'),
TextInput::make('2fa-disable-code')
->label('Disable 2FA')
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
];
}
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url] = $setupService->handle($this->getUser());
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
"users.{$this->getUser()->id}.2fa.state",
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
);
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
@ -147,9 +165,19 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
Placeholder::make('qr')
->label('Scan QR Code')
->content(fn () => new HtmlString("
<div style='width: 300px'>$image</div>
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
"))
->default('asdfasdf'),
->helperText('Setup Key: '. $secret),
TextInput::make('2facode')
->label('Code')
->requiredWith('2fapassword')
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
TextInput::make('2fapassword')
->label('Current Password')
->requiredWith('2facode')
->currentPassword()
->password()
->helperText('Enter your current password to verify.'),
];
}),
@ -158,7 +186,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->schema([
Grid::make('asdf')->columns(5)->schema([
Section::make('Create API Key')->columnSpan(3)->schema([
TextInput::make('description'),
TextInput::make('description')->required(),
TagsInput::make('allowed_ips')
->splitKeys([',', ' ', 'Tab'])
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
@ -182,8 +210,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$action->success();
}),
]),
Section::make('API Keys')->columnSpan(2)->schema([
Section::make('Keys')->columnSpan(2)->schema([
Repeater::make('keys')
->label('')
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
@ -235,4 +264,43 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
),
];
}
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
{
if ($token = $data['2facode'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$tokens = $service->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$service->handle($record, $token, false);
cache()->forget("users.$record->id.2fa.state");
}
return parent::handleRecordUpdate($record, $data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
Notification::make()
->title('Invalid 2FA Code')
->body($e->getMessage())
->color('danger')
->icon('tabler-2fa')
->danger()
->send();
$stopPropagation();
}
}
}

View File

@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
return new JsonResponse([
'node' => $node->id,
'token' => $key->identifier . decrypt($key->token),
'token' => $key->identifier . $key->token,
]);
}
}

View File

@ -24,8 +24,8 @@ class NodeDeploymentController extends ApplicationApiController
$data = $request->validated();
$nodes = $this->viableNodesService->handle(
$data['disk'] ?? 0,
$data['memory'] ?? 0,
$data['disk'] ?? 0,
$data['cpu'] ?? 0,
$data['tags'] ?? $data['location_ids'] ?? [],
);

View File

@ -65,9 +65,7 @@ class LoginCheckpointController extends AbstractLoginController
return $this->sendLoginResponse($user, $request);
}
} else {
$decrypted = decrypt($user->totp_secret);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
Event::dispatch(new ProvidedAuthenticationToken($user));
return $this->sendLoginResponse($user, $request);

View File

@ -41,7 +41,7 @@ class DaemonAuthenticate
/** @var Node $node */
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
if (hash_equals((string) decrypt($node->daemon_token), $parts[1])) {
if (hash_equals((string) $node->daemon_token, $parts[1])) {
$request->attributes->set('node', $node);
return $next($request);

View File

@ -56,11 +56,10 @@ class StoreServerRequest extends ApplicationApiRequest
// Automatic deployment rules
'deploy' => 'sometimes|required|array',
'deploy.locations' => 'array',
'deploy.locations.*' => 'integer|min:1',
'deploy.locations.*' => 'required_with:deploy.locations,integer|min:1',
'deploy.dedicated_ip' => 'required_with:deploy,boolean',
'deploy.port_range' => 'array',
'deploy.port_range.*' => 'string',
'start_on_completion' => 'sometimes|boolean',
];
}

View File

@ -22,7 +22,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property bool $has_alias
* @property \App\Models\Server|null $server
* @property \App\Models\Node $node
* @property string $hashid
*
* @method static \Database\Factories\AllocationFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|Allocation newModelQuery()
@ -88,14 +87,6 @@ class Allocation extends Model
return $this->getKeyName();
}
/**
* Return a hashid encoded string to represent the ID of the allocation.
*/
public function getHashidAttribute(): string
{
return app()->make('hashids')->encode($this->id);
}
/**
* Accessor to automatically provide the IP alias if defined.
*/

View File

@ -149,6 +149,7 @@ class ApiKey extends Model
'user_id' => 'int',
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
'token' => 'encrypted',
self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime',
'r_' . AdminAcl::RESOURCE_USERS => 'int',
@ -188,7 +189,7 @@ class ApiKey extends Model
$identifier = substr($token, 0, self::IDENTIFIER_LENGTH);
$model = static::where('identifier', $identifier)->first();
if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) {
if (!is_null($model) && $model->token === substr($token, strlen($identifier))) {
return $model;
}

View File

@ -2,9 +2,7 @@
namespace App\Models;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
use Illuminate\Support\Facades\DB;
/**
@ -64,6 +62,7 @@ class Database extends Model
'server_id' => 'integer',
'database_host_id' => 'integer',
'max_connections' => 'integer',
'password' => 'encrypted',
];
}
@ -72,26 +71,6 @@ class Database extends Model
return $this->getKeyName();
}
/**
* Resolves the database using the ID by checking if the value provided is a HashID
* string value, or just the ID to the database itself.
*
* @param mixed $value
* @param string|null $field
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function resolveRouteBinding($value, $field = null): ?\Illuminate\Database\Eloquent\Model
{
if (is_scalar($value) && ($field ?? $this->getRouteKeyName()) === 'id') {
$value = ctype_digit((string) $value)
? $value
: Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value);
}
return $this->where($field ?? $this->getRouteKeyName(), $value)->firstOrFail();
}
/**
* Gets the host database server associated with a database.
*/

View File

@ -60,6 +60,7 @@ class DatabaseHost extends Model
'id' => 'integer',
'max_databases' => 'integer',
'node_id' => 'integer',
'password' => 'encrypted',
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
];

View File

@ -63,10 +63,6 @@ class Node extends Model
*/
protected $hidden = ['daemon_token_id', 'daemon_token'];
public int $sum_memory;
public int $sum_disk;
public int $sum_cpu;
/**
* Fields that are mass assignable.
*/
@ -127,6 +123,7 @@ class Node extends Model
'cpu' => 'integer',
'daemon_listen' => 'integer',
'daemon_sftp' => 'integer',
'daemon_token' => 'encrypted',
'behind_proxy' => 'boolean',
'public' => 'boolean',
'maintenance_mode' => 'boolean',
@ -143,7 +140,7 @@ class Node extends Model
{
static::creating(function (self $node) {
$node->uuid = Str::uuid();
$node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH));
$node->daemon_token = Str::random(self::DAEMON_TOKEN_LENGTH);
$node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH);
return true;
@ -171,7 +168,7 @@ class Node extends Model
'debug' => false,
'uuid' => $this->uuid,
'token_id' => $this->daemon_token_id,
'token' => decrypt($this->daemon_token),
'token' => $this->daemon_token,
'api' => [
'host' => '0.0.0.0',
'port' => $this->daemon_listen,
@ -209,16 +206,6 @@ class Node extends Model
return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES);
}
/**
* Helper function to return the decrypted key for a node.
*/
public function getDecryptedKey(): string
{
return (string) decrypt(
$this->daemon_token
);
}
public function isUnderMaintenance(): bool
{
return $this->maintenance_mode;
@ -250,11 +237,28 @@ class Node extends Model
*/
public function isViable(int $memory, int $disk, int $cpu): bool
{
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
if ($this->memory_overallocate >= 0) {
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
if ($this->servers_sum_memory + $memory > $memoryLimit) {
return false;
}
}
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit && ($this->sum_cpu + $cpu) <= $cpuLimit;
if ($this->disk_overallocate >= 0) {
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
if ($this->servers_sum_disk + $disk > $diskLimit) {
return false;
}
}
if ($this->cpu_overallocate >= 0) {
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
return false;
}
}
return true;
}
public static function getForServerCreation()

View File

@ -4,10 +4,8 @@ namespace App\Models;
use Cron\CronExpression;
use Carbon\CarbonImmutable;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
/**
* @property int $id
@ -25,7 +23,6 @@ use App\Contracts\Extensions\HashidsInterface;
* @property \Carbon\Carbon|null $next_run_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $hashid
* @property \App\Models\Server $server
* @property \App\Models\Task[]|\Illuminate\Support\Collection $tasks
*/
@ -124,14 +121,6 @@ class Schedule extends Model
);
}
/**
* Return a hashid encoded string to represent the ID of the schedule.
*/
public function getHashidAttribute(): string
{
return Container::getInstance()->make(HashidsInterface::class)->encode($this->id);
}
/**
* Return tasks belonging to a schedule.
*/

View File

@ -52,14 +52,6 @@ class Subuser extends Model
];
}
/**
* Return a hashid encoded string to represent the ID of the subuser.
*/
public function getHashidAttribute(): string
{
return app()->make('hashids')->encode($this->id);
}
/**
* Gets the server associated with a subuser.
*/

View File

@ -2,10 +2,8 @@
namespace App\Models;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
/**
* @property int $id
@ -18,7 +16,6 @@ use App\Contracts\Extensions\HashidsInterface;
* @property bool $continue_on_failure
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $hashid
* @property \App\Models\Schedule $schedule
* @property \App\Models\Server $server
*/
@ -96,14 +93,6 @@ class Task extends Model
return $this->getKeyName();
}
/**
* Return a hashid encoded string to represent the ID of the task.
*/
public function getHashidAttribute(): string
{
return Container::getInstance()->make(HashidsInterface::class)->encode($this->id);
}
/**
* Return the schedule that a task belongs to.
*/

View File

@ -31,7 +31,7 @@ trait HasAccessTokens
'user_id' => $this->id,
'key_type' => ApiKey::TYPE_ACCOUNT,
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT),
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
'token' => $plain = Str::random(ApiKey::KEY_LENGTH),
'memo' => $memo ?? '',
'allowed_ips' => $ips ?? [],
]);

View File

@ -171,6 +171,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => 'boolean',
'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted',
];
}

View File

@ -60,7 +60,7 @@ class AppServiceProvider extends ServiceProvider
'daemon',
fn (Node $node, array $headers = []) => Http::acceptJson()
->asJson()
->withToken($node->getDecryptedKey())
->withToken($node->daemon_token)
->withHeaders($headers)
->withOptions(['verify' => (bool) app()->environment('production')])
->timeout(config('panel.guzzle.timeout'))

View File

@ -35,11 +35,13 @@ class AdminPanelProvider extends PanelProvider
->default()
->id('admin')
->path('admin')
->topNavigation(config('panel.filament.top-navigation', false))
->topNavigation(config('panel.filament.top-navigation', true))
->login()
->homeUrl('/')
->favicon('/pelican.ico')
->brandName('Pelican')
->favicon(config('app.favicon', '/pelican.ico'))
->brandName(config('app.name', 'Pelican'))
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->profile(EditProfile::class, false)
->colors([
'danger' => Color::Red,

View File

@ -1,26 +0,0 @@
<?php
namespace App\Providers;
use App\Extensions\Hashids;
use Illuminate\Support\ServiceProvider;
use App\Contracts\Extensions\HashidsInterface;
class HashidsServiceProvider extends ServiceProvider
{
/**
* Register the ability to use Hashids.
*/
public function register(): void
{
$this->app->singleton(HashidsInterface::class, function () {
return new Hashids(
config('hashids.salt', ''),
config('hashids.length', 0),
config('hashids.alphabet', 'abcdefghijkmlnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
);
});
$this->app->alias(HashidsInterface::class, 'hashids');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Providers;
use Illuminate\Http\Request;
use App\Models\Database;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
@ -29,11 +28,6 @@ class RouteServiceProvider extends ServiceProvider
return preg_match(self::FILE_PATH_REGEX, $request->getPathInfo()) === 1;
});
// This is needed to make use of the "resolveRouteBinding" functionality in the
// model. Without it you'll never trigger that logic flow thus resulting in a 404
// error because we request databases with a HashID, and not with a normal ID.
Route::model('database', Database::class);
$this->routes(function () {
Route::middleware('web')->group(function () {
Route::middleware(['auth.session', RequireTwoFactorAuthentication::class])

View File

@ -31,7 +31,7 @@ class KeyCreationService
$data = array_merge($data, [
'key_type' => $this->keyType,
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
'token' => encrypt(str_random(ApiKey::KEY_LENGTH)),
'token' => str_random(ApiKey::KEY_LENGTH),
]);
if ($this->keyType === ApiKey::TYPE_APPLICATION) {

View File

@ -86,9 +86,7 @@ class DatabaseManagementService
$data = array_merge($data, [
'server_id' => $server->id,
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => encrypt(
Utilities::randomStringWithSpecialCharacters(24)
),
'password' => Utilities::randomStringWithSpecialCharacters(24),
]);
return $this->connection->transaction(function () use ($data, &$database) {
@ -100,7 +98,7 @@ class DatabaseManagementService
$database->createUser(
$database->username,
$database->remote,
decrypt($database->password),
$database->password,
$database->max_connections
);
$database->assignUserToDatabase($database->database, $database->username, $database->remote);

View File

@ -33,7 +33,7 @@ class DatabasePasswordService
$this->dynamic->set('dynamic', $database->database_host_id);
$database->update([
'password' => encrypt($password),
'password' => $password,
]);
$database->dropUser($database->username, $database->remote);

View File

@ -28,7 +28,7 @@ class HostCreationService
{
return $this->connection->transaction(function () use ($data) {
$host = DatabaseHost::query()->create([
'password' => encrypt(array_get($data, 'password')),
'password' => array_get($data, 'password'),
'name' => array_get($data, 'name'),
'host' => array_get($data, 'host'),
'port' => array_get($data, 'port'),

View File

@ -26,9 +26,7 @@ class HostUpdateService
*/
public function handle(int $hostId, array $data): DatabaseHost
{
if (!empty(array_get($data, 'password'))) {
$data['password'] = encrypt($data['password']);
} else {
if (empty(array_get($data, 'password'))) {
unset($data['password']);
}

View File

@ -17,19 +17,17 @@ class FindViableNodesService
* are tossed out, as are any nodes marked as non-public, meaning automatic
* deployments should not be done against them.
*/
public function handle(int $disk = 0, int $memory = 0, int $cpu = 0, $tags = []): Collection
public function handle(int $memory = 0, int $disk = 0, int $cpu = 0, $tags = []): Collection
{
$nodes = Node::query()
->withSum('servers', 'disk')
->withSum('servers', 'memory')
->withSum('servers', 'disk')
->withSum('servers', 'cpu')
->where('public', true)
->get();
return $nodes
->filter(fn (Node $node) => !$tags || collect($node->tags)->intersect($tags))
->filter(fn (Node $node) => $node->servers_sum_disk + $disk <= $node->disk * (1 + $node->disk_overallocate / 100))
->filter(fn (Node $node) => $node->servers_sum_memory + $memory <= $node->memory * (1 + $node->memory_overallocate / 100))
->filter(fn (Node $node) => $node->servers_sum_cpu + $cpu <= $node->cpu * (1 + $node->cpu_overallocate / 100));
->filter(fn (Node $node) => $node->isViable($memory, $disk, $cpu));
}
}

View File

@ -25,6 +25,7 @@ class EggExporterService
'exported_at' => Carbon::now()->toAtomString(),
'name' => $egg->name,
'author' => $egg->author,
'uuid' => $egg->uuid,
'description' => $egg->description,
'features' => $egg->features,
'docker_images' => $egg->docker_images,

View File

@ -26,8 +26,11 @@ class EggImporterService
$parsed = $this->parser->handle($file);
return $this->connection->transaction(function () use ($parsed) {
$egg = (new Egg())->forceFill([
'uuid' => Uuid::uuid4()->toString(),
$uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString();
$egg = Egg::where('uuid', $uuid)->first() ?? new Egg();
$egg = $egg->forceFill([
'uuid' => $uuid,
'author' => Arr::get($parsed, 'author'),
'copy_script_from' => null,
]);

View File

@ -16,7 +16,7 @@ class NodeCreationService
public function handle(array $data): Node
{
$data['uuid'] = Uuid::uuid4()->toString();
$data['daemon_token'] = encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
return Node::query()->create($data);

View File

@ -63,7 +63,7 @@ class NodeJWTService
public function handle(Node $node, ?string $identifiedBy, string $algo = 'md5'): Plain
{
$identifier = hash($algo, $identifiedBy);
$config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->getDecryptedKey()));
$config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->daemon_token));
$builder = $config->builder(new TimestampDates())
->issuedBy(config('app.url'))

View File

@ -28,14 +28,14 @@ class NodeUpdateService
public function handle(Node $node, array $data, bool $resetToken = false): Node
{
if ($resetToken) {
$data['daemon_token'] = encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
}
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
/** @var \App\Models\Node $updated */
$updated = $node->replicate()->forceFill($data)->save();
$updated = $node->replicate();
$updated->forceFill($data)->save();
try {
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection
// address. This should alleviate issues where the node gets pointed to a "valid" FQDN that

View File

@ -109,8 +109,8 @@ class ServerCreationService
{
/** @var Collection<\App\Models\Node> $nodes */
$nodes = $this->findViableNodesService->handle(
Arr::get($data, 'disk', 0),
Arr::get($data, 'memory', 0),
Arr::get($data, 'disk', 0),
Arr::get($data, 'cpu', 0),
Arr::get($data, 'tags', []),
);
@ -154,6 +154,7 @@ class ServerCreationService
'database_limit' => Arr::get($data, 'database_limit') ?? 0,
'allocation_limit' => Arr::get($data, 'allocation_limit') ?? 0,
'backup_limit' => Arr::get($data, 'backup_limit') ?? 0,
'docker_labels' => Arr::get($data, 'docker_labels'),
]);
}

View File

@ -58,7 +58,9 @@ class TransferServerService
// Check if the node is viable for the transfer.
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk, IFNULL(SUM(servers.cpu), 0) as sum_cpu')
->withSum('servers', 'disk')
->withSum('servers', 'memory')
->withSum('servers', 'cpu')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();

View File

@ -32,9 +32,7 @@ class ToggleTwoFactorService
*/
public function handle(User $user, string $token, bool $toggleState = null): array
{
$secret = decrypt($user->totp_secret);
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('panel.auth.2fa.window'));
$isValidToken = $this->google2FA->verifyKey($user->totp_secret, $token, config()->get('panel.auth.2fa.window'));
if (!$isValidToken) {
throw new TwoFactorAuthenticationTokenInvalid();

View File

@ -26,7 +26,7 @@ class TwoFactorSetupService
throw new \RuntimeException($exception->getMessage(), 0, $exception);
}
$user->totp_secret = encrypt($secret);
$user->totp_secret = $secret;
$user->save();
$company = urlencode(preg_replace('/\s/', '', config('app.name')));

View File

@ -45,7 +45,7 @@ class ServerDatabaseTransformer extends BaseTransformer
{
return $this->item($model, function (Database $model) {
return [
'password' => decrypt($model->password),
'password' => $model->password,
];
}, 'database_password');
}

View File

@ -55,7 +55,7 @@ class ActivityLogTransformer extends BaseClientTransformer
$properties = $model->properties
->mapWithKeys(function ($value, $key) use ($model) {
if ($key === 'ip' && !$model->actor->is($this->request->user())) {
if ($key === 'ip' && $model->actor && !$model->actor->is($this->request->user())) {
return [$key => '[hidden]'];
}

View File

@ -6,22 +6,11 @@ use App\Models\Database;
use League\Fractal\Resource\Item;
use App\Models\Permission;
use League\Fractal\Resource\NullResource;
use App\Contracts\Extensions\HashidsInterface;
class DatabaseTransformer extends BaseClientTransformer
{
protected array $availableIncludes = ['password'];
private HashidsInterface $hashids;
/**
* Handle dependency injection.
*/
public function handle(HashidsInterface $hashids)
{
$this->hashids = $hashids;
}
public function getResourceName(): string
{
return Database::RESOURCE_NAME;
@ -32,7 +21,7 @@ class DatabaseTransformer extends BaseClientTransformer
$model->loadMissing('host');
return [
'id' => $this->hashids->encode($model->id),
'id' => $model->id,
'host' => [
'address' => $model->getRelation('host')->host,
'port' => $model->getRelation('host')->port,
@ -55,7 +44,7 @@ class DatabaseTransformer extends BaseClientTransformer
return $this->item($database, function (Database $model) {
return [
'password' => decrypt($model->password),
'password' => $model->password,
];
}, 'database_password');
}

View File

@ -6,7 +6,6 @@ return [
App\Providers\BackupsServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\HashidsServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class,
];

View File

@ -16,7 +16,6 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.2",
"guzzlehttp/guzzle": "^7.8.1",
"hashids/hashids": "~5.0.0",
"laracasts/utilities": "~3.2.2",
"laravel/framework": "^11.7",
"laravel/helpers": "^1.7",
@ -34,7 +33,8 @@
"s1lentium/iptools": "~1.2.0",
"spatie/laravel-fractal": "^6.2",
"spatie/laravel-query-builder": "^5.8.1",
"symfony/mailgun-mailer": "^7.0.7",
"symfony/http-client": "^7.1",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.0.7",
"symfony/yaml": "^7.0.7",
"webbingbrasil/filament-copyactions": "^3.0.1",

257
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dc1c1e5ee766f2e31e84c50670fa0c98",
"content-hash": "328bdd29cb83793ec0a99b2210941740",
"packages": [
{
"name": "abdelhamiderrahmouni/filament-monaco-editor",
@ -2613,75 +2613,6 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
{
"name": "hashids/hashids",
"version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/vinkla/hashids.git",
"reference": "197171016b77ddf14e259e186559152eb3f8cf33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vinkla/hashids/zipball/197171016b77ddf14e259e186559152eb3f8cf33",
"reference": "197171016b77ddf14e259e186559152eb3f8cf33",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"suggest": {
"ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).",
"ext-gmp": "Required to use GNU multiple precision mathematics (*)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Hashids\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ivan Akimov",
"email": "ivan@barreleye.com"
},
{
"name": "Vincent Klaiber",
"email": "hello@doubledip.se"
}
],
"description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
"homepage": "https://hashids.org/php",
"keywords": [
"bitly",
"decode",
"encode",
"hash",
"hashid",
"hashids",
"ids",
"obfuscate",
"youtube"
],
"support": {
"issues": "https://github.com/vinkla/hashids/issues",
"source": "https://github.com/vinkla/hashids/tree/5.0.2"
},
"time": "2023-02-23T15:00:54+00:00"
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "3.5.6",
@ -7722,6 +7653,178 @@
],
"time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/http-client",
"version": "v7.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "2266f9813ed7d8c84e04627edead7b7fd249d6e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/2266f9813ed7d8c84e04627edead7b7fd249d6e9",
"reference": "2266f9813ed7d8c84e04627edead7b7fd249d6e9",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "^3.4.1",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/amp": "^2.5",
"amphp/http-client": "^4.2.1",
"amphp/http-tunnel": "^1.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.1.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-13T15:35:37+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "20414d96f391677bf80078aa55baece78b82647d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d",
"reference": "20414d96f391677bf80078aa55baece78b82647d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.0.7",
@ -7994,16 +8097,16 @@
},
{
"name": "symfony/mailgun-mailer",
"version": "v7.0.7",
"version": "v7.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailgun-mailer.git",
"reference": "e9bb8fdbdd79334a8a88bdd233204315abd992c5"
"reference": "aa5afbe846bbc8bde6afe2602f0427834b872f55"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/e9bb8fdbdd79334a8a88bdd233204315abd992c5",
"reference": "e9bb8fdbdd79334a8a88bdd233204315abd992c5",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/aa5afbe846bbc8bde6afe2602f0427834b872f55",
"reference": "aa5afbe846bbc8bde6afe2602f0427834b872f55",
"shasum": ""
},
"require": {
@ -8043,7 +8146,7 @@
"description": "Symfony Mailgun Mailer Bridge",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.0.7"
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.1.0"
},
"funding": [
{
@ -8059,7 +8162,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:29:19+00:00"
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/mime",
@ -13061,5 +13164,5 @@
"ext-zip": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@ -1,15 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Hashids Configuration
|--------------------------------------------------------------------------
|
| Here are the settings that control the Hashids setup and usage in the panel.
|
*/
'salt' => env('HASHIDS_SALT'),
'length' => env('HASHIDS_LENGTH', 8),
'alphabet' => env('HASHIDS_ALPHABET', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'),
];

View File

@ -26,7 +26,7 @@ class ApiKeyFactory extends Factory
return [
'key_type' => ApiKey::TYPE_APPLICATION,
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION),
'token' => $token ?: $token = encrypt(Str::random(ApiKey::KEY_LENGTH)),
'token' => $token ?: $token = Str::random(ApiKey::KEY_LENGTH),
'allowed_ips' => null,
'memo' => 'Test Function Key',
'created_at' => Carbon::now(),

View File

@ -27,7 +27,7 @@ class DatabaseFactory extends Factory
'database' => Str::random(10),
'username' => Str::random(10),
'remote' => '%',
'password' => $password ?: encrypt('test123'),
'password' => $password ?: 'test123',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];

View File

@ -3,7 +3,6 @@
namespace Database\Factories;
use App\Models\DatabaseHost;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Factories\Factory;
class DatabaseHostFactory extends Factory
@ -25,7 +24,7 @@ class DatabaseHostFactory extends Factory
'host' => $this->faker->unique()->ipv4(),
'port' => 3306,
'username' => $this->faker->colorName(),
'password' => Crypt::encrypt($this->faker->word()),
'password' => $this->faker->word(),
];
}
}

View File

@ -5,7 +5,6 @@ namespace Database\Factories;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use App\Models\Node;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Factories\Factory;
class NodeFactory extends Factory
@ -37,7 +36,7 @@ class NodeFactory extends Factory
'cpu_overallocate' => 0,
'upload_size' => 100,
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
'daemon_token' => Crypt::encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
'daemon_listen' => 8080,
'daemon_sftp' => 2022,
'daemon_base' => '/var/lib/panel/volumes',

View File

@ -0,0 +1,81 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$keys = DB::table('api_keys')->get();
foreach ($keys as $key) {
try {
$reEncrypted = encrypt(decrypt($key->token), false);
DB::table('api_keys')
->where('id', $key->id)
->update(['token' => $reEncrypted]);
} catch (Exception $exception) {
logger()->error($exception->getMessage());
}
}
$databases = DB::table('databases')->get();
foreach ($databases as $database) {
try {
$reEncrypted = encrypt(decrypt($database->password), false);
DB::table('databases')
->where('id', $database->id)
->update(['password' => $reEncrypted]);
} catch (Exception $exception) {
logger()->error($exception->getMessage());
}
}
$databaseHosts = DB::table('database_hosts')->get();
foreach ($databaseHosts as $host) {
try {
$reEncrypted = encrypt(decrypt($host->password), false);
DB::table('database_hosts')
->where('id', $host->id)
->update(['password' => $reEncrypted]);
} catch (Exception $exception) {
logger()->error($exception->getMessage());
}
}
$nodes = DB::table('nodes')->get();
foreach ($nodes as $node) {
try {
$reEncrypted = encrypt(decrypt($node->daemon_token), false);
DB::table('nodes')
->where('id', $node->id)
->update(['daemon_token' => $reEncrypted]);
} catch (Exception $exception) {
logger()->error($exception->getMessage());
}
}
$users = DB::table('users')->get();
foreach ($users as $user) {
try {
$reEncrypted = encrypt(decrypt($user->totp_secret), false);
DB::table('users')
->where('id', $user->id)
->update(['totp_secret' => $reEncrypted]);
} catch (Exception $exception) {
logger()->error($exception->getMessage());
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// No need to do anything
}
};

View File

@ -23,7 +23,7 @@ return [
'2fa_disabled' => '2-Factor authentication has been disabled for :email.',
],
'schedule' => [
'output_line' => 'Dispatching job for first task in `:schedule` (:hash).',
'output_line' => 'Dispatching job for first task in `:schedule` (:id).',
],
'maintenance' => [
'deleting_service_backup' => 'Deleting service backup file :file.',

View File

@ -2,6 +2,10 @@
# Pelican Panel
![Total Downloads](https://img.shields.io/github/downloads/pelican-dev/panel/total?style=flat&label=Total%20Downloads&labelColor=rgba(0%2C%2070%2C%20114%2C%201)&color=rgba(255%2C%20255%2C%20255%2C%201))
![Latest Release](https://img.shields.io/github/v/release/pelican-dev/panel?style=flat&label=Latest%20Release&labelColor=rgba(0%2C%2070%2C%20114%2C%201)&color=rgba(255%2C%20255%2C%20255%2C%201))
Pelican Panel is an open-source, web-based application designed for easy management of game servers.
It offers a user-friendly interface for deploying, configuring, and managing servers, with features like real-time resource monitoring, Docker container isolation, and extensive customization options.
Ideal for both individual gamers and hosting companies, it simplifies server administration without requiring deep technical knowledge.

View File

@ -33,7 +33,7 @@
</tr>
@foreach($keys as $key)
<tr>
<td><code>{{ $key->identifier }}{{ decrypt($key->token) }}</code></td>
<td><code>{{ $key->identifier }}{{ $key->token }}</code></td>
<td>{{ $key->memo }}</td>
<td>
@if(!is_null($key->last_used_at))

View File

@ -49,7 +49,7 @@
</tr>
@foreach ($nodes as $node)
<tr>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->getDecryptedKey() }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemon_listen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->daemon_token }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemon_listen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td>{{ $node->memory }} MiB</td>
<td>{{ $node->disk }} MiB</td>

View File

@ -37,7 +37,7 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
$this
->withHeader('Accept', 'application/vnd.panel.v1+json')
->withHeader('Authorization', 'Bearer ' . $this->key->identifier . decrypt($this->key->token));
->withHeader('Authorization', 'Bearer ' . $this->key->identifier . $this->key->token);
}
public function getApiUser(): User
@ -57,7 +57,7 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
{
$this->key = $this->createApiKey($user, $permissions);
$this->withHeader('Authorization', 'Bearer ' . $this->key->identifier . decrypt($this->key->token));
$this->withHeader('Authorization', 'Bearer ' . $this->key->identifier . $this->key->token);
return $this->key;
}

View File

@ -71,7 +71,7 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
$key = ApiKey::query()->where('identifier', $response->json('attributes.identifier'))->firstOrFail();
$this->assertJsonTransformedWith($response->json('attributes'), $key);
$response->assertJsonPath('meta.secret_token', decrypt($key->token));
$response->assertJsonPath('meta.secret_token', $key->token);
$this->assertActivityFor('user:api-key.create', $user, [$key, $user]);
}

View File

@ -5,7 +5,6 @@ namespace App\Tests\Integration\Api\Client\Server\Database;
use App\Models\Subuser;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Contracts\Extensions\HashidsInterface;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Databases\DatabaseManagementService;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
@ -39,23 +38,22 @@ class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase
->expects($method === 'POST' ? 'handle' : 'delete')
->andReturn($method === 'POST' ? 'foo' : null);
$hashids = $this->app->make(HashidsInterface::class);
// This is the only valid call for this test, accessing the database for the same
// server that the API user is the owner of.
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $hashids->encode($database1->id) . $endpoint))
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $database1->id . $endpoint))
->assertStatus($method === 'DELETE' ? 204 : 200);
// This request fails because the database is valid for that server but the user
// making the request is not authorized to perform that action.
$this->actingAs($user)->json($method, $this->link($server2, '/databases/' . $hashids->encode($database2->id) . $endpoint))->assertForbidden();
$this->actingAs($user)->json($method, $this->link($server2, '/databases/' . $database2->id . $endpoint))->assertForbidden();
// Both of these should report a 404 error due to the database being linked to
// servers that are not the same as the server in the request, or are assigned
// to a server for which the user making the request has no access to.
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $hashids->encode($database2->id) . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server2, '/databases/' . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server3, '/databases/' . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $database2->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server1, '/databases/' . $database3->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server2, '/databases/' . $database3->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server3, '/databases/' . $database3->id . $endpoint))->assertNotFound();
}
public static function methodDataProvider(): array

View File

@ -62,7 +62,7 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase
$this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.');
$this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection, 'Failed asserting that websocket connection address uses expected Daemon endpoint.');
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->getDecryptedKey()));
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token));
$config->setValidationConstraints(new SignedWith(new Sha256(), $key));
/** @var \Lcobucci\JWT\Token\Plain $token */
$token = $config->parser()->parse($response->json('data.token'));
@ -107,7 +107,7 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase
$response->assertOk();
$response->assertJsonStructure(['data' => ['token', 'socket']]);
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->getDecryptedKey()));
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token));
$config->setValidationConstraints(new SignedWith(new Sha256(), $key));
/** @var \Lcobucci\JWT\Token\Plain $token */
$token = $config->parser()->parse($response->json('data.token'));

View File

@ -85,8 +85,7 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
/** @var \PragmaRX\Google2FA\Google2FA $service */
$service = $this->app->make(Google2FA::class);
$secret = decrypt($user->totp_secret);
$token = $service->getCurrentOtp($secret);
$token = $service->getCurrentOtp($user->totp_secret);
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [
'code' => $token,

View File

@ -94,7 +94,7 @@ class DaemonAuthenticateTest extends MiddlewareTestCase
public function testSuccessfulMiddlewareProcess(): void
{
$node = Node::factory()->create();
$node->daemon_token = encrypt('the_same');
$node->daemon_token = 'the_same';
$node->save();
$this->request->expects('route->getName')->withNoArgs()->andReturn('random.route');

View File

@ -229,6 +229,6 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
{
$node = $node ?? $this->server->node;
$this->withHeader('Authorization', 'Bearer ' . $node->daemon_token_id . '.' . decrypt($node->daemon_token));
$this->withHeader('Authorization', 'Bearer ' . $node->daemon_token_id . '.' . $node->daemon_token);
}
}