mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-08 12:58:37 +02:00
Merge branch 'main' into filament-v4
Oh man, this is a disaster.
This commit is contained in:
commit
d4da111a14
34
app/Enums/WebhookType.php
Normal file
34
app/Enums/WebhookType.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
|
||||
enum WebhookType: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case Regular = 'regular';
|
||||
case Discord = 'discord';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return trans('admin/webhook.' . $this->value);
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Regular => null,
|
||||
self::Discord => 'blurple',
|
||||
};
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Regular => 'tabler-world-www',
|
||||
self::Discord => 'tabler-brand-discord',
|
||||
};
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class AvatarProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
public static function getProvider(string $id): ?self
|
||||
{
|
||||
return Arr::get(static::$providers, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, static>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return static::$providers;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function get(User $user): ?string;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
}
|
14
app/Extensions/Avatar/AvatarSchemaInterface.php
Normal file
14
app/Extensions/Avatar/AvatarSchemaInterface.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
interface AvatarSchemaInterface
|
||||
{
|
||||
public function getId(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
public function get(User $user): ?string;
|
||||
}
|
55
app/Extensions/Avatar/AvatarService.php
Normal file
55
app/Extensions/Avatar/AvatarService.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AvatarService
|
||||
{
|
||||
/** @var AvatarSchemaInterface[] */
|
||||
private array $schemas = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly bool $allowUploadedAvatars,
|
||||
private readonly string $activeSchema,
|
||||
) {}
|
||||
|
||||
public function get(string $id): ?AvatarSchemaInterface
|
||||
{
|
||||
return array_get($this->schemas, $id);
|
||||
}
|
||||
|
||||
public function getActiveSchema(): ?AvatarSchemaInterface
|
||||
{
|
||||
return $this->get($this->activeSchema);
|
||||
}
|
||||
|
||||
public function getAvatarUrl(User $user): ?string
|
||||
{
|
||||
if ($this->allowUploadedAvatars) {
|
||||
$path = "avatars/$user->id.png";
|
||||
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getActiveSchema()?->get($user);
|
||||
}
|
||||
|
||||
public function register(AvatarSchemaInterface $schema): void
|
||||
{
|
||||
if (array_key_exists($schema->getId(), $this->schemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function getMappings(): array
|
||||
{
|
||||
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
|
||||
}
|
||||
}
|
@ -1,24 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
namespace App\Extensions\Avatar\Schemas;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Avatar\AvatarSchemaInterface;
|
||||
use App\Models\User;
|
||||
|
||||
class GravatarProvider extends AvatarProvider
|
||||
class GravatarSchema implements AvatarSchemaInterface
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gravatar';
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Gravatar';
|
||||
}
|
||||
|
||||
public function get(User $user): string
|
||||
{
|
||||
return 'https://gravatar.com/avatar/' . md5($user->email);
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
namespace App\Extensions\Avatar\Schemas;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Avatar\AvatarSchemaInterface;
|
||||
use App\Models\User;
|
||||
|
||||
class UiAvatarsProvider extends AvatarProvider
|
||||
class UiAvatarsSchema implements AvatarSchemaInterface
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
@ -22,9 +22,4 @@ class UiAvatarsProvider extends AvatarProvider
|
||||
// UI Avatars is the default of filament so just return null here
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
48
app/Extensions/Captcha/CaptchaService.php
Normal file
48
app/Extensions/Captcha/CaptchaService.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha;
|
||||
|
||||
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CaptchaService
|
||||
{
|
||||
/** @var array<string, CaptchaSchemaInterface> */
|
||||
private array $schemas = [];
|
||||
|
||||
/**
|
||||
* @return CaptchaSchemaInterface[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->schemas;
|
||||
}
|
||||
|
||||
public function get(string $id): ?CaptchaSchemaInterface
|
||||
{
|
||||
return array_get($this->schemas, $id);
|
||||
}
|
||||
|
||||
public function register(CaptchaSchemaInterface $schema): void
|
||||
{
|
||||
if (array_key_exists($schema->getId(), $this->schemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig());
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
|
||||
/** @return Collection<CaptchaSchemaInterface> */
|
||||
public function getActiveSchemas(): Collection
|
||||
{
|
||||
return collect($this->schemas)
|
||||
->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled());
|
||||
}
|
||||
|
||||
public function getActiveSchema(): ?CaptchaSchemaInterface
|
||||
{
|
||||
return $this->getActiveSchemas()->first();
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class CaptchaProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function get(?string $id = null): array|self
|
||||
{
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function getComponent(): Component;
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
|
||||
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
|
||||
->label('Site Key')
|
||||
->placeholder('Site Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SITE_KEY")),
|
||||
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
|
||||
->label('Secret Key')
|
||||
->placeholder('Secret Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return env("CAPTCHA_{$id}_ENABLED", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array
|
||||
{
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'validateResponse not defined',
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
59
app/Extensions/Captcha/Schemas/BaseSchema.php
Normal file
59
app/Extensions/Captcha/Schemas/BaseSchema.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class BaseSchema
|
||||
{
|
||||
abstract public function getId(): string;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::upper($this->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
|
||||
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
|
||||
->label('Site Key')
|
||||
->placeholder('Site Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SITE_KEY")),
|
||||
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
|
||||
->label('Secret Key')
|
||||
->placeholder('Secret Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
|
||||
];
|
||||
}
|
||||
}
|
30
app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php
Normal file
30
app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
|
||||
interface CaptchaSchemaInterface
|
||||
{
|
||||
public function getId(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array;
|
||||
|
||||
public function isEnabled(): bool;
|
||||
|
||||
public function getFormComponent(): Component;
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array;
|
||||
|
||||
public function getIcon(): ?string;
|
||||
|
||||
public function validateResponse(?string $captchaResponse = null): void;
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Forms\Fields;
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use App\Rules\ValidTurnstileCaptcha;
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class TurnstileCaptcha extends Field
|
||||
class Component extends Field
|
||||
{
|
||||
protected string $viewIdentifier = 'turnstile';
|
||||
|
||||
@ -19,8 +18,6 @@ class TurnstileCaptcha extends Field
|
||||
|
||||
$this->required();
|
||||
|
||||
$this->after(function (TurnstileCaptcha $component) {
|
||||
$component->rule(new ValidTurnstileCaptcha());
|
||||
});
|
||||
$this->rule(new Rule());
|
||||
}
|
||||
}
|
23
app/Extensions/Captcha/Schemas/Turnstile/Rule.php
Normal file
23
app/Extensions/Captcha/Schemas/Turnstile/Rule.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use App\Extensions\Captcha\CaptchaService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class Rule implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
try {
|
||||
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
$fail('Captcha validation failed: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use Filament\Schemas\Components\Component;
|
||||
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
|
||||
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
|
||||
use App\Extensions\Captcha\Schemas\BaseSchema;
|
||||
use Exception;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class TurnstileProvider extends CaptchaProvider
|
||||
class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'turnstile';
|
||||
}
|
||||
|
||||
public function getComponent(): Component
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return TurnstileCaptcha::make('turnstile');
|
||||
return env('CAPTCHA_TURNSTILE_ENABLED', false);
|
||||
}
|
||||
|
||||
public function getFormComponent(): BaseComponent
|
||||
{
|
||||
return Component::make('turnstile');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,7 +39,7 @@ class TurnstileProvider extends CaptchaProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
* @return BaseComponent[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
@ -52,24 +57,18 @@ class TurnstileProvider extends CaptchaProvider
|
||||
->label(trans('admin/setting.captcha.info_label'))
|
||||
->columnSpan(2)
|
||||
->state(new HtmlString(trans('admin/setting.captcha.info'))),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return 'tabler-brand-cloudflare';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
* @throws Exception
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array
|
||||
public function validateResponse(?string $captchaResponse = null): void
|
||||
{
|
||||
$captchaResponse ??= request()->get('cf-turnstile-response');
|
||||
|
||||
@ -84,22 +83,33 @@ class TurnstileProvider extends CaptchaProvider
|
||||
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'secret' => $secret,
|
||||
'response' => $captchaResponse,
|
||||
]);
|
||||
])
|
||||
->json();
|
||||
|
||||
return count($response->json()) ? $response->json() : [
|
||||
'success' => false,
|
||||
'message' => 'Unknown error occurred, please try again',
|
||||
];
|
||||
if (!$response['success']) {
|
||||
match ($response['error-codes'][0] ?? null) {
|
||||
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
|
||||
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
|
||||
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
|
||||
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
|
||||
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
|
||||
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
|
||||
default => throw new Exception('An internal error happened while validating the response.'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!$this->verifyDomain($response['hostname'] ?? '')) {
|
||||
throw new Exception('Domain verification failed.');
|
||||
}
|
||||
}
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
|
||||
private function verifyDomain(string $hostname): bool
|
||||
{
|
||||
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$requestUrl ??= request()->url;
|
||||
$requestUrl = parse_url($requestUrl);
|
||||
$requestUrl = parse_url(request()->url());
|
||||
|
||||
return $hostname === array_get($requestUrl, 'host');
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
abstract class FeatureProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @param string[] $id
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function getProviders(string|array|null $id = null): array|self
|
||||
{
|
||||
if (is_array($id)) {
|
||||
return array_intersect_key(static::$providers, array_flip($id));
|
||||
}
|
||||
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
/**
|
||||
* A matching subset string (case-insensitive) from the console output
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
abstract public function getListeners(): array;
|
||||
|
||||
abstract public function getAction(): Action;
|
||||
}
|
15
app/Extensions/Features/FeatureSchemaInterface.php
Normal file
15
app/Extensions/Features/FeatureSchemaInterface.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
|
||||
interface FeatureSchemaInterface
|
||||
{
|
||||
/** @return string[] */
|
||||
public function getListeners(): array;
|
||||
|
||||
public function getId(): string;
|
||||
|
||||
public function getAction(): Action;
|
||||
}
|
52
app/Extensions/Features/FeatureService.php
Normal file
52
app/Extensions/Features/FeatureService.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
class FeatureService
|
||||
{
|
||||
/** @var FeatureSchemaInterface[] */
|
||||
private array $schemas = [];
|
||||
|
||||
/**
|
||||
* @return FeatureSchemaInterface[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->schemas;
|
||||
}
|
||||
|
||||
public function get(string $id): ?FeatureSchemaInterface
|
||||
{
|
||||
return array_get($this->schemas, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $features
|
||||
* @return FeatureSchemaInterface[]
|
||||
*/
|
||||
public function getActiveSchemas(?array $features = []): array
|
||||
{
|
||||
return collect($this->schemas)->only($features)->all();
|
||||
}
|
||||
|
||||
public function register(FeatureSchemaInterface $schema): void
|
||||
{
|
||||
if (array_key_exists($schema->getId(), $this->schemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $features
|
||||
* @return array<string, array<string>>
|
||||
*/
|
||||
public function getMappings(?array $features = []): array
|
||||
{
|
||||
return collect($this->getActiveSchemas($features))
|
||||
->mapWithKeys(fn (FeatureSchemaInterface $schema) => [
|
||||
$schema->getId() => $schema->getListeners(),
|
||||
])->all();
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
namespace App\Extensions\Features\Schemas;
|
||||
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
@ -15,18 +16,12 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class GSLToken extends FeatureProvider
|
||||
class GSLTokenSchema implements FeatureSchemaInterface
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
@ -122,9 +117,4 @@ class GSLToken extends FeatureProvider
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
namespace App\Extensions\Features\Schemas;
|
||||
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
@ -12,15 +13,9 @@ use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
class JavaVersion extends FeatureProvider
|
||||
class JavaVersionSchema implements FeatureSchemaInterface
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
@ -92,9 +87,4 @@ class JavaVersion extends FeatureProvider
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
namespace App\Extensions\Features\Schemas;
|
||||
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
@ -9,17 +10,11 @@ use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class MinecraftEula extends FeatureProvider
|
||||
class MinecraftEulaSchema implements FeatureSchemaInterface
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
@ -63,9 +58,4 @@ class MinecraftEula extends FeatureProvider
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,19 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
namespace App\Extensions\Features\Schemas;
|
||||
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class PIDLimit extends FeatureProvider
|
||||
class PIDLimitSchema implements FeatureSchemaInterface
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
@ -68,9 +63,4 @@ class PIDLimit extends FeatureProvider
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,19 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
namespace App\Extensions\Features\Schemas;
|
||||
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class SteamDiskSpace extends FeatureProvider
|
||||
class SteamDiskSpaceSchema implements FeatureSchemaInterface
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
@ -56,9 +51,4 @@ class SteamDiskSpace extends FeatureProvider
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
35
app/Extensions/OAuth/OAuthSchemaInterface.php
Normal file
35
app/Extensions/OAuth/OAuthSchemaInterface.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
interface OAuthSchemaInterface
|
||||
{
|
||||
public function getId(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
public function getConfigKey(): string;
|
||||
|
||||
/** @return ?class-string */
|
||||
public function getSocialiteProvider(): ?string;
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getServiceConfig(): array;
|
||||
|
||||
/** @return Component[] */
|
||||
public function getSettingsForm(): array;
|
||||
|
||||
/** @return Step[] */
|
||||
public function getSetupSteps(): array;
|
||||
|
||||
public function getIcon(): ?string;
|
||||
|
||||
public function getHexColor(): ?string;
|
||||
|
||||
public function isEnabled(): bool;
|
||||
}
|
46
app/Extensions/OAuth/OAuthService.php
Normal file
46
app/Extensions/OAuth/OAuthService.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class OAuthService
|
||||
{
|
||||
/** @var OAuthSchemaInterface[] */
|
||||
private array $schemas = [];
|
||||
|
||||
/** @return OAuthSchemaInterface[] */
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->schemas;
|
||||
}
|
||||
|
||||
public function get(string $id): ?OAuthSchemaInterface
|
||||
{
|
||||
return array_get($this->schemas, $id);
|
||||
}
|
||||
|
||||
/** @return OAuthSchemaInterface[] */
|
||||
public function getEnabled(): array
|
||||
{
|
||||
return collect($this->schemas)
|
||||
->filter(fn (OAuthSchemaInterface $schema) => $schema->isEnabled())
|
||||
->all();
|
||||
}
|
||||
|
||||
public function register(OAuthSchemaInterface $schema): void
|
||||
{
|
||||
if (array_key_exists($schema->getId(), $this->schemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('services.' . $schema->getId(), array_merge($schema->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $schema->getId()]));
|
||||
|
||||
if ($schema->getSocialiteProvider()) {
|
||||
Event::listen(fn (SocialiteWasCalled $event) => $event->extendSocialite($schema->getId(), $schema->getSocialiteProvider()));
|
||||
}
|
||||
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
final class CommonProvider extends OAuthProvider
|
||||
{
|
||||
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProviderClass(): ?string
|
||||
{
|
||||
return $this->providerClass;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getHexColor(): ?string
|
||||
{
|
||||
return $this->hexColor;
|
||||
}
|
||||
|
||||
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
|
||||
{
|
||||
return new self($app, $id, $providerClass, $icon, $hexColor);
|
||||
}
|
||||
}
|
@ -1,25 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use SocialiteProviders\Authentik\Provider;
|
||||
|
||||
final class AuthentikProvider extends OAuthProvider
|
||||
final class AuthentikSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
public function getSocialiteProvider(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
@ -66,9 +60,4 @@ final class AuthentikProvider extends OAuthProvider
|
||||
{
|
||||
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
39
app/Extensions/OAuth/Schemas/CommonSchema.php
Normal file
39
app/Extensions/OAuth/Schemas/CommonSchema.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
final class CommonSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly ?string $name = null,
|
||||
private readonly ?string $configName = null,
|
||||
private readonly ?string $icon = null,
|
||||
private readonly ?string $hexColor = null,
|
||||
) {}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name ?? parent::getName();
|
||||
}
|
||||
|
||||
public function getConfigKey(): string
|
||||
{
|
||||
return $this->configName ?? parent::getConfigKey();
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getHexColor(): ?string
|
||||
{
|
||||
return $this->hexColor;
|
||||
}
|
||||
}
|
@ -1,28 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use SocialiteProviders\Discord\Provider;
|
||||
|
||||
final class DiscordProvider extends OAuthProvider
|
||||
final class DiscordSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'discord';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
public function getSocialiteProvider(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
@ -57,9 +52,4 @@ final class DiscordProvider extends OAuthProvider
|
||||
{
|
||||
return '#5865F2';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,21 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
final class GithubProvider extends OAuthProvider
|
||||
final class GithubSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'github';
|
||||
@ -57,9 +52,4 @@ final class GithubProvider extends OAuthProvider
|
||||
{
|
||||
return '#4078c0';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,21 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
final class GitlabProvider extends OAuthProvider
|
||||
final class GitlabSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gitlab';
|
||||
@ -68,9 +63,4 @@ final class GitlabProvider extends OAuthProvider
|
||||
{
|
||||
return '#fca326';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,61 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use App\Extensions\OAuth\OAuthSchemaInterface;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Illuminate\Support\Str;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
abstract class OAuthProvider
|
||||
abstract class OAuthSchema implements OAuthSchemaInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function get(?string $id = null): array|self
|
||||
{
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()]));
|
||||
|
||||
if ($this->getProviderClass()) {
|
||||
Event::listen(function (SocialiteWasCalled $event) {
|
||||
$event->extendSocialite($this->getId(), $this->getProviderClass());
|
||||
});
|
||||
}
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
public function getProviderClass(): ?string
|
||||
public function getSocialiteProvider(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
@ -112,6 +73,13 @@ abstract class OAuthProvider
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
|
||||
public function getConfigKey(): string
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return "OAUTH_{$id}_ENABLED";
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return null;
|
@ -1,28 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use SocialiteProviders\Steam\Provider;
|
||||
|
||||
final class SteamProvider extends OAuthProvider
|
||||
final class SteamSchema extends OAuthSchema
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'steam';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
public function getSocialiteProvider(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
@ -74,9 +69,4 @@ final class SteamProvider extends OAuthProvider
|
||||
{
|
||||
return '#00adee';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Extensions\Avatar\AvatarService;
|
||||
use App\Extensions\Captcha\CaptchaService;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
@ -59,7 +59,13 @@ class Settings extends Page implements HasSchemas
|
||||
|
||||
protected string $view = 'filament.pages.settings';
|
||||
|
||||
/** @var array<string, mixed>|null */
|
||||
protected OAuthService $oauthService;
|
||||
|
||||
protected AvatarService $avatarService;
|
||||
|
||||
protected CaptchaService $captchaService;
|
||||
|
||||
/** @var array<mixed>|null */
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
@ -67,6 +73,13 @@ class Settings extends Page implements HasSchemas
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
|
||||
{
|
||||
$this->oauthService = $oauthService;
|
||||
$this->avatarService = $avatarService;
|
||||
$this->captchaService = $captchaService;
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view settings');
|
||||
@ -179,7 +192,7 @@ class Settings extends Page implements HasSchemas
|
||||
Select::make('FILAMENT_AVATAR_PROVIDER')
|
||||
->label(trans('admin/setting.general.avatar_provider'))
|
||||
->native(false)
|
||||
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
|
||||
->options($this->avatarService->getMappings())
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
|
||||
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
|
||||
@ -270,15 +283,14 @@ class Settings extends Page implements HasSchemas
|
||||
{
|
||||
$formFields = [];
|
||||
|
||||
$captchaProviders = CaptchaProvider::get();
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$id = Str::upper($captchaProvider->getId());
|
||||
$name = Str::title($captchaProvider->getId());
|
||||
$captchaSchemas = $this->captchaService->getAll();
|
||||
foreach ($captchaSchemas as $schema) {
|
||||
$id = Str::upper($schema->getId());
|
||||
|
||||
$formFields[] = Section::make($name)
|
||||
$formFields[] = Section::make($schema->getName())
|
||||
->columns(5)
|
||||
->icon($captchaProvider->getIcon() ?? 'tabler-shield')
|
||||
->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false))
|
||||
->icon($schema->getIcon() ?? 'tabler-shield')
|
||||
->collapsed(fn () => !$schema->isEnabled())
|
||||
->collapsible()
|
||||
->schema([
|
||||
Hidden::make("CAPTCHA_{$id}_ENABLED")
|
||||
@ -289,21 +301,14 @@ class Settings extends Page implements HasSchemas
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.disable'))
|
||||
->color('danger')
|
||||
->action(function (Set $set) use ($id) {
|
||||
$set("CAPTCHA_{$id}_ENABLED", false);
|
||||
}),
|
||||
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
|
||||
Action::make("enable_captcha_$id")
|
||||
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.enable'))
|
||||
->color('success')
|
||||
->action(function (Set $set) use ($id, $captchaProviders) {
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$loopId = Str::upper($captchaProvider->getId());
|
||||
$set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id);
|
||||
}
|
||||
}),
|
||||
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
|
||||
])->columnSpan(1),
|
||||
Group::make($captchaProvider->getSettingsForm())
|
||||
Group::make($schema->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->columns(4)
|
||||
->columnSpan(4),
|
||||
@ -548,39 +553,37 @@ class Settings extends Page implements HasSchemas
|
||||
{
|
||||
$formFields = [];
|
||||
|
||||
$oauthProviders = OAuthProvider::get();
|
||||
foreach ($oauthProviders as $oauthProvider) {
|
||||
$id = Str::upper($oauthProvider->getId());
|
||||
$name = Str::title($oauthProvider->getId());
|
||||
$oauthSchemas = $this->oauthService->getAll();
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
$id = Str::upper($schema->getId());
|
||||
$key = $schema->getConfigKey();
|
||||
|
||||
$formFields[] = Section::make($name)
|
||||
$formFields[] = Section::make($schema->getName())
|
||||
->columns(5)
|
||||
->icon($oauthProvider->getIcon() ?? 'tabler-brand-oauth')
|
||||
->collapsed(fn () => !env("OAUTH_{$id}_ENABLED", false))
|
||||
->icon($schema->getIcon() ?? 'tabler-brand-oauth')
|
||||
->collapsed(fn () => !env($key, false))
|
||||
->collapsible()
|
||||
->schema([
|
||||
Hidden::make("OAUTH_{$id}_ENABLED")
|
||||
Hidden::make($key)
|
||||
->live()
|
||||
->default(env("OAUTH_{$id}_ENABLED")),
|
||||
->default(env($key)),
|
||||
Actions::make([
|
||||
Action::make("disable_oauth_$id")
|
||||
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
|
||||
->visible(fn (Get $get) => $get($key))
|
||||
->label(trans('admin/setting.oauth.disable'))
|
||||
->color('danger')
|
||||
->action(function (Set $set) use ($id) {
|
||||
$set("OAUTH_{$id}_ENABLED", false);
|
||||
}),
|
||||
->action(fn (Set $set) => $set($key, false)),
|
||||
Action::make("enable_oauth_$id")
|
||||
->visible(fn (Get $get) => !$get("OAUTH_{$id}_ENABLED"))
|
||||
->visible(fn (Get $get) => !$get($key))
|
||||
->label(trans('admin/setting.oauth.enable'))
|
||||
->color('success')
|
||||
->steps($oauthProvider->getSetupSteps())
|
||||
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $name)
|
||||
->steps($schema->getSetupSteps())
|
||||
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $schema->getName())
|
||||
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
|
||||
->modalCancelAction(false)
|
||||
->action(function ($data, Set $set) use ($id) {
|
||||
->action(function ($data, Set $set) use ($key) {
|
||||
$data = array_merge([
|
||||
"OAUTH_{$id}_ENABLED" => 'true',
|
||||
$key => 'true',
|
||||
], $data);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
@ -588,8 +591,8 @@ class Settings extends Page implements HasSchemas
|
||||
}
|
||||
}),
|
||||
])->columnSpan(1),
|
||||
Group::make($oauthProvider->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
|
||||
Group::make($schema->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get($key))
|
||||
->columns(4)
|
||||
->columnSpan(4),
|
||||
]);
|
||||
|
@ -258,7 +258,11 @@ class CreateEgg extends CreateRecord
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
CodeEditor::make('script_install')
|
||||
->label(trans('admin/egg.script_install'))
|
||||
|
@ -247,7 +247,11 @@ class EditEgg extends EditRecord
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
CodeEditor::make('script_install')
|
||||
->hiddenLabel()
|
||||
|
@ -93,16 +93,14 @@ class CreateNode extends CreateRecord
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(255),
|
||||
|
||||
|
@ -160,16 +160,14 @@ class EditNode extends EditRecord
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(255),
|
||||
TextInput::make('ip')
|
||||
@ -635,10 +633,10 @@ class EditNode extends EditRecord
|
||||
$data['config'] = $node->getYamlConfiguration();
|
||||
|
||||
if (!is_ip($node->fqdn)) {
|
||||
$validRecords = gethostbynamel($node->fqdn);
|
||||
if ($validRecords) {
|
||||
$ip = get_ip_from_hostname($node->fqdn);
|
||||
if ($ip) {
|
||||
$data['dns'] = true;
|
||||
$data['ip'] = collect($validRecords)->first();
|
||||
$data['ip'] = $ip;
|
||||
} else {
|
||||
$data['dns'] = false;
|
||||
}
|
||||
|
@ -66,7 +66,16 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(trans('admin/node.table.allocation_notes'))
|
||||
->placeholder(trans('admin/node.table.no_notes')),
|
||||
SelectColumn::make('ip')
|
||||
->options(fn (Allocation $allocation) => collect($this->getOwnerRecord()->ipAddresses())->merge([$allocation->ip])->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->options(function (Allocation $allocation) {
|
||||
$ips = Allocation::where('port', $allocation->port)->pluck('ip');
|
||||
|
||||
return collect($this->getOwnerRecord()->ipAddresses())
|
||||
->diff($ips)
|
||||
->unshift($allocation->ip)
|
||||
->unique()
|
||||
->mapWithKeys(fn (string $ip) => [$ip => $ip])
|
||||
->all();
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->searchable()
|
||||
->label(trans('admin/node.table.ip')),
|
||||
|
@ -447,6 +447,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->required(fn (Get $get) => in_array('required', $get('rules')))
|
||||
->rules(
|
||||
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
@ -464,6 +465,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
|
@ -663,6 +663,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
@ -680,6 +681,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
|
@ -2,27 +2,43 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\ListWebhookConfigurations;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\CreateWebhookConfiguration;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\ViewWebhookConfiguration;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\EditWebhookConfiguration;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\ListWebhookConfigurations;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\ViewWebhookConfiguration;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Exception;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use App\Traits\Filament\CanCustomizePages;
|
||||
use App\Traits\Filament\CanCustomizeRelations;
|
||||
use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ReplicateAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Support\Components\Component;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Schemas\Schema;
|
||||
use Livewire\Component as Livewire;
|
||||
use App\Enums\WebhookType;
|
||||
use Livewire\Features\SupportEvents\HandlesEvents;
|
||||
|
||||
class WebhookResource extends Resource
|
||||
{
|
||||
@ -30,6 +46,7 @@ class WebhookResource extends Resource
|
||||
use CanCustomizeRelations;
|
||||
use CanModifyForm;
|
||||
use CanModifyTable;
|
||||
use HandlesEvents;
|
||||
|
||||
protected static ?string $model = WebhookConfiguration::class;
|
||||
|
||||
@ -66,6 +83,12 @@ class WebhookResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('type'),
|
||||
TextColumn::make('endpoint')
|
||||
->label(trans('admin/webhook.table.endpoint'))
|
||||
->formatStateUsing(fn (string $state) => str($state)->after('://'))
|
||||
->limit(60)
|
||||
->wrap(),
|
||||
TextColumn::make('description')
|
||||
->label(trans('admin/webhook.table.description')),
|
||||
TextColumn::make('endpoint')
|
||||
@ -73,9 +96,15 @@ class WebhookResource extends Resource
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make()
|
||||
->hidden(fn ($record) => static::canEdit($record)),
|
||||
->hidden(fn (WebhookConfiguration $record) => static::canEdit($record)),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
ReplicateAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('filament-actions::replicate.single.label'))
|
||||
->modal(false)
|
||||
->excludeAttributes(['created_at', 'updated_at'])
|
||||
->beforeReplicaSaved(fn (WebhookConfiguration $replica) => $replica->description .= ' Copy ' . now()->format('Y-m-d H:i:s'))
|
||||
->successRedirectUrl(fn (WebhookConfiguration $replica) => EditWebhookConfiguration::getUrl(['record' => $replica])),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make(),
|
||||
@ -85,6 +114,12 @@ class WebhookResource extends Resource
|
||||
->emptyStateHeading(trans('admin/webhook.no_webhooks'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->persistFiltersInSession()
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->options(WebhookType::class)
|
||||
->attribute('type'),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -92,25 +127,217 @@ class WebhookResource extends Resource
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('endpoint')
|
||||
->label(trans('admin/webhook.endpoint'))
|
||||
->activeUrl()
|
||||
->required(),
|
||||
ToggleButtons::make('type')
|
||||
->live()
|
||||
->inline()
|
||||
->options(WebhookType::class)
|
||||
->default(WebhookType::Regular->value)
|
||||
->afterStateHydrated(function (string $state) {
|
||||
if ($state === WebhookType::Discord->value) {
|
||||
self::sendHelpBanner();
|
||||
}
|
||||
})
|
||||
->afterStateUpdated(function (string $state) {
|
||||
if ($state === WebhookType::Discord->value) {
|
||||
}
|
||||
}),
|
||||
TextInput::make('description')
|
||||
->label(trans('admin/webhook.description'))
|
||||
->required(),
|
||||
CheckboxList::make('events')
|
||||
->lazy()
|
||||
->options(fn () => WebhookConfiguration::filamentCheckboxList())
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
TextInput::make('endpoint')
|
||||
->label(trans('admin/webhook.endpoint'))
|
||||
->activeUrl()
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->gridDirection('row')
|
||||
->required(),
|
||||
->afterStateUpdated(fn (string $state, Set $set) => $set('type', str($state)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value)),
|
||||
Section::make(trans('admin/webhook.regular'))
|
||||
->hidden(fn (Get $get) => $get('type') === WebhookType::Discord->value)
|
||||
->dehydratedWhenHidden()
|
||||
->schema(fn () => self::getRegularFields())
|
||||
->formBefore(),
|
||||
Section::make(trans('admin/webhook.discord'))
|
||||
->hidden(fn (Get $get) => $get('type') === WebhookType::Regular->value)
|
||||
->dehydratedWhenHidden()
|
||||
->afterStateUpdated(fn (Livewire $livewire) => $livewire->dispatch('refresh-widget'))
|
||||
->schema(fn () => self::getDiscordFields())
|
||||
->view('filament.components.webhooksection')
|
||||
->aside()
|
||||
->formBefore(),
|
||||
Section::make(trans('admin/webhook.events'))
|
||||
->collapsible()
|
||||
->collapsed(fn (Get $get) => count($get('events') ?? []))
|
||||
->schema([
|
||||
CheckboxList::make('events')
|
||||
->live()
|
||||
->options(fn () => WebhookConfiguration::filamentCheckboxList())
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->columnSpanFull()
|
||||
->required(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Component[]
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getRegularFields(): array
|
||||
{
|
||||
return [
|
||||
KeyValue::make('headers')
|
||||
->label(trans('admin/webhook.headers')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return Component[]
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getDiscordFields(): array
|
||||
{
|
||||
return [
|
||||
Section::make(trans('admin/webhook.discord_message.profile'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_message.username')),
|
||||
TextInput::make('avatar_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_message.avatar_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_message.message'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('content')
|
||||
->label(trans('admin/webhook.discord_message.message'))
|
||||
->live(debounce: 500)
|
||||
->required(fn (Get $get) => empty($get('embeds'))),
|
||||
TextInput::make('thread_name')
|
||||
->label(trans('admin/webhook.discord_message.forum_thread')),
|
||||
CheckboxList::make('flags')
|
||||
->label('Flags')
|
||||
->options([
|
||||
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
|
||||
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),
|
||||
])
|
||||
->descriptions([
|
||||
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds_text'),
|
||||
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications_text'),
|
||||
]),
|
||||
CheckboxList::make('allowed_mentions')
|
||||
->label(trans('admin/webhook.discord_embed.allowed_mentions'))
|
||||
->options([
|
||||
'roles' => trans('admin/webhook.discord_embed.roles'),
|
||||
'users' => trans('admin/webhook.discord_embed.users'),
|
||||
'everyone' => trans('admin/webhook.discord_embed.everyone'),
|
||||
]),
|
||||
]),
|
||||
Repeater::make('embeds')
|
||||
->live(debounce: 500)
|
||||
->itemLabel(fn (array $state) => $state['title'])
|
||||
->addActionLabel(trans('admin/webhook.discord_embed.add_embed'))
|
||||
->required(fn (Get $get) => empty($get('content')))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->maxItems(10)
|
||||
->schema([
|
||||
Section::make(trans('admin/webhook.discord_embed.author'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('author.name')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author'))
|
||||
->required(fn (Get $get) => filled($get('author.url')) || filled($get('author.icon_url'))),
|
||||
TextInput::make('author.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author_url')),
|
||||
TextInput::make('author.icon_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author_icon_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.body'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.title'))
|
||||
->required(fn (Get $get) => $get('description') === null),
|
||||
Textarea::make('description')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.body'))
|
||||
->required(fn (Get $get) => $get('title') === null),
|
||||
ColorPicker::make('color')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.color'))
|
||||
->hex(),
|
||||
TextInput::make('url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.images'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('image.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.image_url')),
|
||||
TextInput::make('thumbnail.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.image_thumbnail')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.footer'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('footer.text')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.footer')),
|
||||
Checkbox::make('has_timestamp')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.has_timestamp')),
|
||||
TextInput::make('footer.icon_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.footer_icon_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.fields'))
|
||||
->collapsible()->collapsed()
|
||||
->schema([
|
||||
Repeater::make('fields')
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/webhook.discord_embed.add_field'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.field_name'))
|
||||
->required(),
|
||||
Textarea::make('value')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.field_value'))
|
||||
->rows(4)
|
||||
->required(),
|
||||
Checkbox::make('inline')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.inline_field')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public static function sendHelpBanner(): void
|
||||
{
|
||||
AlertBanner::make('discord_webhook_help')
|
||||
->title(trans('admin/webhook.help'))
|
||||
->body(trans('admin/webhook.help_text'))
|
||||
->icon('tabler-question-mark')
|
||||
->info()
|
||||
->send();
|
||||
}
|
||||
|
||||
/** @return array<string, PageRegistration> */
|
||||
public static function getDefaultPages(): array
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class CreateWebhookConfiguration extends CreateRecord
|
||||
{
|
||||
@ -22,6 +23,7 @@ class CreateWebhookConfiguration extends CreateRecord
|
||||
protected function getDefaultHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getCancelFormAction()->formId('form'),
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@ -30,4 +32,35 @@ class CreateWebhookConfiguration extends CreateRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'username'),
|
||||
'avatar_url' => data_get($data, 'avatar_url'),
|
||||
'content' => data_get($data, 'content'),
|
||||
'image' => data_get($data, 'image'),
|
||||
'thumbnail' => data_get($data, 'thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
|
||||
$data['payload'] = $tmp;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
@ -22,6 +24,12 @@ class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
Action::make('test_now')
|
||||
->label(trans('admin/webhook.test_now'))
|
||||
->color('primary')
|
||||
->disabled(fn (WebhookConfiguration $webhookConfiguration) => count($webhookConfiguration->events) === 0)
|
||||
->action(fn (WebhookConfiguration $webhookConfiguration) => $webhookConfiguration->run())
|
||||
->tooltip(trans('admin/webhook.test_now_help')),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@ -30,4 +38,89 @@ class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'username'),
|
||||
'avatar_url' => data_get($data, 'avatar_url'),
|
||||
'content' => data_get($data, 'content'),
|
||||
'image' => data_get($data, 'image'),
|
||||
'thumbnail' => data_get($data, 'thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
|
||||
|
||||
$data['payload'] = $tmp;
|
||||
}
|
||||
|
||||
if (($data['type'] ?? null) === WebhookType::Regular->value && isset($data['headers']) && is_array($data['headers'])) {
|
||||
$newHeaders = [];
|
||||
foreach ($data['headers'] as $key => $value) {
|
||||
$newKey = str_replace(' ', '-', $key);
|
||||
$newHeaders[$newKey] = $value;
|
||||
}
|
||||
$data['headers'] = $newHeaders;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'payload.embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = '#' . dechex(data_get($embed, 'color'));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = data_get($data, 'payload.flags');
|
||||
$flags = collect(range(0, PHP_INT_SIZE * 8 - 1))
|
||||
->filter(fn ($i) => ($flags & (1 << $i)) !== 0)
|
||||
->map(fn ($i) => 1 << $i)
|
||||
->values();
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'payload.username'),
|
||||
'avatar_url' => data_get($data, 'payload.avatar_url'),
|
||||
'content' => data_get($data, 'payload.content'),
|
||||
'image' => data_get($data, 'payload.image'),
|
||||
'thumbnail' => data_get($data, 'payload.thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'payload.thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'payload.allowed_mentions'),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['payload'], $data['created_at'], $data['updated_at'], $data['deleted_at']);
|
||||
$data = array_merge($data, $tmp);
|
||||
}
|
||||
|
||||
if (($data['type'] ?? null) === WebhookType::Regular->value) {
|
||||
$data['headers'] = $data['headers'] ?? [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->dispatch('refresh-widget');
|
||||
}
|
||||
}
|
||||
|
163
app/Filament/Admin/Widgets/DiscordPreview.php
Normal file
163
app/Filament/Admin/Widgets/DiscordPreview.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class DiscordPreview extends Widget
|
||||
{
|
||||
protected string $view = 'filament.admin.widgets.discord-preview';
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $listeners = [
|
||||
'refresh-widget' => '$refresh',
|
||||
];
|
||||
|
||||
protected static bool $isDiscovered = false; // Without this its shown on every Admin Pages
|
||||
|
||||
protected int|string|array $columnSpan = 1;
|
||||
|
||||
public ?WebhookConfiguration $record = null;
|
||||
|
||||
/** @var string|array<string, mixed>|null */
|
||||
public string|array|null $payload = null;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* link: callable,
|
||||
* content: mixed,
|
||||
* sender: array{name: string, avatar: string},
|
||||
* embeds: array<int, mixed>,
|
||||
* getTime: mixed
|
||||
* }
|
||||
*/
|
||||
public function getViewData(): array
|
||||
{
|
||||
if (!$this->record || !$this->record->payload) {
|
||||
return [
|
||||
'link' => fn ($href, $child) => $href ? "<a href=\"$href\" target=\"_blank\" class=\"link\">$child</a>" : $child,
|
||||
'content' => null,
|
||||
'sender' => [
|
||||
'name' => 'Pelican',
|
||||
'avatar' => 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico',
|
||||
],
|
||||
'embeds' => [],
|
||||
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
|
||||
];
|
||||
}
|
||||
|
||||
$data = $this->getWebhookSampleData();
|
||||
|
||||
if (is_string($this->record->payload)) {
|
||||
$payload = $this->replaceVarsInStringPayload($this->record->payload, $data);
|
||||
} else {
|
||||
$payload = $this->replaceVarsInArrayPayload($this->record->payload, $data);
|
||||
}
|
||||
|
||||
$embeds = data_get($payload, 'embeds', []);
|
||||
foreach ($embeds as &$embed) {
|
||||
if (data_get($embed, 'has_timestamp')) {
|
||||
unset($embed['has_timestamp']);
|
||||
$embed['timestamp'] = 'Today at ' . Carbon::now()->format('h:i A');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'link' => fn ($href, $child) => $href ? sprintf('<a href="%s" target="_blank" class="link">%s</a>', $href, $child) : $child,
|
||||
'content' => data_get($payload, 'content'),
|
||||
'sender' => [
|
||||
'name' => data_get($payload, 'username', 'Pelican'),
|
||||
'avatar' => data_get($payload, 'avatar_url', 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico'),
|
||||
],
|
||||
'embeds' => $embeds,
|
||||
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function replaceVarsInStringPayload(?string $payload, array $data): ?string
|
||||
{
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_replace_callback('/{{\s*([\w\.]+)\s*}}/', fn ($m) => data_get($data, $m[1], $m[0]),
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $payload
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function replaceVarsInArrayPayload(?array $payload, array $data): ?array
|
||||
{
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($payload as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$payload[$key] = $this->replaceVarsInStringPayload($value, $data);
|
||||
} elseif (is_array($value)) {
|
||||
$payload[$key] = $this->replaceVarsInArrayPayload($value, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getWebhookSampleData(): array
|
||||
{
|
||||
return [
|
||||
'event' => 'updated: server',
|
||||
'id' => 2,
|
||||
'external_id' => 10,
|
||||
'uuid' => '651fgbc1-dee6-4250-814e-10slda13f1e',
|
||||
'uuid_short' => '651fgbc1',
|
||||
'node_id' => 1,
|
||||
'name' => 'Example Server',
|
||||
'description' => 'This is an example server description.',
|
||||
'status' => 'running',
|
||||
'skip_scripts' => false,
|
||||
'owner_id' => 1,
|
||||
'memory' => 512,
|
||||
'swap' => 128,
|
||||
'disk' => 10240,
|
||||
'io' => 500,
|
||||
'cpu' => 500,
|
||||
'threads' => '1, 3, 5',
|
||||
'oom_killer' => false,
|
||||
'allocation_id' => 4,
|
||||
'egg_id' => 2,
|
||||
'startup' => 'This is a example startup command.',
|
||||
'image' => 'Image here',
|
||||
'allocation_limit' => 5,
|
||||
'database_limit' => 1,
|
||||
'backup_limit' => 3,
|
||||
'created_at' => '2025-03-17T15:20:32.000000Z',
|
||||
'updated_at' => '2025-05-12T17:53:12.000000Z',
|
||||
'installed_at' => '2025-04-27T21:06:01.000000Z',
|
||||
'docker_labels' => [],
|
||||
'allocation' => [
|
||||
'id' => 4,
|
||||
'node_id' => 1,
|
||||
'ip' => '192.168.0.3',
|
||||
'ip_alias' => null,
|
||||
'port' => 25567,
|
||||
'server_id' => 2,
|
||||
'notes' => null,
|
||||
'created_at' => '2025-03-17T15:20:09.000000Z',
|
||||
'updated_at' => '2025-03-17T15:20:32.000000Z',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ use Filament\Actions\ActionGroup;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Support\Enums\TextSize;
|
||||
use Filament\Tables\Columns\Column;
|
||||
use Filament\Tables\Columns\Layout\Stack;
|
||||
@ -124,6 +126,9 @@ class ListServers extends ListRecords
|
||||
->query(fn () => $baseQuery)
|
||||
->poll('15s')
|
||||
->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
|
||||
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
|
||||
->recordActions(!$usingGrid ? ActionGroup::make(static::getPowerActions(view: 'table')) : [])
|
||||
->recordActionsAlignment(Alignment::Center->value)
|
||||
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
|
||||
->recordActions($usingGrid ? [] : ActionGroup::make(static::getPowerActions())->icon('tabler-power')->tooltip('Power Actions'))
|
||||
->recordUrl(fn (Server $server) => $usingGrid ? null : Console::getUrl(panel: 'server', tenant: $server))
|
||||
@ -234,10 +239,10 @@ class ListServers extends ListRecords
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Action[] */
|
||||
public static function getPowerActions(?Server $server = null): array
|
||||
/** @return Action[]|ActionGroup[] */
|
||||
public static function getPowerActions(string $view): array
|
||||
{
|
||||
return [
|
||||
$actions = [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->icon('tabler-player-play-filled')
|
||||
@ -264,5 +269,17 @@ class ListServers extends ListRecords
|
||||
->visible(fn (?Server $record) => !($record ?? $server)->isInConflictState() & ($record ?? $server)->retrieveStatus()->isKillable())
|
||||
->dispatch('powerAction', fn (?Server $record) => ['server' => $record ?? $server, 'action' => 'kill']),
|
||||
];
|
||||
|
||||
if ($view === 'table') {
|
||||
return $actions;
|
||||
} else {
|
||||
return [
|
||||
ActionGroup::make($actions)
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor())
|
||||
->tooltip(fn (Server $server) => $server->condition->getLabel())
|
||||
->iconSize(IconSize::Large),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,14 @@
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\User;
|
||||
use App\Models\UserSSHKey;
|
||||
use App\Services\Helpers\LanguageService;
|
||||
use App\Services\Ssh\KeyCreationService;
|
||||
use App\Services\Users\ToggleTwoFactorService;
|
||||
use App\Services\Users\TwoFactorSetupService;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
@ -42,7 +44,6 @@ use Filament\Schemas\Schema;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@ -60,9 +61,12 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
|
||||
private ToggleTwoFactorService $toggleTwoFactorService;
|
||||
|
||||
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
|
||||
protected OAuthService $oauthService;
|
||||
|
||||
public function boot(ToggleTwoFactorService $toggleTwoFactorService, OAuthService $oauthService): void
|
||||
{
|
||||
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
||||
$this->oauthService = $oauthService;
|
||||
}
|
||||
|
||||
public function getMaxWidth(): Width|string
|
||||
@ -75,7 +79,7 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
*/
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
$oauthProviders = collect(OAuthProvider::get())->filter(fn (OAuthProvider $provider) => $provider->isEnabled())->all();
|
||||
$oauthSchemas = $this->oauthService->getEnabled();
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
@ -84,157 +88,161 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
Tab::make(trans('profile.tabs.account'))
|
||||
->icon('tabler-user')
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->label(trans('profile.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
TextInput::make('email')
|
||||
->prefixIcon('tabler-mail')
|
||||
->label(trans('profile.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
TextInput::make('password')
|
||||
->label(trans('profile.password'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->rule(Password::default())
|
||||
->autocomplete('new-password')
|
||||
->dehydrated(fn ($state): bool => filled($state))
|
||||
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
|
||||
->live(debounce: 500)
|
||||
->same('passwordConfirmation'),
|
||||
TextInput::make('passwordConfirmation')
|
||||
->label(trans('profile.password_confirmation'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password-fingerprint')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default('UTC')
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable()
|
||||
->native(false),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->selectablePlaceholder(false)
|
||||
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
|
||||
->native(false),
|
||||
FileUpload::make('avatar')
|
||||
->avatar()
|
||||
->acceptedFileTypes(['image/png'])
|
||||
->directory('avatars')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
/* TODO ->hintAction(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
|
||||
return Action::make('remove_avatar')
|
||||
->icon('tabler-photo-minus')
|
||||
->iconButton()
|
||||
->hidden(fn () => !$fileUpload->getDisk()->exists($path))
|
||||
->action(fn () => $fileUpload->getDisk()->delete($path))
|
||||
;
|
||||
}) */,
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(count($oauthProviders) > 0)
|
||||
->schema(function () use ($oauthProviders) {
|
||||
$actions = [];
|
||||
|
||||
foreach ($oauthProviders as $oauthProvider) {
|
||||
|
||||
$id = $oauthProvider->getId();
|
||||
$name = $oauthProvider->getName();
|
||||
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
// TODO ->color(Color::hex($oauthProvider->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = auth()->user()->oauth;
|
||||
unset($oauth[$id]);
|
||||
|
||||
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
redirect(Socialite::with($id)->redirect()->getTargetUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.2fa'))
|
||||
->icon('tabler-shield-lock')
|
||||
->schema(function (TwoFactorSetupService $setupService) {
|
||||
if ($this->getUser()->use_totp) {
|
||||
return [
|
||||
TextEntry::make('2fa-already-enabled')
|
||||
->label(trans('profile.2fa_enabled')),
|
||||
Textarea::make('backup-tokens')
|
||||
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->rows(10)
|
||||
Tab::make(trans('profile.tabs.account'))
|
||||
->icon('tabler-user')
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->label(trans('profile.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->helperText(trans('profile.backup_help'))
|
||||
->label(trans('profile.backup_codes')),
|
||||
TextInput::make('2fa-disable-code')
|
||||
->label(trans('profile.disable_2fa'))
|
||||
->helperText(trans('profile.disable_2fa_help')),
|
||||
];
|
||||
}
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
TextInput::make('email')
|
||||
->prefixIcon('tabler-mail')
|
||||
->label(trans('profile.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
TextInput::make('password')
|
||||
->label(trans('profile.password'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->rule(Password::default())
|
||||
->autocomplete('new-password')
|
||||
->dehydrated(fn ($state): bool => filled($state))
|
||||
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
|
||||
->live(debounce: 500)
|
||||
->same('passwordConfirmation'),
|
||||
TextInput::make('passwordConfirmation')
|
||||
->label(trans('profile.password_confirmation'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password-fingerprint')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default('UTC')
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable()
|
||||
->native(false),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->selectablePlaceholder(false)
|
||||
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
|
||||
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
|
||||
->native(false),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn () => config('panel.filament.uploadable-avatars'))
|
||||
->avatar()
|
||||
->acceptedFileTypes(['image/png'])
|
||||
->directory('avatars')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
->hintAction(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
|
||||
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
||||
"users.{$this->getUser()->id}.2fa.state",
|
||||
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
|
||||
);
|
||||
return Action::make('remove_avatar')
|
||||
->icon('tabler-photo-minus')
|
||||
->iconButton()
|
||||
->hidden(fn () => !$fileUpload->getDisk()->exists($path))
|
||||
->action(fn () => $fileUpload->getDisk()->delete($path));
|
||||
}),
|
||||
]),
|
||||
|
||||
$options = new QROptions([
|
||||
'svgLogo' => public_path('pelican.svg'),
|
||||
'svgLogoScale' => 0.05,
|
||||
'addLogoSpace' => true,
|
||||
'logoSpaceWidth' => 13,
|
||||
'logoSpaceHeight' => 13,
|
||||
'version' => Version::AUTO,
|
||||
// 'outputInterface' => QRSvgWithLogo::class,
|
||||
'outputBase64' => false,
|
||||
'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos
|
||||
'addQuietzone' => true,
|
||||
// 'drawLightModules' => true,
|
||||
'connectPaths' => true,
|
||||
'drawCircularModules' => true,
|
||||
// 'circleRadius' => 0.45,
|
||||
'svgDefs' => '
|
||||
Tab::make(trans('profile.tabs.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(count($oauthSchemas) > 0)
|
||||
->schema(function () use ($oauthSchemas) {
|
||||
$actions = [];
|
||||
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = auth()->user()->oauth;
|
||||
unset($oauth[$id]);
|
||||
|
||||
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
redirect(Socialite::with($id)->redirect()->getTargetUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.2fa'))
|
||||
->icon('tabler-shield-lock')
|
||||
->schema(function (TwoFactorSetupService $setupService) {
|
||||
if ($this->getUser()->use_totp) {
|
||||
return [
|
||||
TextEntry::make('2fa-already-enabled')
|
||||
->label(trans('profile.2fa_enabled')),
|
||||
Textarea::make('backup-tokens')
|
||||
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->rows(10)
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->helperText(trans('profile.backup_help'))
|
||||
->label(trans('profile.backup_codes')),
|
||||
TextInput::make('2fa-disable-code')
|
||||
->label(trans('profile.disable_2fa'))
|
||||
->helperText(trans('profile.disable_2fa_help')),
|
||||
];
|
||||
}
|
||||
|
||||
['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'),
|
||||
'svgLogoScale' => 0.05,
|
||||
'addLogoSpace' => true,
|
||||
'logoSpaceWidth' => 13,
|
||||
'logoSpaceHeight' => 13,
|
||||
'version' => Version::AUTO,
|
||||
// 'outputInterface' => QRSvgWithLogo::class,
|
||||
'outputBase64' => false,
|
||||
'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos
|
||||
'addQuietzone' => true,
|
||||
// 'drawLightModules' => true,
|
||||
'connectPaths' => true,
|
||||
'drawCircularModules' => true,
|
||||
// 'circleRadius' => 0.45,
|
||||
'svgDefs' => '
|
||||
<linearGradient id="gradient" x1="100%" y2="100%">
|
||||
<stop stop-color="#7dd4fc" offset="0"/>
|
||||
<stop stop-color="#38bdf8" offset="0.5"/>
|
||||
@ -245,85 +253,181 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
.light{fill: #000;}
|
||||
]]></style>
|
||||
',
|
||||
]);
|
||||
]);
|
||||
|
||||
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
||||
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
||||
|
||||
$image = (new QRCode($options))->render($url);
|
||||
$image = (new QRCode($options))->render($url);
|
||||
|
||||
return [
|
||||
TextEntry::make('qr')
|
||||
->label(trans('profile.scan_qr'))
|
||||
->state(fn () => new HtmlString("
|
||||
return [
|
||||
TextEntry::make('qr')
|
||||
->label(trans('profile.scan_qr'))
|
||||
->state(fn () => new HtmlString("
|
||||
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||
"))
|
||||
->helperText(trans('profile.setup_key') .': '. $secret),
|
||||
TextInput::make('2facode')
|
||||
->label(trans('profile.code'))
|
||||
->requiredWith('2fapassword')
|
||||
->helperText(trans('profile.code_help')),
|
||||
TextInput::make('2fapassword')
|
||||
->label(trans('profile.current_password'))
|
||||
->requiredWith('2facode')
|
||||
->currentPassword()
|
||||
->password(),
|
||||
];
|
||||
}),
|
||||
->helperText(trans('profile.setup_key') .': '. $secret),
|
||||
TextInput::make('2facode')
|
||||
->label(trans('profile.code'))
|
||||
->requiredWith('2fapassword')
|
||||
->helperText(trans('profile.code_help')),
|
||||
TextInput::make('2fapassword')
|
||||
->label(trans('profile.current_password'))
|
||||
->requiredWith('2facode')
|
||||
->currentPassword()
|
||||
->password(),
|
||||
];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
Tab::make(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Grid::make('name')->columns(5)->schema([
|
||||
Section::make(trans('profile.create_api_key'))->columnSpan(3)->schema([
|
||||
TextInput::make('description')
|
||||
->label(trans('profile.description'))
|
||||
->live(),
|
||||
TagsInput::make('allowed_ips')
|
||||
->label(trans('profile.allowed_ips'))
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('127.0.0.1 or 192.168.1.1')
|
||||
->helperText(trans('profile.allowed_ips_help'))
|
||||
->columnSpanFull(),
|
||||
])->headerActions([
|
||||
Action::make('create')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('description')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.api_key_created'))
|
||||
->body($token->accessToken->identifier . $token->plainTextToken)
|
||||
->persistent()
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$apiKey = ApiKey::find($key['id'] ?? null);
|
||||
if ($apiKey->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
Placeholder::make('memo')
|
||||
->label(fn (ApiKey $key) => $key->memo),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->schema([
|
||||
Grid::make(5)->schema([
|
||||
Section::make(trans('profile.create_key'))->columnSpan(3)->schema([
|
||||
TextInput::make('description')
|
||||
->label(trans('profile.description'))
|
||||
Grid::make('name')->columns(5)->schema([
|
||||
Section::make(trans('profile.create_ssh_key'))->columnSpan(3)->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('profile.name'))
|
||||
->live(),
|
||||
Textarea::make('public_key')
|
||||
->label(trans('profile.public_key'))
|
||||
->autosize()
|
||||
->live(),
|
||||
TagsInput::make('allowed_ips')
|
||||
->label(trans('profile.allowed_ips'))
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('127.0.0.1 or 192.168.1.1')
|
||||
->helperText(trans('profile.allowed_ips_help'))
|
||||
->columnSpanFull(),
|
||||
])->headerActions([
|
||||
Action::make('Create')
|
||||
Action::make('create')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => '-ssh-keys-tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user, KeyCreationService $service) {
|
||||
try {
|
||||
$sshKey = $service->handle($user, $get('name'), $get('public_key'));
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
Activity::event('user:ssh-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.key_created'))
|
||||
->body($token->accessToken->identifier . $token->plainTextToken)
|
||||
->persistent()
|
||||
->success()
|
||||
->send();
|
||||
Notification::make()
|
||||
->title(trans('profile.ssh_key_created'))
|
||||
->body("SHA256:{$sshKey->fingerprint}")
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
$action->success();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('profile.could_not_create_ssh_key'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$action->failure();
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.keys'))->label(trans('profile.keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('keys')
|
||||
->label('')
|
||||
->relationship('apiKeys')
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
ApiKey::find($key['id'] ?? null)?->delete();
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
@ -332,152 +436,41 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('adf')->label(fn (ApiKey $key) => $key->memo),
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->label(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->hidden(),
|
||||
|
||||
Tab::make(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->label('')
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('activity!')->label('')->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
TextEntry::make('activity!')->label('')->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
Tab::make(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
->collapsible()
|
||||
->icon('tabler-dashboard')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
->collapsible()
|
||||
->icon('tabler-dashboard')
|
||||
->schema([
|
||||
ToggleButtons::make('dashboard_layout')
|
||||
->label(trans('profile.dashboard_layout'))
|
||||
->inline()
|
||||
->required()
|
||||
->options([
|
||||
'grid' => trans('profile.grid'),
|
||||
'table' => trans('profile.table'),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
->required()
|
||||
->options(function () {
|
||||
$fonts = [
|
||||
'monospace' => 'monospace', //default
|
||||
];
|
||||
|
||||
if (!Storage::disk('public')->exists('fonts')) {
|
||||
Storage::disk('public')->makeDirectory('fonts');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
|
||||
$fileInfo = pathinfo($file);
|
||||
|
||||
if ($fileInfo['extension'] === 'ttf') {
|
||||
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
return $fonts;
|
||||
})
|
||||
->reactive()
|
||||
->default('monospace')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
|
||||
TextEntry::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->state(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
.preview-text {
|
||||
font-family: $fontName;
|
||||
font-size: $fontSize;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
CSS;
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
@font-face {
|
||||
font-family: $fontName;
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
$style
|
||||
CSS;
|
||||
}
|
||||
|
||||
return new HtmlString(<<<HTML
|
||||
<style>
|
||||
{$style}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
->suffix(trans('profile.seconds'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('profile.graph_period_helper'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(30)
|
||||
->minValue(10)
|
||||
->maxValue(120)
|
||||
->required(),
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->default(30),
|
||||
]),
|
||||
ToggleButtons::make('dashboard_layout')
|
||||
->label(trans('profile.dashboard_layout'))
|
||||
->inline()
|
||||
->required()
|
||||
->options([
|
||||
'grid' => trans('profile.grid'),
|
||||
'table' => trans('profile.table'),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(1)
|
||||
->default(30),
|
||||
// Select::make('console_font')
|
||||
// ->label(trans('profile.font'))
|
||||
// ->hidden() //TODO
|
||||
// ->columnSpan(1),
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
@ -485,6 +478,82 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
|
||||
->numeric()
|
||||
->required()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
->required()
|
||||
->options(function () {
|
||||
$fonts = [
|
||||
'monospace' => 'monospace', //default
|
||||
];
|
||||
|
||||
if (!Storage::disk('public')->exists('fonts')) {
|
||||
Storage::disk('public')->makeDirectory('fonts');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
|
||||
$fileInfo = pathinfo($file);
|
||||
|
||||
if ($fileInfo['extension'] === 'ttf') {
|
||||
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
return $fonts;
|
||||
})
|
||||
->reactive()
|
||||
->default('monospace')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
|
||||
TextEntry::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->state(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
.preview-text {
|
||||
font-family: $fontName;
|
||||
font-size: $fontSize;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
CSS;
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
@font-face {
|
||||
font-family: $fontName;
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
$style
|
||||
CSS;
|
||||
}
|
||||
|
||||
return new HtmlString(<<<HTML
|
||||
<style>
|
||||
{$style}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
->suffix(trans('profile.seconds'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('profile.graph_period_helper'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(30)
|
||||
->minValue(10)
|
||||
->maxValue(120)
|
||||
->required(),
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->default(30),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
@ -3,8 +3,8 @@
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Events\Auth\ProvidedAuthenticationToken;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Extensions\Captcha\CaptchaService;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\User;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||
@ -27,9 +27,15 @@ class Login extends \Filament\Auth\Pages\Login
|
||||
|
||||
public bool $verifyTwoFactor = false;
|
||||
|
||||
public function boot(Google2FA $google2FA): void
|
||||
protected OAuthService $oauthService;
|
||||
|
||||
protected CaptchaService $captchaService;
|
||||
|
||||
public function boot(Google2FA $google2FA, OAuthService $oauthService, CaptchaService $captchaService): void
|
||||
{
|
||||
$this->google2FA = $google2FA;
|
||||
$this->oauthService = $oauthService;
|
||||
$this->captchaService = $captchaService;
|
||||
}
|
||||
|
||||
public function authenticate(): ?LoginResponse
|
||||
@ -116,8 +122,8 @@ class Login extends \Filament\Auth\Pages\Login
|
||||
$this->getTwoFactorAuthenticationComponent(),
|
||||
];
|
||||
|
||||
if ($captchaProvider = $this->getCaptchaComponent()) {
|
||||
$components = array_merge($components, [$captchaProvider]);
|
||||
if ($captchaComponent = $this->getCaptchaComponent()) {
|
||||
$schema = array_merge($schema, [$captchaComponent]);
|
||||
}
|
||||
|
||||
return $schema
|
||||
@ -137,13 +143,7 @@ class Login extends \Filament\Auth\Pages\Login
|
||||
|
||||
private function getCaptchaComponent(): ?Component
|
||||
{
|
||||
$captchaProvider = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->first();
|
||||
|
||||
if (!$captchaProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $captchaProvider->getComponent();
|
||||
return $this->captchaService->getActiveSchema()?->getFormComponent();
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
@ -169,15 +169,15 @@ class Login extends \Filament\Auth\Pages\Login
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
$oauthProviders = collect(OAuthProvider::get())->filter(fn (OAuthProvider $provider) => $provider->isEnabled())->all();
|
||||
$oauthSchemas = $this->oauthService->getEnabled();
|
||||
|
||||
foreach ($oauthProviders as $oauthProvider) {
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
|
||||
$id = $oauthProvider->getId();
|
||||
$id = $schema->getId();
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label($oauthProvider->getName())
|
||||
->icon($oauthProvider->getIcon())
|
||||
->label($schema->getName())
|
||||
->icon($schema->getIcon())
|
||||
//TODO ->color(Color::hex($oauthProvider->getHexColor()))
|
||||
->url(route('auth.oauth.redirect', ['driver' => $id], false));
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ namespace App\Filament\Server\Pages;
|
||||
use App\Enums\ConsoleWidgetPosition;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
use App\Extensions\Features\FeatureProvider;
|
||||
use App\Extensions\Features\FeatureService;
|
||||
use App\Filament\Server\Widgets\ServerConsole;
|
||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||
@ -41,6 +41,8 @@ class Console extends Page
|
||||
|
||||
public ContainerStatus $status = ContainerStatus::Offline;
|
||||
|
||||
protected FeatureService $featureService;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
@ -57,12 +59,12 @@ class Console extends Page
|
||||
}
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
public function boot(FeatureService $featureService): void
|
||||
{
|
||||
$this->featureService = $featureService;
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
/** @var FeatureProvider $feature */
|
||||
foreach ($server->egg->features() as $feature) {
|
||||
foreach ($featureService->getActiveSchemas($server->egg->features) as $feature) {
|
||||
$this->cacheAction($feature->getAction());
|
||||
}
|
||||
}
|
||||
@ -73,8 +75,8 @@ class Console extends Page
|
||||
$data = json_decode($data);
|
||||
$feature = data_get($data, 'key');
|
||||
|
||||
$feature = FeatureProvider::getProviders($feature);
|
||||
if ($this->getMountedAction()) {
|
||||
$feature = $this->featureService->get($feature);
|
||||
if (!$feature || $this->getMountedAction()) {
|
||||
return;
|
||||
}
|
||||
$this->mountAction($feature->getId());
|
||||
|
@ -110,6 +110,7 @@ class Startup extends ServerFormPage
|
||||
->schema(function () {
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
@ -128,6 +129,7 @@ class Startup extends ServerFormPage
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
@ -2,23 +2,24 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Filament\Pages\Auth\EditProfile;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthManager $auth,
|
||||
private readonly UserUpdateService $updateService
|
||||
private readonly UserUpdateService $updateService,
|
||||
private readonly OAuthService $oauthService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -27,7 +28,7 @@ class OAuthController extends Controller
|
||||
public function redirect(string $driver): RedirectResponse
|
||||
{
|
||||
// Driver is disabled - redirect to normal login
|
||||
if (!OAuthProvider::get($driver)->isEnabled()) {
|
||||
if (!$this->oauthService->get($driver)->isEnabled()) {
|
||||
return redirect()->route('auth.login');
|
||||
}
|
||||
|
||||
@ -40,7 +41,7 @@ class OAuthController extends Controller
|
||||
public function callback(Request $request, string $driver): RedirectResponse
|
||||
{
|
||||
// Driver is disabled - redirect to normal login
|
||||
if (!OAuthProvider::get($driver)->isEnabled()) {
|
||||
if (!$this->oauthService->get($driver)?->isEnabled()) {
|
||||
return redirect()->route('auth.login');
|
||||
}
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Events\Auth\FailedCaptcha;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
readonly class VerifyCaptcha
|
||||
{
|
||||
public function __construct(private Application $app) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if ($this->app->isLocal()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$captchaProviders = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->all();
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$response = $captchaProvider->validateResponse();
|
||||
|
||||
if ($response['success'] && $captchaProvider->verifyDomain($response['hostname'] ?? '', $request->url())) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));
|
||||
|
||||
throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$captchaProvider->getId()} captcha data.");
|
||||
}
|
||||
|
||||
// No captcha enabled
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -4,12 +4,15 @@ namespace App\Jobs;
|
||||
|
||||
use Exception;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class ProcessWebhook implements ShouldQueue
|
||||
{
|
||||
@ -26,17 +29,45 @@ class ProcessWebhook implements ShouldQueue
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$data = $this->data[0];
|
||||
|
||||
if ($this->webhookConfiguration->type === WebhookType::Discord) {
|
||||
$data = array_merge(
|
||||
is_array($data) ? $data : json_decode($data, true),
|
||||
['event' => $this->webhookConfiguration->transformClassName($this->eventName)]
|
||||
);
|
||||
|
||||
$payload = json_encode($this->webhookConfiguration->payload);
|
||||
$tmp = $this->webhookConfiguration->replaceVars($data, $payload);
|
||||
$data = json_decode($tmp, true);
|
||||
|
||||
$embeds = data_get($data, 'embeds');
|
||||
if ($embeds) {
|
||||
foreach ($embeds as &$embed) {
|
||||
if (data_get($embed, 'has_timestamp')) {
|
||||
$embed['timestamp'] = Carbon::now();
|
||||
unset($embed['has_timestamp']);
|
||||
}
|
||||
}
|
||||
$data['embeds'] = $embeds;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Http::withHeader('X-Webhook-Event', $this->eventName)
|
||||
->post($this->webhookConfiguration->endpoint, $this->data)
|
||||
->throw();
|
||||
$headers = [];
|
||||
if ($this->webhookConfiguration->type === WebhookType::Regular && $customHeaders = $this->webhookConfiguration->headers) {
|
||||
$headers = array_merge(['X-Webhook-Event', $this->eventName], $customHeaders);
|
||||
}
|
||||
|
||||
Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw();
|
||||
$successful = now();
|
||||
} catch (Exception) {
|
||||
} catch (Exception $exception) {
|
||||
report($exception->getMessage());
|
||||
$successful = null;
|
||||
}
|
||||
|
||||
$this->webhookConfiguration->webhooks()->create([
|
||||
'payload' => $this->data,
|
||||
'payload' => $data,
|
||||
'successful_at' => $successful,
|
||||
'event' => $this->eventName,
|
||||
'endpoint' => $this->webhookConfiguration->endpoint,
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Jobs\ProcessWebhook;
|
||||
use App\Models\WebhookConfiguration;
|
||||
|
||||
class DispatchWebhooks
|
||||
@ -23,7 +22,7 @@ class DispatchWebhooks
|
||||
/** @var WebhookConfiguration $webhookConfig */
|
||||
foreach ($matchingHooks as $webhookConfig) {
|
||||
if (in_array($eventName, $webhookConfig->events)) {
|
||||
ProcessWebhook::dispatch($webhookConfig, $eventName, $data);
|
||||
$webhookConfig->run($eventName, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,12 @@ use Illuminate\Database\Eloquent\Collection;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Exceptions\Service\Egg\HasChildrenException;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Extensions\Features\FeatureProvider;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@ -163,12 +162,6 @@ class Egg extends Model implements Validatable
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<FeatureProvider> */
|
||||
public function features(): array
|
||||
{
|
||||
return FeatureProvider::getProviders($this->features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the install script for the egg; if egg is copying from another
|
||||
* it will return the copied script.
|
||||
|
@ -9,9 +9,8 @@ use Database\Factories\UserFactory;
|
||||
use BackedEnum;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Avatar\AvatarService;
|
||||
use App\Rules\Username;
|
||||
use App\Facades\Activity;
|
||||
use App\Traits\HasValidation;
|
||||
use DateTimeZone;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
@ -23,6 +22,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
@ -37,7 +37,6 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use ResourceBundle;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
@ -402,17 +401,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
public function getFilamentAvatarUrl(): ?string
|
||||
{
|
||||
if (config('panel.filament.uploadable-avatars')) {
|
||||
$path = "avatars/$this->id.png";
|
||||
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::url($path);
|
||||
}
|
||||
}
|
||||
|
||||
$provider = AvatarProvider::getProvider(config('panel.filament.avatar-provider'));
|
||||
|
||||
return $provider?->get($this);
|
||||
return App::call(fn (AvatarService $service) => $service->getAvatarUrl($this));
|
||||
}
|
||||
|
||||
public function canTarget(Model $model): bool
|
||||
|
@ -2,25 +2,31 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Jobs\ProcessWebhook;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Features\SupportEvents\HandlesEvents;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
/**
|
||||
* @property string|array<string, mixed>|null $payload
|
||||
* @property string $endpoint
|
||||
* @property string $description
|
||||
* @property string[] $events
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $deleted_at
|
||||
* @property WebhookType|string|null $type
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property array<string, string>|null $headers
|
||||
*/
|
||||
class WebhookConfiguration extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HandlesEvents, HasFactory, SoftDeletes;
|
||||
|
||||
/** @var string[] */
|
||||
protected static array $eventBlacklist = [
|
||||
@ -28,15 +34,29 @@ class WebhookConfiguration extends Model
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'payload',
|
||||
'endpoint',
|
||||
'description',
|
||||
'events',
|
||||
'headers',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default values for specific fields in the database.
|
||||
*/
|
||||
protected $attributes = [
|
||||
'type' => WebhookType::Regular,
|
||||
'payload' => null,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'events' => 'json',
|
||||
'events' => 'array',
|
||||
'payload' => 'array',
|
||||
'type' => WebhookType::class,
|
||||
'headers' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
@ -44,7 +64,7 @@ class WebhookConfiguration extends Model
|
||||
{
|
||||
self::saved(static function (self $webhookConfiguration): void {
|
||||
$changedEvents = collect([
|
||||
...((array) $webhookConfiguration->events),
|
||||
...($webhookConfiguration->events),
|
||||
...$webhookConfiguration->getOriginal('events', '[]'),
|
||||
])->unique();
|
||||
|
||||
@ -52,7 +72,7 @@ class WebhookConfiguration extends Model
|
||||
});
|
||||
|
||||
self::deleted(static function (self $webhookConfiguration): void {
|
||||
self::updateCache(collect((array) $webhookConfiguration->events));
|
||||
self::updateCache(collect($webhookConfiguration->events));
|
||||
});
|
||||
}
|
||||
|
||||
@ -141,9 +161,7 @@ class WebhookConfiguration extends Model
|
||||
foreach (File::allFiles($directory) as $file) {
|
||||
$namespace = str($file->getPath())
|
||||
->after(base_path())
|
||||
->replace(DIRECTORY_SEPARATOR, '\\')
|
||||
->replace('\\app\\', 'App\\')
|
||||
->toString();
|
||||
->replace([DIRECTORY_SEPARATOR, '\\app\\'], ['\\', 'App\\']);
|
||||
|
||||
$events[] = $namespace . '\\' . str($file->getFilename())
|
||||
->replace([DIRECTORY_SEPARATOR, '.php'], ['\\', '']);
|
||||
@ -151,4 +169,66 @@ class WebhookConfiguration extends Model
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed, mixed> $replacement
|
||||
* */
|
||||
public function replaceVars(array $replacement, string $subject): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/{{(.*?)}}/',
|
||||
function ($matches) use ($replacement) {
|
||||
$trimmed = trim($matches[1]);
|
||||
|
||||
return Arr::get($replacement, $trimmed, $trimmed);
|
||||
},
|
||||
$subject
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<mixed, mixed> $eventData */
|
||||
public function run(?string $eventName = null, ?array $eventData = null): void
|
||||
{
|
||||
$eventName ??= 'eloquent.created: '.Server::class;
|
||||
$eventData ??= $this->getWebhookSampleData();
|
||||
|
||||
ProcessWebhook::dispatch($this, $eventName, [$eventData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getWebhookSampleData(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'installing',
|
||||
'oom_killer' => false,
|
||||
'installed_at' => null,
|
||||
'external_id' => 10,
|
||||
'uuid' => '651fgbc1-dee6-4250-814e-10slda13f1e',
|
||||
'uuid_short' => '651fgbc1',
|
||||
'node_id' => 1,
|
||||
'name' => 'Eagle',
|
||||
'description' => 'This is an example server description.',
|
||||
'skip_scripts' => false,
|
||||
'owner_id' => 1,
|
||||
'memory' => 2048,
|
||||
'swap' => 128,
|
||||
'disk' => 10240,
|
||||
'io' => 500,
|
||||
'cpu' => 100,
|
||||
'threads' => '1,3,5',
|
||||
'allocation_id' => 4,
|
||||
'egg_id' => 2,
|
||||
'startup' => 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
'image' => 'ghcr.io/parkervcp/yolks:java_21',
|
||||
'database_limit' => 1,
|
||||
'allocation_limit' => 5,
|
||||
'backup_limit' => 3,
|
||||
'docker_labels' => [],
|
||||
'created_at' => '2025-03-17T15:20:32.000000Z',
|
||||
'updated_at' => '2025-05-12T17:53:12.000000Z',
|
||||
'id' => 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -22,21 +22,6 @@ use App\Checks\NodeVersionsCheck;
|
||||
use App\Checks\PanelVersionCheck;
|
||||
use App\Checks\ScheduleCheck;
|
||||
use App\Checks\UsedDiskSpaceCheck;
|
||||
use App\Extensions\Avatar\Providers\GravatarProvider;
|
||||
use App\Extensions\Avatar\Providers\UiAvatarsProvider;
|
||||
use App\Extensions\OAuth\Providers\GitlabProvider;
|
||||
use App\Extensions\Captcha\Providers\TurnstileProvider;
|
||||
use App\Extensions\Features\GSLToken;
|
||||
use App\Extensions\Features\JavaVersion;
|
||||
use App\Extensions\Features\MinecraftEula;
|
||||
use App\Extensions\Features\PIDLimit;
|
||||
use App\Extensions\Features\SteamDiskSpace;
|
||||
use App\Extensions\OAuth\Providers\AuthentikProvider;
|
||||
use App\Extensions\OAuth\Providers\CommonProvider;
|
||||
use App\Extensions\OAuth\Providers\DiscordProvider;
|
||||
use App\Extensions\OAuth\Providers\GithubProvider;
|
||||
use App\Extensions\OAuth\Providers\SteamProvider;
|
||||
use App\Filament\Components\Actions\CopyAction;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
@ -119,35 +104,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
|
||||
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
|
||||
|
||||
// Default OAuth providers included with Socialite
|
||||
CommonProvider::register($app, 'facebook', null, 'tabler-brand-facebook-f', '#1877f2');
|
||||
CommonProvider::register($app, 'x', null, 'tabler-brand-x-f', '#1da1f2');
|
||||
CommonProvider::register($app, 'linkedin', null, 'tabler-brand-linkedin-f', '#0a66c2');
|
||||
CommonProvider::register($app, 'google', null, 'tabler-brand-google-f', '#4285f4');
|
||||
GithubProvider::register($app);
|
||||
GitlabProvider::register($app);
|
||||
CommonProvider::register($app, 'bitbucket', null, 'tabler-brand-bitbucket-f', '#205081');
|
||||
CommonProvider::register($app, 'slack', null, 'tabler-brand-slack', '#6ecadc');
|
||||
|
||||
// Additional OAuth providers from socialiteproviders.com
|
||||
AuthentikProvider::register($app);
|
||||
DiscordProvider::register($app);
|
||||
SteamProvider::register($app);
|
||||
|
||||
// Default Captcha provider
|
||||
TurnstileProvider::register($app);
|
||||
|
||||
// Default Avatar providers
|
||||
GravatarProvider::register();
|
||||
UiAvatarsProvider::register();
|
||||
|
||||
// Default Feature providers
|
||||
GSLToken::register($app);
|
||||
JavaVersion::register($app);
|
||||
MinecraftEula::register($app);
|
||||
PIDLimit::register($app);
|
||||
SteamDiskSpace::register($app);
|
||||
|
||||
FilamentColor::register([
|
||||
'danger' => Color::Red,
|
||||
'gray' => Color::Zinc,
|
||||
@ -155,6 +111,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
'primary' => Color::Blue,
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
'blurple' => Color::hex('#5865F2'),
|
||||
]);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
|
24
app/Providers/Extensions/AvatarServiceProvider.php
Normal file
24
app/Providers/Extensions/AvatarServiceProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Extensions;
|
||||
|
||||
use App\Extensions\Avatar\AvatarService;
|
||||
use App\Extensions\Avatar\Schemas\GravatarSchema;
|
||||
use App\Extensions\Avatar\Schemas\UiAvatarsSchema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AvatarServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(AvatarService::class, function ($app) {
|
||||
$service = new AvatarService(config('panel.filament.uploadable-avatars', false), config('panel.filament.avatar-provider', 'gravatar'));
|
||||
|
||||
// Default Avatar providers
|
||||
$service->register(new GravatarSchema());
|
||||
$service->register(new UiAvatarsSchema());
|
||||
|
||||
return $service;
|
||||
});
|
||||
}
|
||||
}
|
22
app/Providers/Extensions/CaptchaServiceProvider.php
Normal file
22
app/Providers/Extensions/CaptchaServiceProvider.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Extensions;
|
||||
|
||||
use App\Extensions\Captcha\CaptchaService;
|
||||
use App\Extensions\Captcha\Schemas\Turnstile\TurnstileSchema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CaptchaServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(CaptchaService::class, function ($app) {
|
||||
$service = new CaptchaService();
|
||||
|
||||
// Default Captcha providers
|
||||
$service->register(new TurnstileSchema());
|
||||
|
||||
return $service;
|
||||
});
|
||||
}
|
||||
}
|
30
app/Providers/Extensions/FeatureServiceProvider.php
Normal file
30
app/Providers/Extensions/FeatureServiceProvider.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Extensions;
|
||||
|
||||
use App\Extensions\Features\FeatureService;
|
||||
use App\Extensions\Features\Schemas\GSLTokenSchema;
|
||||
use App\Extensions\Features\Schemas\JavaVersionSchema;
|
||||
use App\Extensions\Features\Schemas\MinecraftEulaSchema;
|
||||
use App\Extensions\Features\Schemas\PIDLimitSchema;
|
||||
use App\Extensions\Features\Schemas\SteamDiskSpaceSchema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class FeatureServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(FeatureService::class, function ($app) {
|
||||
$provider = new FeatureService();
|
||||
|
||||
// Default Feature providers
|
||||
$provider->register(new GSLTokenSchema());
|
||||
$provider->register(new JavaVersionSchema());
|
||||
$provider->register(new MinecraftEulaSchema());
|
||||
$provider->register(new PIDLimitSchema());
|
||||
$provider->register(new SteamDiskSpaceSchema());
|
||||
|
||||
return $provider;
|
||||
});
|
||||
}
|
||||
}
|
39
app/Providers/Extensions/OAuthServiceProvider.php
Normal file
39
app/Providers/Extensions/OAuthServiceProvider.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Extensions;
|
||||
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Extensions\OAuth\Schemas\AuthentikSchema;
|
||||
use App\Extensions\OAuth\Schemas\CommonSchema;
|
||||
use App\Extensions\OAuth\Schemas\DiscordSchema;
|
||||
use App\Extensions\OAuth\Schemas\GithubSchema;
|
||||
use App\Extensions\OAuth\Schemas\GitlabSchema;
|
||||
use App\Extensions\OAuth\Schemas\SteamSchema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class OAuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(OAuthService::class, function ($app) {
|
||||
$service = new OAuthService();
|
||||
|
||||
// Default OAuth providers included with Socialite
|
||||
$service->register(new CommonSchema('facebook', icon: 'tabler-brand-facebook-f', hexColor: '#1877f2'));
|
||||
$service->register(new CommonSchema('x', icon: 'tabler-brand-x-f', hexColor: '#1da1f2'));
|
||||
$service->register(new CommonSchema('linkedin', icon: 'tabler-brand-linkedin-f', hexColor: '#0a66c2'));
|
||||
$service->register(new CommonSchema('google', icon: 'tabler-brand-google-f', hexColor: '#4285f4'));
|
||||
$service->register(new GithubSchema());
|
||||
$service->register(new GitlabSchema());
|
||||
$service->register(new CommonSchema('bitbucket', icon: 'tabler-brand-bitbucket-f', hexColor: '#205081'));
|
||||
$service->register(new CommonSchema('slack', icon: 'tabler-brand-slack', hexColor: '#6ecadc'));
|
||||
|
||||
// Additional OAuth providers from socialiteproviders.com
|
||||
$service->register(new AuthentikSchema());
|
||||
$service->register(new DiscordSchema());
|
||||
$service->register(new SteamSchema());
|
||||
|
||||
return $service;
|
||||
});
|
||||
}
|
||||
}
|
@ -51,11 +51,11 @@ class AdminPanelProvider extends PanelProvider
|
||||
->sort(24),
|
||||
])
|
||||
->navigationGroups([
|
||||
NavigationGroup::make(trans('admin/dashboard.server'))
|
||||
NavigationGroup::make(fn () => trans('admin/dashboard.server'))
|
||||
->collapsible(false),
|
||||
NavigationGroup::make(trans('admin/dashboard.user'))
|
||||
NavigationGroup::make(fn () => trans('admin/dashboard.user'))
|
||||
->collapsible(false),
|
||||
NavigationGroup::make(trans('admin/dashboard.advanced')),
|
||||
NavigationGroup::make(fn () => trans('admin/dashboard.advanced')),
|
||||
])
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
|
||||
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidTurnstileCaptcha implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
$response = CaptchaProvider::get('turnstile')->validateResponse($value);
|
||||
|
||||
if (!$response['success']) {
|
||||
$fail($response['message'] ?? 'Unknown error occurred, please try again');
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Extensions\Features\FeatureService;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
|
||||
class ServerConfigurationStructureService
|
||||
{
|
||||
public function __construct(private EnvironmentService $environment) {}
|
||||
public function __construct(private EnvironmentService $environment, private FeatureService $featureService) {}
|
||||
|
||||
/**
|
||||
* Return a configuration array for a specific server when passed a server model.
|
||||
@ -58,7 +60,7 @@ class ServerConfigurationStructureService
|
||||
* default: array{ip: string, port: int},
|
||||
* mappings: array<string, array<int>>,
|
||||
* },
|
||||
* egg: array{id: string, file_denylist: string[]},
|
||||
* egg: array{id: string, file_denylist: string[], features: string[][]},
|
||||
* labels?: string[],
|
||||
* mounts: array{source: string, target: string, read_only: bool},
|
||||
* }
|
||||
@ -101,9 +103,7 @@ class ServerConfigurationStructureService
|
||||
'egg' => [
|
||||
'id' => $server->egg->uuid,
|
||||
'file_denylist' => $server->egg->inherit_file_denylist,
|
||||
'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [
|
||||
$feature->getId() => $feature->getListeners(),
|
||||
])->all(),
|
||||
'features' => $this->featureService->getMappings($server->egg->features),
|
||||
],
|
||||
];
|
||||
|
||||
|
48
app/Services/Ssh/KeyCreationService.php
Normal file
48
app/Services/Ssh/KeyCreationService.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ssh;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserSSHKey;
|
||||
use Exception;
|
||||
use phpseclib3\Crypt\DSA;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
use phpseclib3\Exception\NoKeyLoadedException;
|
||||
|
||||
class KeyCreationService
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(User $user, string $name, string $publicKey): UserSSHKey
|
||||
{
|
||||
try {
|
||||
$key = PublicKeyLoader::loadPublicKey($publicKey);
|
||||
} catch (NoKeyLoadedException) {
|
||||
throw new Exception('The public key provided is not valid');
|
||||
}
|
||||
|
||||
if ($key instanceof DSA) {
|
||||
throw new Exception('DSA keys are not supported');
|
||||
}
|
||||
|
||||
if ($key instanceof RSA && $key->getLength() < 2048) {
|
||||
throw new Exception('RSA keys must be at least 2048 bytes in length');
|
||||
}
|
||||
|
||||
$fingerprint = $key->getFingerprint('sha256');
|
||||
if ($user->sshKeys()->where('fingerprint', $fingerprint)->exists()) {
|
||||
throw new Exception('The public key provided already exists on your account');
|
||||
}
|
||||
|
||||
/** @var UserSSHKey $sshKey */
|
||||
$sshKey = $user->sshKeys()->create([
|
||||
'name' => $name,
|
||||
'public_key' => $key->toString('PKCS8'),
|
||||
'fingerprint' => $fingerprint,
|
||||
]);
|
||||
|
||||
return $sshKey;
|
||||
}
|
||||
}
|
@ -81,3 +81,20 @@ if (!function_exists('resolve_path')) {
|
||||
return implode('/', $absolutes);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_ip_from_hostname')) {
|
||||
function get_ip_from_hostname(string $hostname): string|bool
|
||||
{
|
||||
$validARecords = @dns_get_record($hostname, DNS_A);
|
||||
if ($validARecords) {
|
||||
return collect($validARecords)->first()['ip'];
|
||||
}
|
||||
|
||||
$validAAAARecords = @dns_get_record($hostname, DNS_AAAA);
|
||||
if ($validAAAARecords) {
|
||||
return collect($validAAAARecords)->first()['ipv6'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,6 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class,
|
||||
'captcha' => \App\Http\Middleware\VerifyCaptcha::class,
|
||||
]);
|
||||
})
|
||||
->withSingletons([
|
||||
|
@ -8,6 +8,10 @@ return [
|
||||
App\Providers\Filament\AdminPanelProvider::class,
|
||||
App\Providers\Filament\AppPanelProvider::class,
|
||||
App\Providers\Filament\ServerPanelProvider::class,
|
||||
App\Providers\Extensions\AvatarServiceProvider::class,
|
||||
App\Providers\Extensions\CaptchaServiceProvider::class,
|
||||
App\Providers\Extensions\FeatureServiceProvider::class,
|
||||
App\Providers\Extensions\OAuthServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
SocialiteProviders\Manager\ServiceProvider::class,
|
||||
];
|
||||
|
@ -103,4 +103,4 @@
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
@ -16,4 +16,6 @@ return [
|
||||
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
|
||||
],
|
||||
|
||||
'fallback_locale' => 'en',
|
||||
|
||||
];
|
||||
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WebhookType;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->string('type')->nullable()->after('id');
|
||||
$table->json('payload')->nullable()->after('type');
|
||||
});
|
||||
|
||||
foreach (WebhookConfiguration::all() as $webhookConfig) {
|
||||
$type = str($webhookConfig->endpoint)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value;
|
||||
|
||||
DB::table('webhook_configurations')
|
||||
->where('id', $webhookConfig->id)
|
||||
->update(['type' => $type]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
$table->dropColumn('payload');
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->json('headers')->nullable()->after('payload');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->dropColumn('headers');
|
||||
});
|
||||
}
|
||||
};
|
29
database/migrations/2025_07_06_213447_match-node-port.php
Normal file
29
database/migrations/2025_07_06_213447_match-node-port.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$nodes = Node::where('behind_proxy', false)->get();
|
||||
foreach ($nodes as $node) {
|
||||
$node->update(['daemon_connect' => $node->daemon_listen]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Not needed
|
||||
}
|
||||
};
|
122
lang/ar/activity.php
Normal file
122
lang/ar/activity.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Contains all of the translation strings for different activity log
|
||||
* events. These should be keyed by the value in front of the colon (:)
|
||||
* in the event name. If there is no colon present, they should live at
|
||||
* the top level.
|
||||
*/
|
||||
return [
|
||||
'auth' => [
|
||||
'fail' => 'فشل تسجيل الدخول',
|
||||
'success' => 'تم تسجيل الدخول',
|
||||
'password-reset' => 'إعادة تعيين كلمة المرور',
|
||||
'checkpoint' => 'تم طلب المصادقة الثنائية',
|
||||
'recovery-token' => 'تم استخدام رمز استعادة المصادقة الثنائية',
|
||||
'token' => 'تم حل تحدي المصادقة الثنائية',
|
||||
'ip-blocked' => 'تم حظر الطلب من عنوان IP غير مدرج لـ <b>:identifier</b>',
|
||||
'sftp' => [
|
||||
'fail' => 'فشل تسجيل الدخول عبر SFTP',
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
'account' => [
|
||||
'email-changed' => 'تم تغيير البريد الإلكتروني من <b>:old</b> إلى <b>:new</b>',
|
||||
'password-changed' => 'تم تغيير كلمة المرور',
|
||||
],
|
||||
'api-key' => [
|
||||
'create' => 'تم إنشاء مفتاح API جديد <b>:identifier</b>',
|
||||
'delete' => 'تم حذف مفتاح API <b>:identifier</b>',
|
||||
],
|
||||
'ssh-key' => [
|
||||
'create' => 'تمت إضافة مفتاح SSH <b>:fingerprint</b> إلى الحساب',
|
||||
'delete' => 'تمت إزالة مفتاح SSH <b>:fingerprint</b> من الحساب',
|
||||
],
|
||||
'two-factor' => [
|
||||
'create' => 'تم تمكين المصادقة الثنائية',
|
||||
'delete' => 'تم تعطيل المصادقة الثنائية',
|
||||
],
|
||||
],
|
||||
'server' => [
|
||||
'console' => [
|
||||
'command' => 'تم تنفيذ الأمر "<b>:command</b>" على الخادم',
|
||||
],
|
||||
'power' => [
|
||||
'start' => 'تم تشغيل الخادم',
|
||||
'stop' => 'تم إيقاف الخادم',
|
||||
'restart' => 'تم إعادة تشغيل الخادم',
|
||||
'kill' => 'تم إنهاء عملية الخادم',
|
||||
],
|
||||
'backup' => [
|
||||
'download' => 'تم تنزيل النسخة الاحتياطية <b>:name</b>',
|
||||
'delete' => 'تم حذف النسخة الاحتياطية <b>:name</b>',
|
||||
'restore' => 'تمت استعادة النسخة الاحتياطية <b>:name</b> (تم حذف الملفات: <b>:truncate</b>)',
|
||||
'restore-complete' => 'تمت استعادة النسخة الاحتياطية <b>:name</b> بنجاح',
|
||||
'restore-failed' => 'فشلت استعادة النسخة الاحتياطية <b>:name</b>',
|
||||
'start' => 'تم بدء نسخة احتياطية جديدة <b>:name</b>',
|
||||
'complete' => 'تم تمييز النسخة الاحتياطية <b>:name</b> كمكتملة',
|
||||
'fail' => 'تم تمييز النسخة الاحتياطية <b>:name</b> كفاشلة',
|
||||
'lock' => 'تم قفل النسخة الاحتياطية <b>:name</b>',
|
||||
'unlock' => 'تم فك قفل النسخة الاحتياطية <b>:name</b>',
|
||||
],
|
||||
'database' => [
|
||||
'create' => 'تم إنشاء قاعدة بيانات جديدة <b>:name</b>',
|
||||
'rotate-password' => 'تم تغيير كلمة مرور قاعدة البيانات <b>:name</b>',
|
||||
'delete' => 'تم حذف قاعدة البيانات <b>:name</b>',
|
||||
],
|
||||
'file' => [
|
||||
'compress' => 'تم ضغط <b>:directory:files</b>|تم ضغط <b>:count</b> ملفات في <b>:directory</b>',
|
||||
'read' => 'تم عرض محتوى <b>:file</b>',
|
||||
'copy' => 'تم إنشاء نسخة من <b>:file</b>',
|
||||
'create-directory' => 'تم إنشاء المجلد <b>:directory:name</b>',
|
||||
'decompress' => 'تم فك ضغط <b>:file</b> في <b>:directory</b>',
|
||||
'delete' => 'تم حذف <b>:directory:files</b>|تم حذف <b>:count</b> ملفات في <b>:directory</b>',
|
||||
'download' => 'تم تنزيل <b>:file</b>',
|
||||
'pull' => 'تم تنزيل ملف عن بعد من <b>:url</b> إلى <b>:directory</b>',
|
||||
'rename' => 'تم نقل/إعادة تسمية <b>:from</b> إلى <b>:to</b>|تم نقل/إعادة تسمية <b>:count</b> ملفات في <b>:directory</b>',
|
||||
'write' => 'تمت كتابة محتوى جديد إلى <b>:file</b>',
|
||||
'upload' => 'تم بدء رفع ملف',
|
||||
'uploaded' => 'تم رفع <b>:directory:file</b>',
|
||||
],
|
||||
'sftp' => [
|
||||
'denied' => 'تم حظر الوصول إلى SFTP بسبب الأذونات',
|
||||
'create' => 'تم إنشاء <b>:files</b>|تم إنشاء <b>:count</b> ملفات جديدة',
|
||||
'write' => 'تم تعديل محتوى <b>:files</b>|تم تعديل محتوى <b>:count</b> ملفات',
|
||||
'delete' => 'تم حذف <b>:files</b>|تم حذف <b>:count</b> ملفات',
|
||||
'create-directory' => 'تم إنشاء المجلد <b>:files</b>|تم إنشاء <b>:count</b> مجلدات',
|
||||
'rename' => 'تمت إعادة تسمية <b>:from</b> إلى <b>:to</b>|تمت إعادة تسمية أو نقل <b>:count</b> ملفات',
|
||||
],
|
||||
'allocation' => [
|
||||
'create' => 'تمت إضافة <b>:allocation</b> إلى الخادم',
|
||||
'notes' => 'تم تحديث الملاحظات لـ <b>:allocation</b> من "<b>:old</b>" إلى "<b>:new</b>"',
|
||||
'primary' => 'تم تعيين <b>:allocation</b> كالتخصيص الأساسي للخادم',
|
||||
'delete' => 'تم حذف التخصيص <b>:allocation</b>',
|
||||
],
|
||||
'schedule' => [
|
||||
'create' => 'تم إنشاء الجدولة <b>:name</b>',
|
||||
'update' => 'تم تحديث الجدولة <b>:name</b>',
|
||||
'execute' => 'تم تنفيذ الجدولة <b>:name</b> يدويًا',
|
||||
'delete' => 'تم حذف الجدولة <b>:name</b>',
|
||||
],
|
||||
'task' => [
|
||||
'create' => 'تم إنشاء مهمة جديدة "<b>:action</b>" لجدولة <b>:name</b>',
|
||||
'update' => 'تم تحديث المهمة "<b>:action</b>" لجدولة <b>:name</b>',
|
||||
'delete' => 'تم حذف "<b>:action</b>" لمهمة الجدول <b>:name</b>',
|
||||
],
|
||||
'settings' => [
|
||||
'rename' => 'تمت إعادة تسمية الخادم من "<b>:old</b>" إلى "<b>:new</b>"',
|
||||
'description' => 'تم تغيير وصف الخادم من "<b>:old</b>" إلى "<b>:new</b>"',
|
||||
'reinstall' => 'تم إعادة تثبيت الخادم',
|
||||
],
|
||||
'startup' => [
|
||||
'edit' => 'تم تغيير المتغير <b>:variable</b> من "<b>:old</b>" إلى "<b>:new</b>"',
|
||||
'image' => 'تم تحديث صورة Docker للخادم من <b>:old</b> إلى <b>:new</b>',
|
||||
],
|
||||
'subuser' => [
|
||||
'create' => 'تمت إضافة <b>:email</b> كمستخدم فرعي',
|
||||
'update' => 'تم تحديث أذونات المستخدم الفرعي <b>:email</b>',
|
||||
'delete' => 'تمت إزالة <b>:email</b> كمستخدم فرعي',
|
||||
],
|
||||
'crashed' => 'تعطل الخادم',
|
||||
],
|
||||
];
|
27
lang/ar/admin/apikey.php
Normal file
27
lang/ar/admin/apikey.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'مفاتيح API للتطبيق',
|
||||
'empty_table' => 'لا توجد مفاتيح API.',
|
||||
'whitelist' => 'عناوين IPv4 المسموح بها',
|
||||
'whitelist_help' => 'يمكن تقييد مفاتيح API بحيث تعمل فقط من عناوين IPv4 محددة. أدخل كل عنوان في سطر جديد.',
|
||||
'whitelist_placeholder' => 'مثال: 127.0.0.1 أو 192.168.1.1',
|
||||
'description' => 'الوصف',
|
||||
'description_help' => 'وصف موجز لاستخدام هذا المفتاح.',
|
||||
'nav_title' => 'مفاتيح API',
|
||||
'model_label' => 'مفتاح API للتطبيق',
|
||||
'model_label_plural' => 'مفاتيح API للتطبيق',
|
||||
'table' => [
|
||||
'key' => 'المفتاح',
|
||||
'description' => 'الوصف',
|
||||
'last_used' => 'آخر استخدام',
|
||||
'created' => 'تم الإنشاء',
|
||||
'created_by' => 'تم الإنشاء بواسطة',
|
||||
'never_used' => 'لم يُستخدم أبدًا',
|
||||
],
|
||||
'permissions' => [
|
||||
'none' => 'لا شيء',
|
||||
'read' => 'قراءة',
|
||||
'read_write' => 'قراءة وكتابة',
|
||||
],
|
||||
];
|
44
lang/ar/admin/dashboard.php
Normal file
44
lang/ar/admin/dashboard.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'heading' => 'مرحبًا بك في Pelican!',
|
||||
'version' => 'الإصدار: :version',
|
||||
'advanced' => 'متقدم',
|
||||
'server' => 'الخادم',
|
||||
'user' => 'المستخدم',
|
||||
'sections' => [
|
||||
'intro-developers' => [
|
||||
'heading' => 'معلومات للمطورين',
|
||||
'content' => 'شكرًا لك على تجربة الإصدار التجريبي!',
|
||||
'extra_note' => 'إذا واجهت أي مشاكل، يرجى الإبلاغ عنها على GitHub.',
|
||||
'button_issues' => 'إنشاء مشكلة',
|
||||
'button_features' => 'مناقشة الميزات',
|
||||
],
|
||||
'intro-update-available' => [
|
||||
'heading' => 'تحديث متاح',
|
||||
'content' => ':latestVersion متوفر الآن! اقرأ وثائقنا لتحديث اللوحة الخاصة بك.',
|
||||
],
|
||||
'intro-no-update' => [
|
||||
'heading' => 'لوحتك محدثة',
|
||||
'content' => 'أنت تستخدم حاليًا الإصدار :version. لوحتك محدثة بالكامل!',
|
||||
],
|
||||
'intro-first-node' => [
|
||||
'heading' => 'لم يتم اكتشاف أي عقد',
|
||||
'content' => 'يبدو أنه لم يتم إعداد أي عقد حتى الآن، لكن لا تقلق، يمكنك النقر على زر الإجراء لإنشاء العقدة الأولى!',
|
||||
'extra_note' => 'إذا واجهت أي مشاكل، يرجى الإبلاغ عنها على GitHub.',
|
||||
'button_label' => 'إنشاء العقدة الأولى في Pelican',
|
||||
],
|
||||
'intro-support' => [
|
||||
'heading' => 'دعم Pelican',
|
||||
'content' => 'شكرًا لاستخدامك Pelican، لقد تحقق هذا بفضل دعمك، ودعم المساهمين، وبقية داعمينا!',
|
||||
'extra_note' => 'نقدر أي نوع من الدعم من الجميع.',
|
||||
'button_translate' => 'المساعدة في الترجمة',
|
||||
'button_donate' => 'التبرع مباشرة',
|
||||
],
|
||||
'intro-help' => [
|
||||
'heading' => 'بحاجة إلى مساعدة؟',
|
||||
'content' => 'تحقق من الوثائق أولًا! إذا كنت لا تزال بحاجة إلى المساعدة، انضم إلى خادم الديسكورد الخاص بنا!',
|
||||
'button_docs' => 'قراءة الوثائق',
|
||||
],
|
||||
],
|
||||
];
|
73
lang/ar/admin/databasehost.php
Normal file
73
lang/ar/admin/databasehost.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'مضيفو قواعد البيانات',
|
||||
'model_label' => 'مضيف قاعدة بيانات',
|
||||
'model_label_plural' => 'مضيفو قواعد البيانات',
|
||||
'table' => [
|
||||
'database' => 'قاعدة البيانات',
|
||||
'name' => 'الاسم',
|
||||
'host' => 'المضيف',
|
||||
'port' => 'المنفذ',
|
||||
'name_helper' => 'ترك هذا الحقل فارغًا سيؤدي إلى إنشاء اسم عشوائي تلقائيًا',
|
||||
'username' => 'اسم المستخدم',
|
||||
'password' => 'كلمة المرور',
|
||||
'remote' => 'الاتصالات من',
|
||||
'remote_helper' => 'المكان الذي يجب السماح بالاتصالات منه. اتركه فارغًا للسماح بالاتصالات من أي مكان.',
|
||||
'max_connections' => 'الحد الأقصى للاتصالات',
|
||||
'created_at' => 'تم الإنشاء في',
|
||||
'connection_string' => 'سلسلة اتصال JDBC',
|
||||
],
|
||||
'error' => 'خطأ في الاتصال بالمضيف',
|
||||
'host' => 'المضيف',
|
||||
'host_help' => 'عنوان IP أو اسم النطاق الذي يجب استخدامه عند محاولة الاتصال بهذا المضيف MySQL من هذه اللوحة لإنشاء قواعد بيانات جديدة.',
|
||||
'port' => 'المنفذ',
|
||||
'port_help' => 'المنفذ الذي يعمل عليه MySQL لهذا المضيف.',
|
||||
'max_database' => 'الحد الأقصى لعدد قواعد البيانات',
|
||||
'max_databases_help' => 'الحد الأقصى لعدد قواعد البيانات التي يمكن إنشاؤها على هذا المضيف. إذا تم الوصول إلى الحد، فلن يمكن إنشاء قواعد بيانات جديدة على هذا المضيف. اتركه فارغًا ليكون غير محدود.',
|
||||
'display_name' => 'اسم العرض',
|
||||
'display_name_help' => 'عنوان IP أو اسم المجال الذي يجب أن يظهر للمستخدم النهائي.',
|
||||
'username' => 'اسم المستخدم',
|
||||
'username_help' => 'اسم المستخدم لحساب لديه أذونات كافية لإنشاء مستخدمين جدد وقواعد بيانات على النظام.',
|
||||
'password' => 'كلمة المرور',
|
||||
'password_help' => 'كلمة المرور الخاصة بمستخدم قاعدة البيانات.',
|
||||
'linked_nodes' => 'العقد المرتبطة',
|
||||
'linked_nodes_help' => 'يكون هذا الإعداد افتراضيًا فقط لهذا المضيف عند إضافة قاعدة بيانات إلى خادم على العقدة المحددة.',
|
||||
'connection_error' => 'خطأ في الاتصال بمضيف قاعدة البيانات',
|
||||
'no_database_hosts' => 'لا توجد مضيفات قواعد بيانات',
|
||||
'no_nodes' => 'لا توجد عقد',
|
||||
'delete_help' => 'مضيف قاعدة البيانات يحتوي على قواعد بيانات',
|
||||
'unlimited' => 'غير محدود',
|
||||
'anywhere' => 'في أي مكان',
|
||||
|
||||
'rotate' => 'تدوير',
|
||||
'rotate_password' => 'تدوير كلمة المرور',
|
||||
'rotated' => 'تم تدوير كلمة المرور',
|
||||
'rotate_error' => 'فشل تدوير كلمة المرور',
|
||||
'databases' => 'قواعد البيانات',
|
||||
|
||||
'setup' => [
|
||||
'preparations' => 'التحضيرات',
|
||||
'database_setup' => 'إعداد قاعدة البيانات',
|
||||
'panel_setup' => 'إعداد اللوحة',
|
||||
|
||||
'note' => 'حالياً، يتم دعم قواعد البيانات MySQL/MariaDB فقط لمضيفي قاعدة البيانات!',
|
||||
'different_server' => 'هل اللوحة وقاعدة البيانات <i>ليسا</i> على نفس الخادم؟',
|
||||
|
||||
'database_user' => 'مستخدم قاعدة البيانات',
|
||||
'cli_login' => 'استخدم <code>mysql -u root -p</code> للوصول إلى mysql cli.',
|
||||
'command_create_user' => 'الأمر لإنشاء المستخدم',
|
||||
'command_assign_permissions' => 'الأمر لتعيين الصلاحيات',
|
||||
'cli_exit' => 'للخروج من mysql cli عليك تشغيل الأمر <code>exit</code>.',
|
||||
'external_access' => 'الوصول الخارجي',
|
||||
'allow_external_access' => '
|
||||
<p>من المحتمل أنك ستحتاج إلى السماح بالوصول الخارجي إلى قاعدة البيانات MySQL حتى تتمكن الخوادم من الاتصال به.</p>
|
||||
<br>
|
||||
<p>لتفعل ذلك، عليك فتح الملف<code>my.cnf</code>، الذي يختلف مكانه اعتمادا على نظام التشغيل وكيفية تثبيت قاعدة البيانات MYSQL، يمكنك كتابة <code>/etc -iname my.cnf</code> لتجده</p>
|
||||
<br>
|
||||
<p>افتح الملف <code>my.cnf</code>، اضف النص المشار عليه في الأسفل إلى نهاية الملف ثم قم بحفظه:<br>
|
||||
<code>[mysqld]<br>bind-address=0.0.0.0</code></p>
|
||||
<br>
|
||||
<p>أعد تشغيل MySQL/ MariaDB لتطبيق هذه التغيرات، هذا سيتجاوز إعدادات MySQL الافتراضية، التي في الافتراضي تقبل الطلبات فقط من الجهاز المحلي، تحديث هذا سيسمح بالاتصالات على جميع الواجهات، وبالتالي الاتصالات الخارجية، تأكد من السماح لمنفذ MySQL (الافتراضي 3306) في جدار الحماية الخاص بك.</p> ',
|
||||
],
|
||||
];
|
60
lang/ar/admin/health.php
Normal file
60
lang/ar/admin/health.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'الصحة',
|
||||
'results_refreshed' => 'تم تحديث نتائج فحص الصحة',
|
||||
'checked' => 'تم التحقق من النتائج منذ :time',
|
||||
'refresh' => 'تحديث',
|
||||
'results' => [
|
||||
'cache' => [
|
||||
'label' => 'التخزين المؤقت',
|
||||
'ok' => 'موافق',
|
||||
'failed_retrieve' => 'تعذر تعيين أو استرجاع قيمة التخزين المؤقت للتطبيق.',
|
||||
'failed' => 'حدث استثناء في التخزين المؤقت للتطبيق: :error',
|
||||
],
|
||||
'database' => [
|
||||
'label' => 'قاعدة البيانات',
|
||||
'ok' => 'موافق',
|
||||
'failed' => 'تعذر الاتصال بقاعدة البيانات: :error',
|
||||
],
|
||||
'debugmode' => [
|
||||
'label' => 'وضع التصحيح',
|
||||
'ok' => 'وضع التصحيح معطل',
|
||||
'failed' => 'كان من المتوقع أن يكون وضع التصحيح :expected، لكنه كان :actual',
|
||||
],
|
||||
'environment' => [
|
||||
'label' => 'البيئة',
|
||||
'ok' => 'موافق، تم التعيين إلى :actual',
|
||||
'failed' => 'تم تعيين البيئة إلى :actual، بينما كان المتوقع :expected',
|
||||
],
|
||||
'nodeversions' => [
|
||||
'label' => 'إصدارات العقد',
|
||||
'ok' => 'العقد محدثة',
|
||||
'failed' => ':outdated/:all من العقد قديمة',
|
||||
'no_nodes_created' => 'لم يتم إنشاء أي عقد',
|
||||
'no_nodes' => 'لا توجد عقد',
|
||||
'all_up_to_date' => 'جميعها محدثة',
|
||||
'outdated' => ':outdated/:all قديمة',
|
||||
],
|
||||
'panelversion' => [
|
||||
'label' => 'إصدار اللوحة',
|
||||
'ok' => 'اللوحة محدثة',
|
||||
'failed' => 'الإصدار المثبت هو :currentVersion بينما الأحدث هو :latestVersion',
|
||||
'up_to_date' => 'محدث',
|
||||
'outdated' => 'قديم',
|
||||
],
|
||||
'schedule' => [
|
||||
'label' => 'الجدولة',
|
||||
'ok' => 'موافق',
|
||||
'failed_last_ran' => 'آخر تشغيل للجدولة كان قبل أكثر من :time دقيقة',
|
||||
'failed_not_ran' => 'لم يتم تشغيل الجدولة بعد.',
|
||||
],
|
||||
'useddiskspace' => [
|
||||
'label' => 'مساحة القرص',
|
||||
],
|
||||
],
|
||||
'checks' => [
|
||||
'successful' => 'ناجح',
|
||||
'failed' => 'فشل',
|
||||
],
|
||||
];
|
30
lang/ar/admin/mount.php
Normal file
30
lang/ar/admin/mount.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'التوصيلات',
|
||||
'model_label' => 'التوصيل',
|
||||
'model_label_plural' => 'التوصيلات',
|
||||
'name' => 'الاسم',
|
||||
'name_help' => 'اسم فريد يستخدم لتمييز هذا التوصيل عن غيره.',
|
||||
'source' => 'المصدر',
|
||||
'source_help' => 'مسار الملف في نظام المضيف ليتم توصيله بالحاوية.',
|
||||
'target' => 'الهدف',
|
||||
'target_help' => 'المكان الذي سيكون فيه التوصيل متاحًا داخل الحاوية.',
|
||||
'read_only' => 'للقراءة فقط؟',
|
||||
'read_only_help' => 'هل التوصيل للقراءة فقط داخل الحاوية؟',
|
||||
'description' => 'الوصف',
|
||||
'description_help' => 'وصف تفصيلي لهذا التوصيل',
|
||||
'no_mounts' => 'لا توجد توصيلات',
|
||||
'eggs' => 'البيوض',
|
||||
'nodes' => 'العقد',
|
||||
'toggles' => [
|
||||
'writable' => 'قابل للكتابة',
|
||||
'read_only' => 'للقراءة فقط',
|
||||
],
|
||||
'table' => [
|
||||
'name' => 'الاسم',
|
||||
'all_eggs' => 'جميع البيض',
|
||||
'all_nodes' => 'جميع العقد',
|
||||
'read_only' => 'للقراءة فقط',
|
||||
],
|
||||
];
|
17
lang/ar/admin/role.php
Normal file
17
lang/ar/admin/role.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'الأدوار',
|
||||
'model_label' => 'الدور',
|
||||
'model_label_plural' => 'الأدوار',
|
||||
'no_roles' => 'لا توجد أدوار',
|
||||
'name' => 'اسم الدور',
|
||||
'permissions' => 'الأذونات',
|
||||
'in_use' => 'قيد الاستخدام',
|
||||
'all' => 'الكل',
|
||||
'root_admin' => 'الدور :role يمتلك جميع الأذونات.',
|
||||
'root_admin_delete' => 'لا يمكن حذف المسؤول الجذري',
|
||||
'users' => 'المستخدمون',
|
||||
'nodes' => 'العقد',
|
||||
'nodes_hint' => 'اتركه فارغاً للسماح بالوصول إلى جميع العقد.',
|
||||
];
|
18
lang/ar/admin/user.php
Normal file
18
lang/ar/admin/user.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'المستخدمون',
|
||||
'model_label' => 'المستخدم',
|
||||
'model_label_plural' => 'المستخدمون',
|
||||
'self_delete' => 'لا يمكنك حذف نفسك',
|
||||
'has_servers' => 'المستخدم يمتلك خوادم',
|
||||
'email' => 'البريد الإلكتروني',
|
||||
'username' => 'اسم المستخدم',
|
||||
'password' => 'كلمة المرور',
|
||||
'password_help' => 'إدخال كلمة المرور للمستخدم اختياري. سيتلقى المستخدم الجديد بريدًا إلكترونيًا يطلب منه إنشاء كلمة مرور عند تسجيل الدخول لأول مرة.',
|
||||
'admin_roles' => 'أدوار المسؤول',
|
||||
'roles' => 'الأدوار',
|
||||
'no_roles' => 'لا توجد أدوار',
|
||||
'servers' => 'الخوادم',
|
||||
'subusers' => 'المستخدمون الفرعيون',
|
||||
];
|
57
lang/ar/command/messages.php
Normal file
57
lang/ar/command/messages.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'user' => [
|
||||
'search_users' => 'أدخل اسم المستخدم، معرّف المستخدم، أو عنوان البريد الإلكتروني',
|
||||
'select_search_user' => 'معرّف المستخدم الذي سيتم حذفه (أدخل \'0\' لإعادة البحث)',
|
||||
'deleted' => 'تم حذف المستخدم بنجاح من اللوحة.',
|
||||
'confirm_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المستخدم من اللوحة؟',
|
||||
'no_users_found' => 'لم يتم العثور على مستخدمين لمصطلح البحث المقدم.',
|
||||
'multiple_found' => 'تم العثور على عدة حسابات للمستخدم المقدم، لا يمكن حذف المستخدم بسبب علامة --no-interaction.',
|
||||
'ask_admin' => 'هل هذا المستخدم مدير؟',
|
||||
'ask_email' => 'عنوان البريد الإلكتروني',
|
||||
'ask_username' => 'اسم المستخدم',
|
||||
'ask_password' => 'كلمة المرور',
|
||||
'ask_password_tip' => 'إذا كنت ترغب في إنشاء حساب بكلمة مرور عشوائية يتم إرسالها بالبريد الإلكتروني للمستخدم، أعد تشغيل هذا الأمر (CTRL+C) ومرر علامة `--no-password`.',
|
||||
'ask_password_help' => 'يجب أن تكون كلمات المرور بطول 8 أحرف على الأقل وتحتوي على حرف كبير ورقم على الأقل.',
|
||||
'2fa_help_text' => [
|
||||
'هذا الأمر سيعطل التوثيق الثنائي لحساب المستخدم إذا كان مفعلاً. يجب استخدام هذا فقط كأمر استرداد حساب إذا كان المستخدم محظورًا من حسابه.',
|
||||
'إذا لم يكن هذا ما تريد القيام به، اضغط CTRL+C للخروج من هذه العملية.',
|
||||
],
|
||||
'2fa_disabled' => 'تم تعطيل التوثيق الثنائي لـ :email.',
|
||||
],
|
||||
'schedule' => [
|
||||
'output_line' => 'يتم تنفيذ المهمة الأولى في `:schedule` (:id).',
|
||||
],
|
||||
'maintenance' => [
|
||||
'deleting_service_backup' => 'جاري حذف ملف النسخ الاحتياطي للخدمة :file.',
|
||||
],
|
||||
'server' => [
|
||||
'rebuild_failed' => 'فشل طلب إعادة بناء ":name" (#:id) على العقدة ":node" مع الخطأ: :message',
|
||||
'reinstall' => [
|
||||
'failed' => 'فشل طلب إعادة تثبيت ":name" (#:id) على العقدة ":node" مع الخطأ: :message',
|
||||
'confirm' => 'أنت على وشك إعادة تثبيت مجموعة من الخوادم. هل ترغب في المتابعة؟',
|
||||
],
|
||||
'power' => [
|
||||
'confirm' => 'أنت على وشك تنفيذ :action ضد :count خوادم. هل ترغب في المتابعة؟',
|
||||
'action_failed' => 'فشل طلب تنفيذ الطاقة لـ ":name" (#:id) على العقدة ":node" مع الخطأ: :message',
|
||||
],
|
||||
],
|
||||
'environment' => [
|
||||
'mail' => [
|
||||
'ask_smtp_host' => 'مضيف SMTP (مثل smtp.gmail.com)',
|
||||
'ask_smtp_port' => 'منفذ SMTP',
|
||||
'ask_smtp_username' => 'اسم مستخدم SMTP',
|
||||
'ask_smtp_password' => 'كلمة مرور SMTP',
|
||||
'ask_mailgun_domain' => 'نطاق Mailgun',
|
||||
'ask_mailgun_endpoint' => 'نقطة نهاية Mailgun',
|
||||
'ask_mailgun_secret' => 'سر Mailgun',
|
||||
'ask_mandrill_secret' => 'سر Mandrill',
|
||||
'ask_postmark_username' => 'مفتاح API Postmark',
|
||||
'ask_driver' => 'أي برنامج يجب استخدامه لإرسال الرسائل البريدية؟',
|
||||
'ask_mail_from' => 'عنوان البريد الإلكتروني الذي يجب أن تنشأ منه الرسائل',
|
||||
'ask_mail_name' => 'الاسم الذي يجب أن تظهر منه الرسائل',
|
||||
'ask_encryption' => 'طريقة التشفير المستخدمة',
|
||||
],
|
||||
],
|
||||
];
|
73
lang/ar/commands.php
Normal file
73
lang/ar/commands.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'appsettings' => [
|
||||
'comment' => [
|
||||
'author' => 'قم بتوفير عنوان البريد الإلكتروني الذي يجب أن تصدر منه البيوض المصدرة بواسطة هذا اللوحة. يجب أن يكون عنوان بريد إلكتروني صالحًا.',
|
||||
'url' => 'يجب أن يبدأ عنوان URL للتطبيق بـ https:// أو http:// حسب استخدامك لـ SSL أم لا. إذا لم تقم بتضمين المخطط، فقد يتم ربط رسائل البريد الإلكتروني والمحتوى الآخر بموقع غير صحيح.',
|
||||
'timezone' => 'يجب أن تتطابق المنطقة الزمنية مع إحدى المناطق الزمنية المدعومة من PHP. إذا كنت غير متأكد، يرجى الرجوع إلى https://php.net/manual/en/timezones.php.',
|
||||
],
|
||||
'redis' => [
|
||||
'note' => 'لقد اخترت برنامج Redis لسائق واحد أو أكثر، يرجى تقديم معلومات اتصال صالحة أدناه. في معظم الحالات، يمكنك استخدام الإعدادات الافتراضية ما لم تكن قد عدلت إعدادك.',
|
||||
'comment' => 'بشكل افتراضي، يكون اسم المستخدم الافتراضي لخادم Redis بدون كلمة مرور لأنه يعمل محليًا وغير متاح للعالم الخارجي. إذا كان هذا هو الحال، فقط اضغط على Enter دون إدخال قيمة.',
|
||||
'confirm' => 'يبدو أن هناك قيمة :field محددة بالفعل لـ Redis، هل ترغب في تغييرها؟',
|
||||
],
|
||||
],
|
||||
'database_settings' => [
|
||||
'DB_HOST_note' => 'يُوصى بشدة بعدم استخدام "localhost" كمضيف قاعدة البيانات، حيث لاحظنا مشكلات متكررة في اتصال المقبس. إذا كنت تريد استخدام اتصال محلي، فيجب أن تستخدم "127.0.0.1".',
|
||||
'DB_USERNAME_note' => 'استخدام حساب الجذر لاتصالات MySQL ليس فقط مرفوضًا بشدة، ولكنه غير مسموح به في هذا التطبيق. ستحتاج إلى إنشاء مستخدم MySQL لهذا البرنامج.',
|
||||
'DB_PASSWORD_note' => 'يبدو أن لديك بالفعل كلمة مرور اتصال MySQL محددة، هل ترغب في تغييرها؟',
|
||||
'DB_error_2' => 'لم يتم حفظ بيانات الاعتماد الخاصة باتصالك. ستحتاج إلى تقديم معلومات اتصال صالحة قبل المتابعة.',
|
||||
'go_back' => 'العودة والمحاولة مرة أخرى',
|
||||
],
|
||||
'make_node' => [
|
||||
'name' => 'أدخل معرفًا قصيرًا لتمييز هذه العقدة عن غيرها',
|
||||
'description' => 'أدخل وصفًا لتحديد العقدة',
|
||||
'scheme' => 'يرجى إدخال https لاستخدام SSL أو http لاتصال غير مشفر',
|
||||
'fqdn' => 'أدخل اسم النطاق (مثل node.example.com) ليتم استخدامه للاتصال بالـ Daemon. يمكن استخدام عنوان IP فقط إذا لم تكن تستخدم SSL لهذه العقدة.',
|
||||
'public' => 'هل يجب أن تكون هذه العقدة عامة؟ ملاحظة: تعيين العقدة كخاصة سيمنع إمكانية النشر التلقائي لهذه العقدة.',
|
||||
'behind_proxy' => 'هل اسم النطاق الخاص بك خلف وكيل؟',
|
||||
'maintenance_mode' => 'هل يجب تمكين وضع الصيانة؟',
|
||||
'memory' => 'أدخل الحد الأقصى للذاكرة',
|
||||
'memory_overallocate' => 'أدخل مقدار الذاكرة المطلوب تجاوزه، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
|
||||
'disk' => 'أدخل الحد الأقصى لمساحة القرص',
|
||||
'disk_overallocate' => 'أدخل مقدار القرص المطلوب تجاوزه، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
|
||||
'cpu' => 'أدخل الحد الأقصى لاستخدام المعالج',
|
||||
'cpu_overallocate' => 'أدخل مقدار تجاوز استخدام المعالج، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
|
||||
'upload_size' => 'أدخل الحد الأقصى لحجم التحميل',
|
||||
'daemonListen' => 'أدخل منفذ استماع الـ Daemon',
|
||||
'daemonSFTP' => 'أدخل منفذ استماع SFTP لـ Daemon',
|
||||
'daemonSFTPAlias' => 'أدخل اسم مستعار لـ SFTP (يمكن أن يكون فارغًا)',
|
||||
'daemonBase' => 'أدخل المجلد الأساسي',
|
||||
'success' => 'تم إنشاء عقدة جديدة بنجاح بالاسم :name ومعرفها :id',
|
||||
],
|
||||
'node_config' => [
|
||||
'error_not_exist' => 'العقدة المحددة غير موجودة.',
|
||||
'error_invalid_format' => 'تنسيق غير صالح محدد. الخيارات الصالحة هي yaml و json.',
|
||||
],
|
||||
'key_generate' => [
|
||||
'error_already_exist' => 'يبدو أنك قمت بالفعل بتكوين مفتاح تشفير التطبيق. المتابعة مع هذه العملية ستؤدي إلى استبدال هذا المفتاح وقد تسبب في تلف البيانات المشفرة الموجودة. لا تتابع ما لم تكن متأكدًا مما تفعله.',
|
||||
'understand' => 'أفهم عواقب تنفيذ هذا الأمر وأتحمل كامل المسؤولية عن فقدان البيانات المشفرة.',
|
||||
'continue' => 'هل أنت متأكد أنك تريد المتابعة؟ تغيير مفتاح تشفير التطبيق سيسبب فقدان البيانات.',
|
||||
],
|
||||
'schedule' => [
|
||||
'process' => [
|
||||
'no_tasks' => 'لا توجد مهام مجدولة للخوادم تحتاج إلى التشغيل.',
|
||||
'error_message' => 'حدث خطأ أثناء معالجة الجدولة: ',
|
||||
],
|
||||
],
|
||||
'upgrade' => [
|
||||
'integrity' => 'هذا الأمر لا يتحقق من سلامة الأصول التي تم تنزيلها. يرجى التأكد من أنك تثق في مصدر التنزيل قبل المتابعة. إذا كنت لا ترغب في تنزيل أرشيف، يرجى تحديد ذلك باستخدام العلامة --skip-download، أو الإجابة بـ "لا" على السؤال أدناه.',
|
||||
'source_url' => 'مصدر التنزيل (يتم تعيينه باستخدام --url=):',
|
||||
'php_version' => 'تعذر تنفيذ عملية الترقية الذاتية. الحد الأدنى المطلوب لإصدار PHP هو 7.4.0، لديك',
|
||||
'skipDownload' => 'هل ترغب في تنزيل واستخراج ملفات الأرشيف لأحدث إصدار؟',
|
||||
'webserver_user' => 'تم اكتشاف مستخدم خادم الويب الخاص بك على أنه <fg=blue>[{:user}]:</> هل هذا صحيح؟',
|
||||
'name_webserver' => 'يرجى إدخال اسم المستخدم الذي يشغل عملية خادم الويب لديك. يختلف هذا من نظام إلى آخر، لكنه عادةً يكون "www-data"، "nginx"، أو "apache".',
|
||||
'group_webserver' => 'تم اكتشاف مجموعة خادم الويب الخاصة بك على أنها <fg=blue>[{:group}]:</> هل هذا صحيح؟',
|
||||
'group_webserver_question' => 'يرجى إدخال اسم المجموعة التي تشغل عملية خادم الويب لديك. عادةً ما تكون هي نفس اسم المستخدم.',
|
||||
'are_your_sure' => 'هل أنت متأكد أنك تريد تنفيذ عملية الترقية للوحة التحكم؟',
|
||||
'terminated' => 'تم إنهاء عملية الترقية بواسطة المستخدم.',
|
||||
'success' => 'تم ترقية اللوحة بنجاح. يرجى التأكد من تحديث أي مثيلات Daemon أيضًا.',
|
||||
|
||||
],
|
||||
];
|
64
lang/ar/exceptions.php
Normal file
64
lang/ar/exceptions.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'daemon_connection_failed' => 'حدث خطأ أثناء محاولة الاتصال بـ Daemon مما أدى إلى استجابة برمز HTTP/:code. تم تسجيل هذا الاستثناء.',
|
||||
'node' => [
|
||||
'servers_attached' => 'يجب ألا يكون هناك أي خوادم مرتبطة بهذه العقدة حتى يتم حذفها.',
|
||||
'error_connecting' => 'حدث خطأ أثناء الاتصال بـ :node',
|
||||
'daemon_off_config_updated' => 'تم <strong>تحديث تكوين Daemon</strong>، ولكن حدث خطأ أثناء محاولة تحديث ملف التكوين تلقائيًا. ستحتاج إلى تحديث ملف التكوين (config.yml) يدويًا ليتم تطبيق هذه التغييرات.',
|
||||
],
|
||||
'allocations' => [
|
||||
'server_using' => 'تم تعيين خادم حاليًا لهذا التخصيص. لا يمكن حذف التخصيص إلا إذا لم يكن هناك خادم معين له.',
|
||||
'too_many_ports' => 'إضافة أكثر من 1000 منفذ في نطاق واحد دفعة واحدة غير مدعومة.',
|
||||
'invalid_mapping' => 'التعيين المقدم للمنفذ :port غير صالح ولا يمكن معالجته.',
|
||||
'cidr_out_of_range' => 'صيغة CIDR تسمح فقط بالأقنعة بين /25 و /32.',
|
||||
'port_out_of_range' => 'يجب أن تكون المنافذ في التخصيص بين 1024 و 65535.',
|
||||
],
|
||||
'egg' => [
|
||||
'delete_has_servers' => 'لا يمكن حذف بيضة تحتوي على خوادم نشطة من اللوحة.',
|
||||
'invalid_copy_id' => 'البيضة المحددة لنسخ السكريبت منها غير موجودة أو تقوم بنسخ سكريبت بنفسها.',
|
||||
'has_children' => 'هذه البيضة تحتوي على بيضات فرعية. يرجى حذفها قبل حذف هذه البيضة.',
|
||||
],
|
||||
'variables' => [
|
||||
'env_not_unique' => 'يجب أن يكون المتغير البيئي :name فريدًا لهذه البيضة.',
|
||||
'reserved_name' => 'المتغير البيئي :name محجوز ولا يمكن تعيينه كمتغير.',
|
||||
'bad_validation_rule' => 'قاعدة التحقق ":rule" غير صالحة لهذا التطبيق.',
|
||||
],
|
||||
'importer' => [
|
||||
'json_error' => 'حدث خطأ أثناء تحليل ملف JSON: :error.',
|
||||
'file_error' => 'ملف JSON المقدم غير صالح.',
|
||||
'invalid_json_provided' => 'ملف JSON المقدم ليس بتنسيق يمكن التعرف عليه.',
|
||||
],
|
||||
'subusers' => [
|
||||
'editing_self' => 'لا يُسمح بتعديل حساب المستخدم الفرعي الخاص بك.',
|
||||
'user_is_owner' => 'لا يمكنك إضافة مالك الخادم كمستخدم فرعي لهذا الخادم.',
|
||||
'subuser_exists' => 'المستخدم بعنوان البريد الإلكتروني هذا مضاف بالفعل كمستخدم فرعي لهذا الخادم.',
|
||||
],
|
||||
'databases' => [
|
||||
'delete_has_databases' => 'لا يمكن حذف مضيف قاعدة بيانات يحتوي على قواعد بيانات نشطة مرتبطة به.',
|
||||
],
|
||||
'tasks' => [
|
||||
'chain_interval_too_long' => 'الحد الأقصى للفاصل الزمني لمهمة متسلسلة هو 15 دقيقة.',
|
||||
],
|
||||
'locations' => [
|
||||
'has_nodes' => 'لا يمكن حذف موقع يحتوي على عقد نشطة مرتبطة به.',
|
||||
],
|
||||
'users' => [
|
||||
'is_self' => 'لا يمكنك حذف حسابك الشخصي.',
|
||||
'has_servers' => 'لا يمكن حذف مستخدم لديه خوادم نشطة مرتبطة بحسابه. يرجى حذف خوادمه قبل المتابعة.',
|
||||
'node_revocation_failed' => 'فشل إلغاء المفاتيح على <a href=":link">العقدة #:node</a>. :error',
|
||||
],
|
||||
'deployment' => [
|
||||
'no_viable_nodes' => 'لم يتم العثور على أي عقد تفي بالمتطلبات المحددة للنشر التلقائي.',
|
||||
'no_viable_allocations' => 'لم يتم العثور على أي تخصيصات تفي بمتطلبات النشر التلقائي.',
|
||||
],
|
||||
'api' => [
|
||||
'resource_not_found' => 'المورد المطلوب غير موجود على هذا الخادم.',
|
||||
],
|
||||
'mount' => [
|
||||
'servers_attached' => 'يجب ألا يكون هناك أي خوادم مرتبطة بهذا التوصيل ليتم حذفه.',
|
||||
],
|
||||
'server' => [
|
||||
'marked_as_failed' => 'لم يكتمل بعد تثبيت هذا الخادم، يرجى المحاولة مرة أخرى لاحقًا.',
|
||||
],
|
||||
];
|
19
lang/ar/pagination.php
Normal file
19
lang/ar/pagination.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« السابق',
|
||||
'next' => 'التالي »',
|
||||
|
||||
];
|
22
lang/ar/passwords.php
Normal file
22
lang/ar/passwords.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك.',
|
||||
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني.',
|
||||
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
|
||||
'token' => 'رمز إعادة تعيين كلمة المرور غير صالح.',
|
||||
'user' => 'لا يمكننا العثور على مستخدم بعنوان البريد الإلكتروني هذا.',
|
||||
|
||||
];
|
54
lang/ar/profile.php
Normal file
54
lang/ar/profile.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'الملف الشخصي',
|
||||
'tabs' => [
|
||||
'account' => 'الحساب',
|
||||
'oauth' => 'OAuth',
|
||||
'activity' => 'النشاط',
|
||||
'api_keys' => 'مفاتيح API',
|
||||
'ssh_keys' => 'مفاتيح SSH',
|
||||
'2fa' => 'المصادقة الثنائية',
|
||||
'customization' => 'التخصيص',
|
||||
],
|
||||
'username' => 'اسم المستخدم',
|
||||
'exit_admin' => 'الخروج من المسؤول',
|
||||
'email' => 'البريد الإلكتروني',
|
||||
'password' => 'كلمة المرور',
|
||||
'current_password' => 'كلمة المرور الحالية',
|
||||
'password_confirmation' => 'تأكيد كلمة المرور',
|
||||
'timezone' => 'المنطقة الزمنية',
|
||||
'language' => 'اللغة',
|
||||
'language_help' => 'لغتك :state لم تتم ترجمتها بعد!',
|
||||
'link' => 'ربط',
|
||||
'unlink' => 'إلغاء الربط',
|
||||
'unlinked' => ':name تم إلغاء ربطه',
|
||||
'scan_qr' => 'مسح رمز QR',
|
||||
'code' => 'الرمز',
|
||||
'setup_key' => 'مفتاح الإعداد',
|
||||
'invalid_code' => 'رمز المصادقة الثنائية غير صالح',
|
||||
'code_help' => 'قم بمسح رمز QR أعلاه باستخدام تطبيق المصادقة الثنائية، ثم أدخل الرمز الذي تم إنشاؤه.',
|
||||
'2fa_enabled' => 'المصادقة الثنائية مفعلة حالياً!',
|
||||
'backup_help' => 'لن يتم عرض هذه الأكواد مرة أخرى!',
|
||||
'backup_codes' => 'أكواد النسخ الاحتياطي',
|
||||
'disable_2fa' => 'تعطيل المصادقة الثنائية',
|
||||
'disable_2fa_help' => 'أدخل رمز المصادقة الثنائية الحالي لتعطيل المصادقة الثنائية',
|
||||
'keys' => 'المفاتيح',
|
||||
'create_key' => 'إنشاء مفتاح API',
|
||||
'key_created' => 'تم إنشاء المفتاح',
|
||||
'description' => 'الوصف',
|
||||
'allowed_ips' => 'عناوين IP المسموح بها',
|
||||
'allowed_ips_help' => 'اضغط على Enter لإضافة عنوان IP جديد أو اتركه فارغًا للسماح بأي عنوان IP',
|
||||
'dashboard' => 'لوحة التحكم',
|
||||
'dashboard_layout' => 'تصميم لوحة التحكم',
|
||||
'console' => 'وحدة التحكم',
|
||||
'grid' => 'شبكة',
|
||||
'table' => 'جدول',
|
||||
'rows' => 'صفوف',
|
||||
'font_size' => 'حجم الخط',
|
||||
'font' => 'نوع الخط',
|
||||
'font_preview' => 'معاينة الخط',
|
||||
'seconds' => 'ثواني',
|
||||
'graph_period' => 'فترة الرسم البياني',
|
||||
'graph_period_helper' => 'كمية نقاط البيانات و الثواني المعروضة على الرسوم البيانية',
|
||||
];
|
9
lang/ar/search.php
Normal file
9
lang/ar/search.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'validation' => 'يرجى إدخال ثلاثة أحرف على الأقل لبدء البحث.',
|
||||
'term' => [
|
||||
'label' => 'مصطلح البحث',
|
||||
'description' => 'أدخل اسم الخادم أو UUID أو التخصيص لبدء البحث.',
|
||||
],
|
||||
];
|
101
lang/ar/validation.php
Normal file
101
lang/ar/validation.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'يجب قبول :attribute.',
|
||||
'active_url' => ':attribute ليس عنوان URL صالحًا.',
|
||||
'after' => 'يجب أن يكون :attribute تاريخًا بعد :date.',
|
||||
'after_or_equal' => 'يجب أن يكون :attribute تاريخًا لاحقًا أو مساويًا لتاريخ :date.',
|
||||
'alpha' => 'يجب أن يحتوي :attribute على حروف فقط.',
|
||||
'alpha_dash' => 'يجب أن يحتوي :attribute على حروف، أرقام، وشرطات.',
|
||||
'alpha_num' => 'يجب أن يحتوي :attribute على حروف وأرقام فقط.',
|
||||
'array' => 'يجب أن يكون :attribute مصفوفة.',
|
||||
'before' => 'يجب أن يكون :attribute تاريخًا قبل :date.',
|
||||
'before_or_equal' => 'يجب أن يكون :attribute تاريخًا قبل أو يساوي :date.',
|
||||
'between' => [
|
||||
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
|
||||
'file' => 'يجب أن يكون حجم :attribute بين :min و :max كيلوبايت.',
|
||||
'string' => 'يجب أن يكون طول :attribute بين :min و :max حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على :min إلى :max عناصر.',
|
||||
],
|
||||
|
||||
'confirmed' => 'تأكيد :attribute غير متطابق.',
|
||||
'date' => ':attribute ليس تاريخًا صالحًا.',
|
||||
'date_format' => ':attribute لا يتطابق مع الشكل :format.',
|
||||
'different' => 'يجب أن يكون :attribute و :other مختلفين.',
|
||||
'digits' => 'يجب أن يكون :attribute :digits أرقام.',
|
||||
'digits_between' => 'يجب أن يكون :attribute بين :min و :max رقمًا.',
|
||||
'dimensions' => ':attribute يحتوي على أبعاد صورة غير صالحة.',
|
||||
|
||||
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالحًا.',
|
||||
|
||||
'file' => 'يجب أن يكون :attribute ملفًا.',
|
||||
'filled' => 'حقل :attribute إلزامي.',
|
||||
'image' => 'يجب أن يكون :attribute صورة.',
|
||||
|
||||
'in_array' => 'حقل :attribute غير موجود في :other.',
|
||||
'integer' => 'يجب أن يكون :attribute عددًا صحيحًا.',
|
||||
'ip' => 'يجب أن يكون :attribute عنوان IP صالحًا.',
|
||||
'json' => 'يجب أن يكون :attribute نصًا من نوع JSON صالحًا.',
|
||||
'max' => [
|
||||
'numeric' => 'قد لا يكون :attribute أكبر من :max.',
|
||||
'file' => 'قد لا يكون حجم :attribute أكبر من :max كيلوبايت.',
|
||||
'string' => 'قد لا يكون طول :attribute أكثر من :max حرفًا.',
|
||||
'array' => 'قد لا يحتوي :attribute على أكثر من :max عناصر.',
|
||||
],
|
||||
'mimes' => 'يجب أن يكون :attribute ملفًا من نوع: :values.',
|
||||
'mimetypes' => 'يجب أن يكون :attribute ملفًا من نوع: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'يجب أن يكون :attribute على الأقل :min.',
|
||||
'file' => 'يجب أن يكون حجم :attribute على الأقل :min كيلوبايت.',
|
||||
'string' => 'يجب أن يكون طول :attribute على الأقل :min حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على الأقل :min عناصر.',
|
||||
],
|
||||
|
||||
'numeric' => 'يجب أن يكون :attribute رقمًا.',
|
||||
|
||||
'regex' => 'تنسيق :attribute غير صالح.',
|
||||
|
||||
'required_with_all' => 'حقل :attribute مطلوب عند توفر كل من :values.',
|
||||
|
||||
'same' => 'يجب أن يتطابق :attribute و :other.',
|
||||
'size' => [
|
||||
'numeric' => 'يجب أن يكون :attribute :size.',
|
||||
'file' => 'يجب أن يكون حجم :attribute :size كيلوبايت.',
|
||||
'string' => 'يجب أن يكون طول :attribute :size حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على :size عناصر.',
|
||||
],
|
||||
'string' => 'يجب أن يكون :attribute نصًا.',
|
||||
'timezone' => 'يجب أن تكون :attribute منطقة زمنية صالحة.',
|
||||
|
||||
'url' => 'تنسيق :attribute غير صالح.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap attribute place-holders
|
||||
| with something more reader friendly such as E-Mail Address instead
|
||||
| of "email". This simply helps us make messages a little cleaner.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
// Internal validation logic for Panel
|
||||
'internal' => [
|
||||
'variable_value' => 'متغير :env',
|
||||
'invalid_password' => 'كلمة المرور التي تم تقديمها غير صالحة لهذا الحساب.',
|
||||
],
|
||||
];
|
122
lang/be/activity.php
Normal file
122
lang/be/activity.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Contains all of the translation strings for different activity log
|
||||
* events. These should be keyed by the value in front of the colon (:)
|
||||
* in the event name. If there is no colon present, they should live at
|
||||
* the top level.
|
||||
*/
|
||||
return [
|
||||
'auth' => [
|
||||
'fail' => 'Не атрымалася аўтарызавацца',
|
||||
'success' => 'Увайшоў',
|
||||
'password-reset' => 'Скінуць пароль',
|
||||
'checkpoint' => 'Двухфактарная аўтэнтыфікацыя ўключана',
|
||||
'recovery-token' => 'Использован резервный код 2FA',
|
||||
'token' => 'Пройдена двухфакторная проверка',
|
||||
'ip-blocked' => 'Блакаваная заявка ад неўлічанага IP-адрасу для <b>:identifier</b>',
|
||||
'sftp' => [
|
||||
'fail' => 'Не атрымалася аўтарызавацца',
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
'account' => [
|
||||
'email-changed' => 'Зменена электронная пошта з <b>:old</b> на <b>:new</b>',
|
||||
'password-changed' => 'Змяніць пароль',
|
||||
],
|
||||
'api-key' => [
|
||||
'create' => 'Створаны новы API ключ <b>:identifier</b>',
|
||||
'delete' => 'Выдалены API ключ <b>:identifier</b>',
|
||||
],
|
||||
'ssh-key' => [
|
||||
'create' => 'Дададзены SSH ключ <b>:fingerprint</b> да ўліковага запісу',
|
||||
'delete' => 'Выдалены SSH ключ <b>:fingerprint</b> з уліковага запісу',
|
||||
],
|
||||
'two-factor' => [
|
||||
'create' => 'Включена двухфакторная авторизация',
|
||||
'delete' => 'Включена двухфакторная авторизация',
|
||||
],
|
||||
],
|
||||
'server' => [
|
||||
'console' => [
|
||||
'command' => 'Выканана дзеянне <b>:command</b> на серверы',
|
||||
],
|
||||
'power' => [
|
||||
'start' => 'Сервер запушчаны',
|
||||
'stop' => 'Сервер спынены',
|
||||
'restart' => 'Сервер перазапушчаны',
|
||||
'kill' => 'Працэс сервера завершаны',
|
||||
],
|
||||
'backup' => [
|
||||
'download' => 'Спампавана рэзервовая копія <b>:name</b>',
|
||||
'delete' => 'Выдалена рэзервовая копія <b>:name</b>',
|
||||
'restore' => 'Адноўлена рэзервовая копія <b>:name</b> (выдаленыя файлы: <b>:truncate</b>)',
|
||||
'restore-complete' => 'Завершана аднаўленне рэзервовай копіі <b>:name</b>',
|
||||
'restore-failed' => 'Няўдалася завяршыць аднаўленне рэзервовай копіі <b>:name</b>',
|
||||
'start' => 'Пачата новая рэзервовая копія <b>:identifier</b>',
|
||||
'complete' => 'Рэзервовая копія <b>:name</b> адзначана як завершаная',
|
||||
'fail' => 'Рэзервовая копія <b>:name</b> адзначана як няўдалая',
|
||||
'lock' => 'Замкнута рэзервовая копія <b>:name</b>',
|
||||
'unlock' => 'Адкрылі рэзервовую копію <b>:name</b>',
|
||||
],
|
||||
'database' => [
|
||||
'create' => 'Створана новая база дадзеных <b>:name</b>',
|
||||
'rotate-password' => 'Пароль для базы даных <b>:name</b> зменены',
|
||||
'delete' => 'Выдалена база дадзеных <b>:name</b>',
|
||||
],
|
||||
'file' => [
|
||||
'compress' => 'Кампрэсаваны <b>:directory:files</b>|Кампрэсавана <b>:count</b> файлаў у <b>:directory</b>',
|
||||
'read' => 'Паглядзелі змесціва файла <b>:file</b>',
|
||||
'copy' => 'Створана копія файла <b>:file</b>',
|
||||
'create-directory' => 'Створана тэчка <b>:directory:name</b>',
|
||||
'decompress' => 'Распакоўка файла <b>:file</b> у <b>:directory</b>',
|
||||
'delete' => 'Выдалены <b>:directory:files</b>|Выдалены <b>:count</b> файлаў у <b>:directory</b>',
|
||||
'download' => 'Спампаваны файл <b>:file</b>',
|
||||
'pull' => 'Спампаваны файл з аддаленага сэрвера з <b>:url</b> у <b>:directory</b>',
|
||||
'rename' => 'Перамешчаны/ Пераназваны <b>:from</b> у <b>:to</b>|Перамешчаны/ Пераназваны <b>:count</b> файлаў у <b>:directory</b>',
|
||||
'write' => 'Запісаны новы кантэнт у файл <b>:file</b>',
|
||||
'upload' => 'Пачата загрузка файла',
|
||||
'uploaded' => 'Загружаны файл <b>:directory:file</b>',
|
||||
],
|
||||
'sftp' => [
|
||||
'denied' => 'Блакаваная магчымасць доступу SFTP з-за правоў',
|
||||
'create' => 'Створаны <b>:files</b>|Створана <b>:count</b> новых файлаў',
|
||||
'write' => 'Заменен змест у <b>:files</b>|Зменены змест <b>:count</b> файлаў',
|
||||
'delete' => 'Выдалены <b>:files</b>|Выдалены <b>:count</b> файлы',
|
||||
'create-directory' => 'Створана папка <b>:files</b>|Створана <b>:count</b> папак',
|
||||
'rename' => 'Пераназваны <b>:from</b> у <b>:to</b>|Пераназваны або перамешчаны <b>:count</b> файлы',
|
||||
],
|
||||
'allocation' => [
|
||||
'create' => 'Дададзена <b>:allocation</b> на сервер',
|
||||
'notes' => 'Абноўлены заўвагі для <b>:allocation</b> з <b>:old</b> на <b>:new</b>',
|
||||
'primary' => 'Усталявана <b>:allocation</b> як асноўная сетка для сервера',
|
||||
'delete' => 'Выдалена сетка <b>:allocation</b>',
|
||||
],
|
||||
'schedule' => [
|
||||
'create' => 'Створана задача <b>:name</b>',
|
||||
'update' => 'Абноўлена задача <b>:name</b>',
|
||||
'execute' => 'Уручную выканана задача <b>:name</b>',
|
||||
'delete' => 'Выдалена задача <b>:name</b>',
|
||||
],
|
||||
'task' => [
|
||||
'create' => 'Створана новая дзеянне "<b>:action</b>" для задачы "<b>:name</b>"',
|
||||
'update' => 'Абноўлена дзеянне "<b>:action</b>" для задачы "<b>:name</b>".',
|
||||
'delete' => 'Выдалена дзеянне "<b>:action</b>" для задачы "<b>:name</b>"',
|
||||
],
|
||||
'settings' => [
|
||||
'rename' => 'Пераназваны сервер з "<b>:old</b>" на "<b>:new</b>"',
|
||||
'description' => 'Змянёна апісанне сервера з "<b>:old</b>" на "<b>:new</b>"',
|
||||
'reinstall' => 'Сервер пераўсталяваны',
|
||||
],
|
||||
'startup' => [
|
||||
'edit' => 'Змянёна зменная "<b>:variable</b>" з "<b>:old</b>" на "<b>:new</b>"',
|
||||
'image' => 'Абноўлены Docker-вобраз для сервера з "<b>:old</b>" на "<b>:new</b>"',
|
||||
],
|
||||
'subuser' => [
|
||||
'create' => 'Дададзены "<b>:email</b>" як падкарыстальнік',
|
||||
'update' => 'Абноўлены правы падкарыстальніка для "<b>:email</b>"',
|
||||
'delete' => 'Выдалены "<b>:email</b>" як падкарыстальнік',
|
||||
],
|
||||
'crashed' => 'Сервер выйшаў з ладу',
|
||||
],
|
||||
];
|
27
lang/be/admin/apikey.php
Normal file
27
lang/be/admin/apikey.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'API ключы прыкладання',
|
||||
'empty_table' => 'Няма API ключоў',
|
||||
'whitelist' => 'Белы спіс IPv4 адрасоў',
|
||||
'whitelist_help' => 'API ключы могуць быць абмежаваны для працы з пэўнымі IPv4 адрасамі. Увядзіце кожны адрас на новым радку.',
|
||||
'whitelist_placeholder' => 'Напрыклад: 127.0.0.1 або 192.168.1.1',
|
||||
'description' => 'Апісанне',
|
||||
'description_help' => 'Кароткае апісанне гэтага ключа.',
|
||||
'nav_title' => 'API ключы',
|
||||
'model_label' => 'API ключ прыкладання',
|
||||
'model_label_plural' => 'API ключы прыкладання',
|
||||
'table' => [
|
||||
'key' => 'Ключ',
|
||||
'description' => 'Апісанне',
|
||||
'last_used' => 'Апошняе выкарыстанне',
|
||||
'created' => 'Створаны',
|
||||
'created_by' => 'Стварыў',
|
||||
'never_used' => 'Не выкарыстоўвалася',
|
||||
],
|
||||
'permissions' => [
|
||||
'none' => 'Няма',
|
||||
'read' => 'Чытаць',
|
||||
'read_write' => 'Чытаць і пісаць',
|
||||
],
|
||||
];
|
45
lang/be/admin/dashboard.php
Normal file
45
lang/be/admin/dashboard.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'heading' => 'Сардэчна запрашаем у Pelican!',
|
||||
'version' => 'Версія: :version',
|
||||
'advanced' => 'Дадатковыя',
|
||||
'server' => 'Сервер',
|
||||
'user' => 'Карыстальнік',
|
||||
'sections' => [
|
||||
'intro-developers' => [
|
||||
'heading' => 'Інфармацыя для Распрацоўшчыкаў
|
||||
',
|
||||
'content' => 'Дзякуем, што паспрабавалі распрацоўчую версію!',
|
||||
'extra_note' => 'Калі ўзнікнуць якія-небудзь праблемы, калі ласка, паведаміце пра іх на GitHub.',
|
||||
'button_issues' => 'Апавясціць аб праблеме.',
|
||||
'button_features' => 'Абмеркаваць функцыі.',
|
||||
],
|
||||
'intro-update-available' => [
|
||||
'heading' => 'Даступна абнаўленне.',
|
||||
'content' => ':latestVersion цяпер даступная! Прачытайце нашу дакументацыю, каб абнавіць вашу панэль.',
|
||||
],
|
||||
'intro-no-update' => [
|
||||
'heading' => 'Ваша панэль абноўлена да актуальнай версіі.',
|
||||
'content' => 'Цяпер вы выкарыстоўваеце :version. Ваша панэль абноўлена!',
|
||||
],
|
||||
'intro-first-node' => [
|
||||
'heading' => 'Вузлы не выяўленыя.',
|
||||
'content' => 'Здаецца, у вас яшчэ няма наладжаных вузлоў, але не хвалюйцеся — проста націсніце кнопку дзеяння, каб стварыць свой першы вузел!',
|
||||
'extra_note' => 'Калі ў вас узнікнуць якія-небудзь праблемы, калі ласка, паведаміце пра іх на GitHub.',
|
||||
'button_label' => 'Стварыце першы вузел у Pelican',
|
||||
],
|
||||
'intro-support' => [
|
||||
'heading' => 'Падтрымка Pelican',
|
||||
'content' => 'Дзякуй за выкарыстанне Pelican! Гэта стала магчымым толькі дзякуючы падтрымцы вас, нашых удзельнікаў і ўсіх астатніх, хто нас падтрымлівае!',
|
||||
'extra_note' => 'Мы ўдзячныя кожнаму за любую падтрымку.',
|
||||
'button_translate' => 'Дапамагчы з перакладам',
|
||||
'button_donate' => 'Ахвяраваць напрамую',
|
||||
],
|
||||
'intro-help' => [
|
||||
'heading' => 'Патрэбна дапамога?',
|
||||
'content' => 'Спачатку азнаёмцеся з дакументацыяй! Калі ўсё яшчэ патрэбна дапамога — заходзьце на наш Discord-сервер!',
|
||||
'button_docs' => 'Прачытаць дакументацыю',
|
||||
],
|
||||
],
|
||||
];
|
72
lang/be/admin/databasehost.php
Normal file
72
lang/be/admin/databasehost.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'Базы даных',
|
||||
'model_label' => 'База даных',
|
||||
'model_label_plural' => 'Хост базы даных',
|
||||
'table' => [
|
||||
'database' => 'База даных',
|
||||
'name' => 'Імя',
|
||||
'host' => 'Хост',
|
||||
'port' => 'Порт',
|
||||
'name_helper' => 'Калі пакінуць гэта поле пустым, імя будзе створана аўтаматычна.',
|
||||
'username' => 'Ідэнтыфікатар карыстальніка',
|
||||
'password' => 'Пароль',
|
||||
'remote' => 'Падключэнні з',
|
||||
'remote_helper' => 'Адкуль павінны дазваляцца падключэнні. Пакіньце пустым, каб дазволіць іх з любога месца.',
|
||||
'max_connections' => 'Максімальная колькасць злучэнняў',
|
||||
'created_at' => 'Створана ў',
|
||||
'connection_string' => 'Радок падключэння да JDBC',
|
||||
],
|
||||
'error' => 'Памылка пры падключэнні з хостам',
|
||||
'host' => 'Хост',
|
||||
'host_help' => 'IP-адрас або даменнае імя, якое павінна выкарыстоўвацца пры спробе падключэння з гэтым хостам MySQL з панэлі для стварэння новых баз даных.',
|
||||
'port' => 'Порт',
|
||||
'port_help' => 'Порт, на якім працуе MySQL для гэтага хоста.',
|
||||
'max_database' => 'Максімальная колькасць баз даных',
|
||||
'max_databases_help' => 'Максімальная колькасць баз дадзеных, якія можна стварыць на гэтым хосце. Калі ліміт дасягнуты, новыя базы дадзеных на гэтым хосце ствараць нельга. Пустое значэнне — без абмежавання.',
|
||||
'display_name' => 'Назва',
|
||||
'display_name_help' => 'IP-адрас або даменнае імя, якое павінна адлюстроўвацца для канчатковага карыстальніка.',
|
||||
'username' => 'Имя пользователя',
|
||||
'username_help' => 'Ідэнтыфікатар карыстальніка ўліковага запісу, які мае дастатковыя правы для стварэння новых карыстальнікаў і баз даных у сістэме.',
|
||||
'password' => 'Пароль',
|
||||
'password_help' => 'Пароль да карыстальніка базы даных.',
|
||||
'linked_nodes' => 'Звязаныя вузлы',
|
||||
'linked_nodes_help' => 'Гэта налада выкарыстоўваецца па змаўчанні толькі для гэтага хоста базы даных пры даданні базы на сервер, што знаходзіцца на выбраным вузле.',
|
||||
'connection_error' => 'Памылка пры падключэнні з хостам базы даных',
|
||||
'no_database_hosts' => 'Няма хостаў базы даных',
|
||||
'no_nodes' => 'Няма вузлоў',
|
||||
'delete_help' => 'Хост базы дадзеных ужо ўтрымлівае базы даных',
|
||||
'unlimited' => 'Без абмежавання',
|
||||
'anywhere' => 'З любога месца',
|
||||
|
||||
'rotate' => 'Павярнуць',
|
||||
'rotate_password' => 'Абнавіць пароль',
|
||||
'rotated' => 'Пароль абноўлены',
|
||||
'rotate_error' => 'Не атрымалася абнавіць пароль',
|
||||
'databases' => 'Базы данных',
|
||||
|
||||
'setup' => [
|
||||
'preparations' => 'Падрыхтоўка',
|
||||
'database_setup' => 'Устаноўка базы даных',
|
||||
'panel_setup' => 'Устаноўка панэлі',
|
||||
|
||||
'note' => 'На дадзены момант падтрымліваюцца толькі базы MySQL / MariaDB!',
|
||||
'different_server' => 'Панэль і база даных <i>не</i> знаходзяцца на адным серверы?',
|
||||
|
||||
'database_user' => 'Карыстальнік базы даных',
|
||||
'cli_login' => 'Выкарыстай <code>mysql -u root -p</code>, каб атрымаць доступ да mysql cli.',
|
||||
'command_create_user' => 'Каманда для стварэння карыстальніка',
|
||||
'command_assign_permissions' => 'Каманда для прызначэння разрашэнняў',
|
||||
'cli_exit' => 'Каб выйсці з mysql cli, увядзіце <code>exit</code>.',
|
||||
'external_access' => 'Знешні доступ',
|
||||
'allow_external_access' => '
|
||||
<p>Скорэй за ўсё, табе спатрэбіцца дазволіць знешні доступ да гэтай інстанцыі MySQL, каб серверы маглі падключацца да яе.</p>
|
||||
<br>
|
||||
<p>Для гэтага адкрый файл <code>my.cnf</code>, які можа знаходзіцца ў розных месцах у залежнасці ад аперацыйнай сістэмы і спосабу ўстаноўкі MySQL. Ты можаш увесці каманду <code>find /etc -iname my.cnf</code>, каб знайсці яго.</p>
|
||||
<br>
|
||||
<p>Адкрый <code>my.cnf</code>, дадай наступны тэкст у канец файла і запішы змены:<br> <code>[mysqld]<br>bind-address=0.0.0.0</code></p>
|
||||
<br>
|
||||
<p>Перазапусці MySQL або MariaDB, каб прымяніць змены. Гэта зменіць стандартную канфігурацыю MySQL, якая па змаўчанні прымае злучэнні толькі з localhost. Пасля абнаўлення канфігурацыі сервер зможа прымаць злучэнні на ўсіх інтэрфейсах, у тым ліку і знешнія. Абавязкова дазволь порт MySQL (па змаўчанні 3306) у брандмаўэры.</p>',
|
||||
],
|
||||
];
|
60
lang/be/admin/health.php
Normal file
60
lang/be/admin/health.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Стан',
|
||||
'results_refreshed' => 'Рэкамендацыі па праверцы стану абноўлены',
|
||||
'checked' => 'Правераныя вынікі ад :time',
|
||||
'refresh' => 'Абнавіць',
|
||||
'results' => [
|
||||
'cache' => [
|
||||
'label' => 'Кэш',
|
||||
'ok' => 'Добра',
|
||||
'failed_retrieve' => 'Няўдалося ўсталяваць або атрымаць значэнне кэша прыкладання.',
|
||||
'failed' => 'Адбылася памылка з кэшам прыкладання: :error',
|
||||
],
|
||||
'database' => [
|
||||
'label' => 'База даных',
|
||||
'ok' => 'Добра',
|
||||
'failed' => 'Няўдалося падключыцца да базы даных: :error',
|
||||
],
|
||||
'debugmode' => [
|
||||
'label' => 'Рэжым адладки',
|
||||
'ok' => 'Рэжым адладки адключаны',
|
||||
'failed' => 'Чакалася, што рэжым адладки будзе: :expected, але на самой справе быў: :actual',
|
||||
],
|
||||
'environment' => [
|
||||
'label' => 'Асяроддзе',
|
||||
'ok' => 'Добра, усталявана на :actual',
|
||||
'failed' => 'Асяроддзе ўсталявана на :actual, чакалася :expected',
|
||||
],
|
||||
'nodeversions' => [
|
||||
'label' => 'Версіі вузлоў',
|
||||
'ok' => 'Вузлы абноўлены',
|
||||
'failed' => ':outdated:/:all вузлы састарэлі',
|
||||
'no_nodes_created' => 'Вузлоў няма',
|
||||
'no_nodes' => 'Вузлоў няма',
|
||||
'all_up_to_date' => 'Усе абноўлена',
|
||||
'outdated' => ':outdated:/:all састарэла',
|
||||
],
|
||||
'panelversion' => [
|
||||
'label' => 'Версія панэлі',
|
||||
'ok' => 'Панэль абноўлена да актуальнай версіі',
|
||||
'failed' => 'Усталяваная версія: :currentVersion, але апошняя: :latestVersion',
|
||||
'up_to_date' => 'Абноўлена',
|
||||
'outdated' => 'Састарэла',
|
||||
],
|
||||
'schedule' => [
|
||||
'label' => 'Задача',
|
||||
'ok' => 'Добра',
|
||||
'failed_last_ran' => 'Апошні запуск задачы адбыўся больш за :time хвілін таму назад',
|
||||
'failed_not_ran' => 'Задача яшчэ не выканана',
|
||||
],
|
||||
'useddiskspace' => [
|
||||
'label' => 'Месца на дыску',
|
||||
],
|
||||
],
|
||||
'checks' => [
|
||||
'successful' => 'Паспяхова',
|
||||
'failed' => 'Няўдалося',
|
||||
],
|
||||
];
|
30
lang/be/admin/mount.php
Normal file
30
lang/be/admin/mount.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'Маунты',
|
||||
'model_label' => 'Маунт',
|
||||
'model_label_plural' => 'Маунты',
|
||||
'name' => 'Імя',
|
||||
'name_help' => 'Унікальнае імя, якое выкарыстоўваецца для аддзялення гэтага маунта ад іншых.',
|
||||
'source' => 'Крыніца',
|
||||
'source_help' => 'Шлях да файла на сістэме для маунта ў кантэйнер.',
|
||||
'target' => 'Месца прызначэння',
|
||||
'target_help' => 'Месца, дзе маунт будзе даступны ўнутры кантэйнера.',
|
||||
'read_only' => 'Толькі для чытання?',
|
||||
'read_only_help' => 'Ці з\'яўляецца маунт толькі для чытання ўнутры кантэйнера?',
|
||||
'description' => 'Апісанне',
|
||||
'description_help' => 'Больш падрабязнае апісанне для гэтага маунта',
|
||||
'no_mounts' => 'Няма маунтаў',
|
||||
'eggs' => 'Вобразы',
|
||||
'nodes' => 'Вузлы',
|
||||
'toggles' => [
|
||||
'writable' => 'Даступна для запісу',
|
||||
'read_only' => 'Толькі для чытання',
|
||||
],
|
||||
'table' => [
|
||||
'name' => 'Імя',
|
||||
'all_eggs' => 'Усе вобразы',
|
||||
'all_nodes' => 'Усе вузлы',
|
||||
'read_only' => 'Толькі для чытання',
|
||||
],
|
||||
];
|
18
lang/be/admin/user.php
Normal file
18
lang/be/admin/user.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'nav_title' => 'Карыстальнікі',
|
||||
'model_label' => 'Карыстальнік',
|
||||
'model_label_plural' => 'Карыстальнікі',
|
||||
'self_delete' => 'Нельга выдаліць сябе',
|
||||
'has_servers' => 'Карыстальнік мае серверы',
|
||||
'email' => 'Пошта',
|
||||
'username' => 'Имя пользователя',
|
||||
'password' => 'Пароль',
|
||||
'password_help' => 'Прапанаванне пароля карыстальніка з\'яўляецца неабавязковым. Электронная пошта новага карыстальніка падказвае стварыць пароль пры першым уводзе.',
|
||||
'admin_roles' => 'Ролі адміністратара',
|
||||
'roles' => 'Ролі',
|
||||
'no_roles' => 'Няма роляў',
|
||||
'servers' => 'Серверы',
|
||||
'subusers' => 'Падкарыстальнікі',
|
||||
];
|
57
lang/be/command/messages.php
Normal file
57
lang/be/command/messages.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'user' => [
|
||||
'search_users' => 'Увядзіце імя карыстальніка, ID карыстальніка або адрас пошты',
|
||||
'select_search_user' => 'ID карыстальніка для выдалення (увядзіце \'0\' для паўторнага пошуку)',
|
||||
'deleted' => 'Карыстальнік паспяхова выдалены з панэлі.',
|
||||
'confirm_delete' => 'Вы ўпэўнены, што хочаце выдаліць гэтага карыстальніка з панэлі?',
|
||||
'no_users_found' => 'Карыстальнікі не знойдзены па дадзеным пошукавым тэрміне.',
|
||||
'multiple_found' => 'Знойдзена некалькі ўліковых запісаў для гэтага карыстальніка, нельга выдаліць карыстальніка з-за флага --no-interaction.',
|
||||
'ask_admin' => 'Ці з\'яўляецца гэты карыстальнік адміністратарам?',
|
||||
'ask_email' => 'Адрас пошты',
|
||||
'ask_username' => 'Ідэнтыфікатар карыстальніка',
|
||||
'ask_password' => 'Пароль',
|
||||
'ask_password_tip' => 'Калі вы хочаце стварыць уліковы запіс з выпадковым паролем, які будзе адпраўлены карыстальніку па пошце, паўторна запусціце гэтую каманду (CTRL+C) і дадайце флаг `--no-password`.',
|
||||
'ask_password_help' => 'Паролі павінны складацца як мінімум з 8 сімвалаў і ўтрымліваць хаця б адну вялікую літару і лічбу.',
|
||||
'2fa_help_text' => [
|
||||
'Гэтая каманда адключыць двухфактарную аўтэнтыфікацыю для ўліковага запісу карыстальніка, калі яна ўключана. Гэта павінна выкарыстоўвацца толькі як каманда аднаўлення ўліковага запісу, калі карыстальнік заблакаваў доступ.',
|
||||
'Калі гэта не тое, што вы хацелі зрабіць, націсніце CTRL+C, каб выйсці з гэтага працэсу.',
|
||||
],
|
||||
'2fa_disabled' => 'Двухфактарная аўтэнтыфікацыя была адключана для :email.',
|
||||
],
|
||||
'schedule' => [
|
||||
'output_line' => 'Адпраўка працы для першай задачы ў `:schedule` (:id).',
|
||||
],
|
||||
'maintenance' => [
|
||||
'deleting_service_backup' => 'Выдаленне файла рэзервовай копіі сэрвісу.',
|
||||
],
|
||||
'server' => [
|
||||
'rebuild_failed' => 'Запыт на аднаўленне для ":name" (#:id) на вузле ":node" не ўдалося з памылкай: :message',
|
||||
'reinstall' => [
|
||||
'failed' => 'Запыт на пераўсталёўку для ":name" (#:id) на вузле ":node" не ўдалося з памылкай: :message',
|
||||
'confirm' => 'Вы збіраецеся пераўсталяваць групу сервераў. Ці хочаце працягнуць?',
|
||||
],
|
||||
'power' => [
|
||||
'confirm' => 'Вы збіраецеся выканаць дзеянне :action супраць :count сервераў. Ці хочаце працягнуць?',
|
||||
'action_failed' => 'Запыт на дзеянне ўключэння/выключэння для "name" (#:id) на вузле ":node" не ўдалося з памылкай: :message',
|
||||
],
|
||||
],
|
||||
'environment' => [
|
||||
'mail' => [
|
||||
'ask_smtp_host' => 'SMTP хост (напрыклад: smtp.gmail.com)',
|
||||
'ask_smtp_port' => 'SMTP порт',
|
||||
'ask_smtp_username' => 'SMTP імя карыстальніка',
|
||||
'ask_smtp_password' => 'SMTP пароль',
|
||||
'ask_mailgun_domain' => 'Дамен Mailgun',
|
||||
'ask_mailgun_endpoint' => 'Кантэйнер Mailgun',
|
||||
'ask_mailgun_secret' => 'Сакрэт Mailgun',
|
||||
'ask_mandrill_secret' => 'Сакрэт Mandrill',
|
||||
'ask_postmark_username' => 'API ключ Postmark',
|
||||
'ask_driver' => 'Які драйвер павінен выкарыстоўвацца для адпраўкі электронных лістоў?',
|
||||
'ask_mail_from' => 'Адрас электроннай пошты, з якога павінны паступаць лісты',
|
||||
'ask_mail_name' => 'Імя, з якога павінны паступаць лісты',
|
||||
'ask_encryption' => 'Метад шыфравання для выкарыстання',
|
||||
],
|
||||
],
|
||||
];
|
73
lang/be/commands.php
Normal file
73
lang/be/commands.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'appsettings' => [
|
||||
'comment' => [
|
||||
'author' => 'Увядзіце адрас электроннай пошты, з якога павінны быць адпраўлены вобразамі, экспартаваныя з гэтай панэлі. Гэта павінен быць сапраўдны адрас электроннай пошты.',
|
||||
'url' => 'URL прыкладання павінен пачынацца з https:// або http:// ў залежнасці ад таго, ці выкарыстоўваецца SSL. Калі схема не будзе ўключаная, вашыя электронныя лісты і іншы кантэнт будуць спасылацца на няправільнае месца.',
|
||||
'timezone' => 'Часавы пояс павінен супадаць з адным з падтрымліваемых часавых паясоў PHP. Калі вы не ўпэўнены, калі ласка, звярніцеся да https://php.net/manual/en/timezones.php.',
|
||||
],
|
||||
'redis' => [
|
||||
'note' => 'Вы выбралі драйвер Redis для адной або некалькіх опцый, калі ласка, прадастаўце сапраўдныя дадзеныя для падключэння ніжэй. У большасці выпадкаў вы можаце выкарыстоўваць па змоўчанні параметры, калі не змянялі вашу наладу.',
|
||||
'comment' => 'Па змоўчанні экземпляр Redis мае імя карыстальніка "default" і не мае пароля, паколькі ён працуе лакальна і недаступны знешняму свету. Калі гэта так, проста націсніце enter без уводу значэння.',
|
||||
'confirm' => 'Выглядае, што :field ужо вызначаны для Redis, хочаце змяніць?',
|
||||
],
|
||||
],
|
||||
'database_settings' => [
|
||||
'DB_HOST_note' => 'Рекомендуецца не выкарыстоўваць "localhost" як хост базы дадзеных, бо мы часта сутыкаліся з праблемамі падключэння праз сокеты. Калі вы хочаце выкарыстоўваць лакальнае падключэнне, вам трэба выкарыстоўваць "127.0.0.1".',
|
||||
'DB_USERNAME_note' => 'Выкарыстанне ўліковага запісу root для падключэнняў да MySQL не толькі настойліва не рэкамендуецца, але таксама не дазваляецца гэтым прыкладаннем. Вам трэба стварыць карыстальніка MySQL для гэтага праграмнага забеспячэння.',
|
||||
'DB_PASSWORD_note' => 'Выглядае, што ў вас ужо вызначаны пароль для падключэння MySQL, хочаце яго змяніць?',
|
||||
'DB_error_2' => 'Вашы дадзеныя для падключэння не былі захаваныя. Вам трэба ўвесці сапраўдныя дадзеныя для падключэння, перш чым працягнуць.',
|
||||
'go_back' => 'Вярнуцца і паспрабаваць зноў',
|
||||
],
|
||||
'make_node' => [
|
||||
'name' => 'Увядзіце кароткі ідэнтыфікатар, які выкарыстоўваецца для адрознення гэтага вузла ад іншых',
|
||||
'description' => 'Увядзіце апісанне для ідэнтыфікацыі вузла',
|
||||
'scheme' => 'Калі ласка, увядзіце https для SSL або http для не-ssl злучэння',
|
||||
'fqdn' => 'Увядзіце даменнае імя (напрыклад, node.example.com), якое будзе выкарыстоўвацца для падключэння да дэману. IP-адрас можа выкарыстоўвацца толькі ў тым выпадку, калі вы не выкарыстоўваеце SSL для гэтага вузла',
|
||||
'public' => 'Ці павінен гэты вузел быць публічным? Як заўвага, усталёўка вузла ў рэжым прыватнасці будзе адмаўляць магчымасць аўтаматычнага разгортвання на гэтым вузле',
|
||||
'behind_proxy' => 'Ваш FQDN знаходзіцца за проксі?',
|
||||
'maintenance_mode' => 'Ці павінен быць уключаны рэжым абслугоўвання?',
|
||||
'memory' => 'Увядзіце максімальную колькасць памяці',
|
||||
'memory_overallocate' => 'Увядзіце колькасць памяці для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
|
||||
'disk' => 'Увядзіце максімальную колькасць месца на дыску',
|
||||
'disk_overallocate' => 'Увядзіце колькасць месца на дыску для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
|
||||
'cpu' => 'Увядзіце максімальную колькасць працэсарных рэсурсаў',
|
||||
'cpu_overallocate' => 'Увядзіце колькасць працэсарных рэсурсаў для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
|
||||
'upload_size' => 'Увядзіце максімальны памер файла для загрузкі',
|
||||
'daemonListen' => 'Увядзіце порт для праслухоўвання дэману',
|
||||
'daemonSFTP' => 'Увядзіце порт праслухоўвання SFTP дэману',
|
||||
'daemonSFTPAlias' => 'Увядзіце псеўданім SFTP дэману (можа быць пустым)',
|
||||
'daemonBase' => 'Увядзіце асноўную тэчку',
|
||||
'success' => 'Паспяхова створаны новы вузел з імем :name і Id :id',
|
||||
],
|
||||
'node_config' => [
|
||||
'error_not_exist' => 'Выбраны вузел не існуе.',
|
||||
'error_invalid_format' => 'Указаны няправільны фармат. Дапушчальныя фарматы: yaml і json.',
|
||||
],
|
||||
'key_generate' => [
|
||||
'error_already_exist' => 'Выглядае, што вы ўжо наладзілі ключ шыфравання прыкладання. Працягваючы гэты працэс, вы перазапішаце гэты ключ і выклікаеце пашкоджанне дадзеных для ўжо зашыфраваных дадзеных. НЕ ПРАДАЛЖАЙЦЕ, КАЛІ НЕ ВЕДАЕЦЕ, ШТО РАБІЦЕ.',
|
||||
'understand' => 'Я разумею наступствы выканання гэтай каманды і бяру на сябе ўсю адказнасць за страту зашыфраваных дадзеных.',
|
||||
'continue' => 'Вы ўпэўнены, што хочаце працягнуць? Змена ключа шыфравання прыкладання прівядзе да страту дадзеных.',
|
||||
],
|
||||
'schedule' => [
|
||||
'process' => [
|
||||
'no_tasks' => 'Няма запланаваных задач для сервера, якія трэба выканаць.',
|
||||
'error_message' => 'Узнікла памылка пры апрацоўцы задачы: ',
|
||||
],
|
||||
],
|
||||
'upgrade' => [
|
||||
'integrity' => 'Гэтая каманда не правярае цэласнасць загружаных рэсурсаў. Пераканайцеся, што вы давяраеце крыніцы загрузкі перад працягам. Калі вы не хочаце загружаць архіў, указвайце гэта, выкарыстоўваючы флаг --skip-download, ці адказвайце "не" на наступнае пытанне.',
|
||||
'source_url' => 'Крыніца загрузкі (устанаўліваецца з --url=):',
|
||||
'php_version' => 'Немагчыма выканаць працэс саманалажэння. Мінімальная патрабаваная версія PHP — 7.4.0, у вас',
|
||||
'skipDownload' => 'Хочаце загрузіць і распакаваць архіўныя файлы для апошняй версіі?',
|
||||
'webserver_user' => 'Ваш карыстальнік вэб-сервера выяўлены як <fg=blue>[{:user}]</>. Ці правільна гэта?',
|
||||
'name_webserver' => 'Увядзіце імя карыстальніка, які выконвае працэс вэб-сервера. Гэта можа адрознівацца ў залежнасці ад сістэмы, але звычайна гэта "www-data", "nginx" або "apache".',
|
||||
'group_webserver' => 'Ваша група вэб-сервера выяўлена як <fg=blue>[{:group}]</>. Ці правільна гэта?',
|
||||
'group_webserver_question' => 'Увядзіце імя групы, якая выконвае працэс вэб-сервера. Звычайна гэта тая ж група, што і ваш карыстальнік.',
|
||||
'are_your_sure' => 'Вы ўпэўнены, што хочаце запусціць працэс абнаўлення для вашай панэлі?',
|
||||
'terminated' => 'Працэс абнаўлення быў спынены карыстальнікам.',
|
||||
'success' => 'Панэль была паспяхова абноўлена. Пераканайцеся, што вы таксама абнаўляеце ўсе экземпляры дэману.',
|
||||
|
||||
],
|
||||
];
|
19
lang/be/pagination.php
Normal file
19
lang/be/pagination.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Папярэдняя',
|
||||
'next' => 'Наступная »',
|
||||
|
||||
];
|
22
lang/be/passwords.php
Normal file
22
lang/be/passwords.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Ваш пароль быў скінуты.',
|
||||
'sent' => 'Мы адправілі вам спасылку для скіду пароля.',
|
||||
'throttled' => 'Калі ласка, пачакайце перад паўторным спробам.',
|
||||
'token' => 'Гэты токен скіду пароля недакладны.',
|
||||
'user' => 'Мы не можам знайсці карыстальніка з гэтай поштай.',
|
||||
|
||||
];
|
9
lang/be/search.php
Normal file
9
lang/be/search.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'validation' => 'Калі ласка, увядзіце хаця б тры сімвалы, каб пачаць пошук.',
|
||||
'term' => [
|
||||
'label' => 'Тэрмін для пошуку',
|
||||
'description' => 'Увядзіце назву сервера, UUID або сець, каб пачаць пошук.',
|
||||
],
|
||||
];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user