Merge pull request #880 from pelican-dev/feature/vite
Remove old client area and switch to vite
This commit is contained in:
commit
6707d1ccf6
@ -1,6 +0,0 @@
|
|||||||
public
|
|
||||||
node_modules
|
|
||||||
resources/views
|
|
||||||
babel.config.js
|
|
||||||
tailwind.config.js
|
|
||||||
webpack.config.js
|
|
52
.eslintrc.js
52
.eslintrc.js
@ -1,52 +0,0 @@
|
|||||||
/** @type {import('eslint').Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 6,
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
project: './tsconfig.json',
|
|
||||||
tsconfigRootDir: './',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
pragma: 'React',
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
linkComponents: [
|
|
||||||
{ name: 'Link', linkAttribute: 'to' },
|
|
||||||
{ name: 'NavLink', linkAttribute: 'to' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'],
|
|
||||||
extends: [
|
|
||||||
// 'standard',
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:jest-dom/recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
eqeqeq: 'error',
|
|
||||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
|
||||||
// TypeScript can infer this significantly better than eslint ever can.
|
|
||||||
'react/prop-types': 0,
|
|
||||||
'react/display-name': 0,
|
|
||||||
'@typescript-eslint/no-explicit-any': 0,
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 0,
|
|
||||||
// 'react/no-unknown-property': ['error', { ignore: ['css'] }],
|
|
||||||
// This setup is required to avoid a spam of errors when running eslint about React being
|
|
||||||
// used before it is defined.
|
|
||||||
//
|
|
||||||
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
|
|
||||||
'no-use-before-define': 0,
|
|
||||||
'@typescript-eslint/no-use-before-define': 'warn',
|
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
|
||||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
|
|
||||||
},
|
|
||||||
};
|
|
19
.github/workflows/build.yaml
vendored
19
.github/workflows/build.yaml
vendored
@ -3,10 +3,10 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ui:
|
ui:
|
||||||
@ -20,14 +20,25 @@ jobs:
|
|||||||
- name: Code Checkout
|
- name: Code Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install JS dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build:production
|
run: yarn build
|
||||||
|
17
.github/workflows/release.yaml
vendored
17
.github/workflows/release.yaml
vendored
@ -11,22 +11,33 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Code checkout
|
- name: Code checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install JS dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build:production
|
run: yarn build
|
||||||
|
|
||||||
- name: Create release branch and bump version
|
- name: Create release branch and bump version
|
||||||
env:
|
env:
|
||||||
|
@ -244,7 +244,7 @@ class Handler extends ExceptionHandler
|
|||||||
return new JsonResponse($this->convertExceptionToArray($exception), JsonResponse::HTTP_UNAUTHORIZED);
|
return new JsonResponse($this->convertExceptionToArray($exception), JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->guest('/auth/login');
|
return redirect()->guest(route('filament.app.auth.login'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Auth\AuthManager;
|
use Illuminate\Auth\AuthManager;
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ class RedirectIfAuthenticated
|
|||||||
public function handle(Request $request, \Closure $next, ?string $guard = null): mixed
|
public function handle(Request $request, \Closure $next, ?string $guard = null): mixed
|
||||||
{
|
{
|
||||||
if ($this->authManager->guard($guard)->check()) {
|
if ($this->authManager->guard($guard)->check()) {
|
||||||
return redirect()->route('index');
|
return redirect(ListServers::getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\ViewComposers;
|
|
||||||
|
|
||||||
use App\Services\Helpers\AssetHashService;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
readonly class AssetComposer
|
|
||||||
{
|
|
||||||
public function __construct(private AssetHashService $assetHashService) {}
|
|
||||||
|
|
||||||
public function compose(View $view): void
|
|
||||||
{
|
|
||||||
$view->with('asset', $this->assetHashService);
|
|
||||||
|
|
||||||
$view->with('siteConfiguration', [
|
|
||||||
'name' => config('app.name', 'Panel'),
|
|
||||||
'locale' => config('app.locale') ?? 'en',
|
|
||||||
'recaptcha' => [
|
|
||||||
'enabled' => config('turnstile.turnstile_enabled', false),
|
|
||||||
'siteKey' => config('turnstile.turnstile_site_key') ?? '',
|
|
||||||
],
|
|
||||||
'usesSyncDriver' => config('queue.default') === 'sync',
|
|
||||||
'serverDescriptionsEditable' => config('panel.editable_server_descriptions'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -201,7 +201,7 @@ class Node extends Model
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
|
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
|
||||||
'remote' => route('index'),
|
'remote' => route('filament.app.resources...index'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Notifications;
|
namespace App\Notifications;
|
||||||
|
|
||||||
use App\Events\Server\SubUserRemoved;
|
use App\Events\Server\SubUserRemoved;
|
||||||
|
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
@ -52,6 +53,6 @@ class RemovedFromServer extends Notification implements ShouldQueue
|
|||||||
->greeting('Hello ' . $this->user->username . '.')
|
->greeting('Hello ' . $this->user->username . '.')
|
||||||
->line('You have been removed as a subuser for the following server.')
|
->line('You have been removed as a subuser for the following server.')
|
||||||
->line('Server Name: ' . $this->server->name)
|
->line('Server Name: ' . $this->server->name)
|
||||||
->action('Visit Panel', route('index'));
|
->action('Visit Panel', ListServers::getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use Illuminate\Bus\Queueable;
|
|||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use App\Events\Server\Installed;
|
use App\Events\Server\Installed;
|
||||||
|
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Contracts\Notifications\Dispatcher;
|
use Illuminate\Contracts\Notifications\Dispatcher;
|
||||||
@ -63,6 +64,6 @@ class ServerInstalled extends Notification implements ShouldQueue
|
|||||||
->greeting('Hello ' . $this->user->username . '.')
|
->greeting('Hello ' . $this->user->username . '.')
|
||||||
->line('Your server has finished installing and is now ready for you to use.')
|
->line('Your server has finished installing and is now ready for you to use.')
|
||||||
->line('Server Name: ' . $this->server->name)
|
->line('Server Name: ' . $this->server->name)
|
||||||
->action('Login and Begin Using', route('index'));
|
->action('Login and Begin Using', ListServers::getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ use Filament\View\PanelsRenderHook;
|
|||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Console\AboutCommand;
|
use Illuminate\Foundation\Console\AboutCommand;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@ -106,12 +107,28 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
'warning' => Color::Amber,
|
'warning' => Color::Amber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
FilamentView::registerRenderHook(
|
||||||
|
PanelsRenderHook::HEAD_START,
|
||||||
|
fn (): string => Blade::render(<<<'HTML'
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@livewireStyles
|
||||||
|
HTML),
|
||||||
|
);
|
||||||
|
|
||||||
FilamentView::registerRenderHook(
|
FilamentView::registerRenderHook(
|
||||||
PanelsRenderHook::CONTENT_START,
|
PanelsRenderHook::CONTENT_START,
|
||||||
fn () => view('filament.server-conflict-banner'),
|
fn () => view('filament.server-conflict-banner'),
|
||||||
scopes: Console::class,
|
scopes: Console::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
FilamentView::registerRenderHook(
|
||||||
|
PanelsRenderHook::BODY_END,
|
||||||
|
fn (): string => Blade::render(<<<'HTML'
|
||||||
|
@livewireScripts
|
||||||
|
@vite(['resources/js/app.js'])
|
||||||
|
HTML),
|
||||||
|
);
|
||||||
|
|
||||||
// Don't run any health checks during tests
|
// Don't run any health checks during tests
|
||||||
if (!$app->runningUnitTests()) {
|
if (!$app->runningUnitTests()) {
|
||||||
Health::checks([
|
Health::checks([
|
||||||
|
@ -25,7 +25,6 @@ class AppPanelProvider extends PanelProvider
|
|||||||
{
|
{
|
||||||
return $panel
|
return $panel
|
||||||
->id('app')
|
->id('app')
|
||||||
->path('app')
|
|
||||||
->spa()
|
->spa()
|
||||||
->breadcrumbs(false)
|
->breadcrumbs(false)
|
||||||
->brandName(config('app.name', 'Pelican'))
|
->brandName(config('app.name', 'Pelican'))
|
||||||
|
@ -31,7 +31,7 @@ class ServerPanelProvider extends PanelProvider
|
|||||||
return $panel
|
return $panel
|
||||||
->id('server')
|
->id('server')
|
||||||
->path('app/server')
|
->path('app/server')
|
||||||
->homeUrl('/app')
|
->homeUrl('/')
|
||||||
->spa()
|
->spa()
|
||||||
->tenant(Server::class)
|
->tenant(Server::class)
|
||||||
->brandName(config('app.name', 'Pelican'))
|
->brandName(config('app.name', 'Pelican'))
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
use App\Http\ViewComposers\AssetComposer;
|
|
||||||
|
|
||||||
class ViewComposerServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Register bindings in the container.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
$this->app->make('view')->composer('*', AssetComposer::class);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Helpers;
|
|
||||||
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Filesystem\FilesystemManager;
|
|
||||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
|
||||||
use App\Exceptions\ManifestDoesNotExistException;
|
|
||||||
|
|
||||||
class AssetHashService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Location of the manifest file generated by gulp.
|
|
||||||
*/
|
|
||||||
public const MANIFEST_PATH = './assets/manifest.json';
|
|
||||||
|
|
||||||
private Filesystem $filesystem;
|
|
||||||
|
|
||||||
protected static mixed $manifest = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AssetHashService constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(FilesystemManager $filesystem)
|
|
||||||
{
|
|
||||||
$this->filesystem = $filesystem->createLocalDriver(['root' => public_path()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify a URL to append the asset hash.
|
|
||||||
*/
|
|
||||||
public function url(string $resource): string
|
|
||||||
{
|
|
||||||
$file = last(explode('/', $resource));
|
|
||||||
$data = Arr::get($this->manifest(), $file) ?? $file;
|
|
||||||
|
|
||||||
return str_replace($file, Arr::get($data, 'src') ?? $file, $resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the data integrity hash for a resource.
|
|
||||||
*/
|
|
||||||
public function integrity(string $resource): string
|
|
||||||
{
|
|
||||||
$file = last(explode('/', $resource));
|
|
||||||
$data = array_get($this->manifest(), $file, $file);
|
|
||||||
|
|
||||||
return Arr::get($data, 'integrity') ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a built CSS import using the provided URL.
|
|
||||||
*/
|
|
||||||
public function css(string $resource): string
|
|
||||||
{
|
|
||||||
$attributes = [
|
|
||||||
'href' => $this->url($resource),
|
|
||||||
'rel' => 'stylesheet preload',
|
|
||||||
'as' => 'style',
|
|
||||||
'crossorigin' => 'anonymous',
|
|
||||||
'referrerpolicy' => 'no-referrer',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (config('panel.assets.use_hash')) {
|
|
||||||
$attributes['integrity'] = $this->integrity($resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = '<link';
|
|
||||||
foreach ($attributes as $key => $value) {
|
|
||||||
$output .= " $key=\"$value\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output . '>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a built JS import using the provided URL.
|
|
||||||
*/
|
|
||||||
public function js(string $resource): string
|
|
||||||
{
|
|
||||||
$attributes = [
|
|
||||||
'src' => $this->url($resource),
|
|
||||||
'crossorigin' => 'anonymous',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (config('panel.assets.use_hash')) {
|
|
||||||
$attributes['integrity'] = $this->integrity($resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = '<script';
|
|
||||||
foreach ($attributes as $key => $value) {
|
|
||||||
$output .= " $key=\"$value\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output . '></script>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the asset manifest and store it in the cache for quicker lookups.
|
|
||||||
*/
|
|
||||||
protected function manifest(): array
|
|
||||||
{
|
|
||||||
if (static::$manifest === null) {
|
|
||||||
self::$manifest = json_decode(
|
|
||||||
$this->filesystem->get(self::MANIFEST_PATH),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$manifest = static::$manifest;
|
|
||||||
if ($manifest === null) {
|
|
||||||
throw new ManifestDoesNotExistException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $manifest;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
module.exports = function (api) {
|
|
||||||
let targets = {};
|
|
||||||
const plugins = [
|
|
||||||
'babel-plugin-macros',
|
|
||||||
'styled-components',
|
|
||||||
'react-hot-loader/babel',
|
|
||||||
'@babel/transform-runtime',
|
|
||||||
'@babel/transform-react-jsx',
|
|
||||||
'@babel/proposal-class-properties',
|
|
||||||
'@babel/proposal-object-rest-spread',
|
|
||||||
'@babel/proposal-optional-chaining',
|
|
||||||
'@babel/proposal-nullish-coalescing-operator',
|
|
||||||
'@babel/syntax-dynamic-import',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (api.env('test')) {
|
|
||||||
targets = { node: 'current' };
|
|
||||||
plugins.push('@babel/transform-modules-commonjs');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins,
|
|
||||||
presets: [
|
|
||||||
'@babel/typescript',
|
|
||||||
['@babel/env', {
|
|
||||||
modules: false,
|
|
||||||
useBuiltIns: 'entry',
|
|
||||||
corejs: 3,
|
|
||||||
targets,
|
|
||||||
}],
|
|
||||||
'@babel/react',
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
@ -10,7 +10,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
$middleware->redirectGuestsTo(fn () => route('auth.login'));
|
$middleware->redirectGuestsTo(fn () => route('filament.app.auth.login'));
|
||||||
|
|
||||||
$middleware->web(\App\Http\Middleware\LanguageMiddleware::class);
|
$middleware->web(\App\Http\Middleware\LanguageMiddleware::class);
|
||||||
|
|
||||||
|
@ -9,6 +9,5 @@ return [
|
|||||||
App\Providers\Filament\AppPanelProvider::class,
|
App\Providers\Filament\AppPanelProvider::class,
|
||||||
App\Providers\Filament\ServerPanelProvider::class,
|
App\Providers\Filament\ServerPanelProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
App\Providers\ViewComposerServiceProvider::class,
|
|
||||||
SocialiteProviders\Manager\ServiceProvider::class,
|
SocialiteProviders\Manager\ServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Pelican'),
|
'name' => env('APP_NAME', 'Pelican'),
|
||||||
|
'logo' => env('APP_LOGO', '/pelican.svg'),
|
||||||
'favicon' => env('APP_FAVICON', '/pelican.ico'),
|
'favicon' => env('APP_FAVICON', '/pelican.ico'),
|
||||||
|
|
||||||
'version' => 'canary',
|
'version' => 'canary',
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
|
||||||
const { compilerOptions } = require('./tsconfig');
|
|
||||||
|
|
||||||
/** @type {import('ts-jest').InitialOptionsTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
globals: {
|
|
||||||
'ts-jest': {
|
|
||||||
isolatedModules: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['js', 'ts', 'tsx', 'd.ts', 'json', 'node'],
|
|
||||||
moduleNameMapper: {
|
|
||||||
'\\.(jpe?g|png|gif|svg)$': '<rootDir>/resources/scripts/__mocks__/file.ts',
|
|
||||||
'\\.(s?css|less)$': 'identity-obj-proxy',
|
|
||||||
...pathsToModuleNameMapper(compilerOptions.paths, {
|
|
||||||
prefix: '<rootDir>/',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
setupFilesAfterEnv: [
|
|
||||||
'<rootDir>/resources/scripts/setup-tests.ts',
|
|
||||||
],
|
|
||||||
transform: {
|
|
||||||
'.*\\.[t|j]sx$': 'babel-jest',
|
|
||||||
'.*\\.ts$': 'ts-jest',
|
|
||||||
},
|
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
|
||||||
};
|
|
169
package.json
169
package.json
@ -1,158 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "panel",
|
"private": true,
|
||||||
"engines": {
|
"type": "module",
|
||||||
"node": ">=18"
|
"scripts": {
|
||||||
},
|
"build": "vite build",
|
||||||
"dependencies": {
|
"dev": "vite"
|
||||||
"@floating-ui/react-dom-interactions": "^0.6.6",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
|
||||||
"@headlessui/react": "^1.6.4",
|
|
||||||
"@heroicons/react": "^1.0.6",
|
|
||||||
"@hot-loader/react-dom": "^16.14.0",
|
|
||||||
"@preact/signals-react": "^1.2.1",
|
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
|
||||||
"@tailwindcss/line-clamp": "^0.4.0",
|
|
||||||
"axios": "^1.6.7",
|
|
||||||
"boring-avatars": "^1.7.0",
|
|
||||||
"chart.js": "^3.8.0",
|
|
||||||
"classnames": "^2.3.1",
|
|
||||||
"codemirror": "^5.58.2",
|
|
||||||
"copy-to-clipboard": "^3.3.1",
|
|
||||||
"date-fns": "^2.28.0",
|
|
||||||
"debounce": "^1.2.0",
|
|
||||||
"deepmerge-ts": "^4.2.1",
|
|
||||||
"easy-peasy": "^4.0.1",
|
|
||||||
"events": "^3.0.0",
|
|
||||||
"formik": "^2.2.6",
|
|
||||||
"framer-motion": "^6.3.10",
|
|
||||||
"i18next": "^21.8.9",
|
|
||||||
"i18next-http-backend": "^1.4.1",
|
|
||||||
"i18next-multiload-backend-adapter": "^1.0.0",
|
|
||||||
"qrcode.react": "^1.0.1",
|
|
||||||
"react": "^16.14.0",
|
|
||||||
"react-chartjs-2": "^4.2.0",
|
|
||||||
"react-dom": "npm:@hot-loader/react-dom",
|
|
||||||
"react-fast-compare": "^3.2.0",
|
|
||||||
"react-hot-loader": "^4.12.21",
|
|
||||||
"react-i18next": "^11.2.1",
|
|
||||||
"react-router-dom": "^5.1.2",
|
|
||||||
"react-transition-group": "^4.4.1",
|
|
||||||
"react-turnstile": "^1.1.4",
|
|
||||||
"rimraf": "^4",
|
|
||||||
"sockette": "^2.0.6",
|
|
||||||
"styled-components": "^5.2.1",
|
|
||||||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
|
||||||
"swr": "^0.2.3",
|
|
||||||
"tailwindcss": "^3.0.24",
|
|
||||||
"use-fit-text": "^2.4.0",
|
|
||||||
"uuid": "^8.3.2",
|
|
||||||
"xterm": "^4.19.0",
|
|
||||||
"xterm-addon-fit": "^0.5.0",
|
|
||||||
"xterm-addon-search": "^0.9.0",
|
|
||||||
"xterm-addon-search-bar": "^0.2.0",
|
|
||||||
"xterm-addon-web-links": "^0.6.0",
|
|
||||||
"yup": "^0.29.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.1",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
"autoprefixer": "^10.4.20",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
"axios": "^1.7.4",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
"concurrently": "^9.0.1",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"laravel-vite-plugin": "^1.0",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.18.2",
|
"postcss": "^8.4.47",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.12.1",
|
"postcss-nesting": "^13.0.1",
|
||||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
"prettier": "^3.4.2",
|
||||||
"@babel/preset-env": "^7.12.1",
|
"tailwindcss": "^3.4.13",
|
||||||
"@babel/preset-react": "^7.12.1",
|
"vite": "^6.0"
|
||||||
"@babel/preset-typescript": "^7.12.1",
|
|
||||||
"@babel/runtime": "^7.12.1",
|
|
||||||
"@testing-library/dom": "^8.14.0",
|
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
|
||||||
"@testing-library/react": "12.1.5",
|
|
||||||
"@testing-library/user-event": "^14.2.1",
|
|
||||||
"@types/codemirror": "^0.0.98",
|
|
||||||
"@types/debounce": "^1.2.0",
|
|
||||||
"@types/events": "^3.0.0",
|
|
||||||
"@types/jest": "^28.1.3",
|
|
||||||
"@types/node": "^14.11.10",
|
|
||||||
"@types/qrcode.react": "^1.0.1",
|
|
||||||
"@types/react": "^16.14.0",
|
|
||||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
|
||||||
"@types/react-dom": "^16.9.16",
|
|
||||||
"@types/react-redux": "^7.1.1",
|
|
||||||
"@types/react-router": "^5.1.3",
|
|
||||||
"@types/react-router-dom": "^5.1.3",
|
|
||||||
"@types/react-transition-group": "^4.4.0",
|
|
||||||
"@types/styled-components": "^5.1.7",
|
|
||||||
"@types/uuid": "^3.4.5",
|
|
||||||
"@types/webpack-env": "^1.15.2",
|
|
||||||
"@types/yup": "^0.29.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
|
||||||
"@typescript-eslint/parser": "^5.29.0",
|
|
||||||
"autoprefixer": "^10.4.7",
|
|
||||||
"babel-jest": "^28.1.1",
|
|
||||||
"babel-loader": "^8.2.5",
|
|
||||||
"babel-plugin-styled-components": "^2.0.7",
|
|
||||||
"cross-env": "^7.0.2",
|
|
||||||
"css-loader": "^5.2.7",
|
|
||||||
"eslint": "^8.18.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-jest-dom": "^4.0.2",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"eslint-plugin-react": "^7.30.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"fork-ts-checker-webpack-plugin": "^6.2.10",
|
|
||||||
"identity-obj-proxy": "^3.0.0",
|
|
||||||
"jest": "^28.1.1",
|
|
||||||
"postcss": "^8.4.35",
|
|
||||||
"postcss-import": "^14.1.0",
|
|
||||||
"postcss-loader": "^4.0.0",
|
|
||||||
"postcss-nesting": "^10.1.8",
|
|
||||||
"postcss-preset-env": "^7.7.1",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"redux-devtools-extension": "^2.13.8",
|
|
||||||
"source-map-loader": "^1.1.3",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"svg-url-loader": "^7.1.1",
|
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
|
||||||
"ts-essentials": "^9.1.2",
|
|
||||||
"ts-jest": "^28.0.5",
|
|
||||||
"twin.macro": "^2.8.2",
|
|
||||||
"typescript": "^4.7.3",
|
|
||||||
"webpack": "^4.47.0",
|
|
||||||
"webpack-assets-manifest": "^3.1.1",
|
|
||||||
"webpack-bundle-analyzer": "^3.8.0",
|
|
||||||
"webpack-cli": "^3.3.12",
|
|
||||||
"webpack-dev-server": "^3.11.0",
|
|
||||||
"yarn-deduplicate": "^1.1.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"clean": "cd public/assets && rimraf -g *.js *.map",
|
|
||||||
"test": "jest",
|
|
||||||
"lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx",
|
|
||||||
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
|
|
||||||
"build": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --progress",
|
|
||||||
"build:production": "yarn run clean && cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode production",
|
|
||||||
"serve": "yarn run clean && cross-env WEBPACK_PUBLIC_PATH=/webpack@hmr/ NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8080 --public https://panel.test --hot"
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"> 0.5%",
|
|
||||||
"last 2 versions",
|
|
||||||
"firefox esr",
|
|
||||||
"not dead"
|
|
||||||
],
|
|
||||||
"babelMacros": {
|
|
||||||
"twin": {
|
|
||||||
"preset": "styled-components"
|
|
||||||
},
|
|
||||||
"styledComponents": {
|
|
||||||
"pure": true,
|
|
||||||
"displayName": true,
|
|
||||||
"fileName": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,7 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
plugins: [
|
plugins: {
|
||||||
require('postcss-import'),
|
'tailwindcss/nesting': 'postcss-nesting',
|
||||||
// We want to make use of nesting following the CSS Nesting spec, and not the
|
tailwindcss: {},
|
||||||
// SASS style nesting.
|
autoprefixer: {},
|
||||||
//
|
},
|
||||||
// @see https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
|
|
||||||
require('tailwindcss/nesting')(require('postcss-nesting')),
|
|
||||||
require('tailwindcss'),
|
|
||||||
require('autoprefixer'),
|
|
||||||
require('postcss-preset-env')({
|
|
||||||
features: {
|
|
||||||
'nesting-rules': false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.8 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 23 KiB |
@ -1,3 +1,4 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@tailwind variants;
|
1
resources/js/app.js
Normal file
1
resources/js/app.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import './bootstrap';
|
4
resources/js/bootstrap.js
vendored
Normal file
4
resources/js/bootstrap.js
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
@ -1,30 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Route } from 'react-router';
|
|
||||||
import { SwitchTransition } from 'react-transition-group';
|
|
||||||
import Fade from '@/components/elements/Fade';
|
|
||||||
import styled from 'styled-components/macro';
|
|
||||||
import tw from 'twin.macro';
|
|
||||||
|
|
||||||
const StyledSwitchTransition = styled(SwitchTransition)`
|
|
||||||
${tw`relative`};
|
|
||||||
|
|
||||||
& section {
|
|
||||||
${tw`absolute w-full top-0 left-0`};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TransitionRouter: React.FC = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
render={({ location }) => (
|
|
||||||
<StyledSwitchTransition>
|
|
||||||
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
|
|
||||||
<section>{children}</section>
|
|
||||||
</Fade>
|
|
||||||
</StyledSwitchTransition>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransitionRouter;
|
|
@ -1 +0,0 @@
|
|||||||
module.exports = 'test-file-stub';
|
|
@ -1,33 +0,0 @@
|
|||||||
import useSWR, { ConfigInterface, responseInterface } from 'swr';
|
|
||||||
import { ActivityLog, Transformers } from '@definitions/user';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
|
||||||
import { toPaginatedSet } from '@definitions/helpers';
|
|
||||||
import useFilteredObject from '@/plugins/useFilteredObject';
|
|
||||||
import { useUserSWRKey } from '@/plugins/useSWRKey';
|
|
||||||
|
|
||||||
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
|
||||||
|
|
||||||
const useActivityLogs = (
|
|
||||||
filters?: ActivityLogFilters,
|
|
||||||
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
|
|
||||||
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
|
||||||
const key = useUserSWRKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]);
|
|
||||||
|
|
||||||
return useSWR<PaginatedResult<ActivityLog>>(
|
|
||||||
key,
|
|
||||||
async () => {
|
|
||||||
const { data } = await http.get('/api/client/account/activity', {
|
|
||||||
params: {
|
|
||||||
...withQueryBuilderParams(filters),
|
|
||||||
include: ['actor'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return toPaginatedSet(data, Transformers.toActivityLog);
|
|
||||||
},
|
|
||||||
{ revalidateOnMount: false, ...(config || {}) }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useActivityLogs };
|
|
@ -1,19 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { ApiKey, rawDataToApiKey } from '@/api/account/getApiKeys';
|
|
||||||
|
|
||||||
export default (description: string, allowedIps: string): Promise<ApiKey & { secretToken: string }> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post('/api/client/account/api-keys', {
|
|
||||||
description,
|
|
||||||
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
|
||||||
})
|
|
||||||
.then(({ data }) =>
|
|
||||||
resolve({
|
|
||||||
...rawDataToApiKey(data.attributes),
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
secretToken: data.meta?.secret_token ?? '',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (identifier: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/account/api-keys/${identifier}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (password: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete('/api/client/account/two-factor', { params: { password } })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (code: string, password: string): Promise<string[]> => {
|
|
||||||
const { data } = await http.post('/api/client/account/two-factor', { code, password });
|
|
||||||
|
|
||||||
return data.attributes.tokens;
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export interface ApiKey {
|
|
||||||
identifier: string;
|
|
||||||
description: string;
|
|
||||||
allowedIps: string[];
|
|
||||||
createdAt: Date | null;
|
|
||||||
lastUsedAt: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawDataToApiKey = (data: any): ApiKey => ({
|
|
||||||
identifier: data.identifier,
|
|
||||||
description: data.description,
|
|
||||||
allowedIps: data.allowed_ips,
|
|
||||||
createdAt: data.created_at ? new Date(data.created_at) : null,
|
|
||||||
lastUsedAt: data.last_used_at ? new Date(data.last_used_at) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (): Promise<ApiKey[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get('/api/client/account/api-keys')
|
|
||||||
.then(({ data }) => resolve((data.data || []).map((d: any) => rawDataToApiKey(d.attributes))))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,38 +0,0 @@
|
|||||||
/* The MIT License (MIT)
|
|
||||||
|
|
||||||
Pterodactyl®
|
|
||||||
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE. */
|
|
||||||
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export interface TwoFactorTokenData {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
image_url_data: string;
|
|
||||||
secret: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (): Promise<TwoFactorTokenData> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get('/api/client/account/two-factor')
|
|
||||||
.then(({ data }) => resolve(data.data))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
import useSWR, { ConfigInterface } from 'swr';
|
|
||||||
import http, { FractalResponseList } from '@/api/http';
|
|
||||||
import { SSHKey, Transformers } from '@definitions/user';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { useUserSWRKey } from '@/plugins/useSWRKey';
|
|
||||||
|
|
||||||
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
|
|
||||||
const key = useUserSWRKey(['account', 'ssh-keys']);
|
|
||||||
|
|
||||||
return useSWR(
|
|
||||||
key,
|
|
||||||
async () => {
|
|
||||||
const { data } = await http.get('/api/client/account/ssh-keys');
|
|
||||||
|
|
||||||
return (data as FractalResponseList).data.map((datum: any) => {
|
|
||||||
return Transformers.toSSHKey(datum.attributes);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ revalidateOnMount: false, ...(config || {}) }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSSHKey = async (name: string, publicKey: string): Promise<SSHKey> => {
|
|
||||||
const { data } = await http.post('/api/client/account/ssh-keys', { name, public_key: publicKey });
|
|
||||||
|
|
||||||
return Transformers.toSSHKey(data.attributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSSHKey = async (fingerprint: string): Promise<void> =>
|
|
||||||
await http.post('/api/client/account/ssh-keys/remove', { fingerprint });
|
|
||||||
|
|
||||||
export { useSSHKeys, createSSHKey, deleteSSHKey };
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (email: string, password: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.put('/api/client/account/email', { email, password })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
current: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.put('/api/client/account/password', {
|
|
||||||
current_password: current,
|
|
||||||
password: password,
|
|
||||||
password_confirmation: confirmPassword,
|
|
||||||
})
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,38 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
complete: boolean;
|
|
||||||
intended?: string;
|
|
||||||
confirmationToken?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginData {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
recaptchaData?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get('/sanctum/csrf-cookie')
|
|
||||||
.then(() =>
|
|
||||||
http.post('/auth/login', {
|
|
||||||
user: username,
|
|
||||||
password,
|
|
||||||
'cf-turnstile-response': recaptchaData,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (!(response.data instanceof Object)) {
|
|
||||||
return reject(new Error('An error occurred while processing the login request.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve({
|
|
||||||
complete: response.data.data.complete,
|
|
||||||
intended: response.data.data.intended || undefined,
|
|
||||||
confirmationToken: response.data.data.confirmation_token || undefined,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { LoginResponse } from '@/api/auth/login';
|
|
||||||
|
|
||||||
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post('/auth/login/checkpoint', {
|
|
||||||
confirmation_token: token,
|
|
||||||
authentication_code: code,
|
|
||||||
recovery_token: recoveryToken && recoveryToken.length > 0 ? recoveryToken : undefined,
|
|
||||||
})
|
|
||||||
.then((response) =>
|
|
||||||
resolve({
|
|
||||||
complete: response.data.data.complete,
|
|
||||||
intended: response.data.data.intended || undefined,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
token: string;
|
|
||||||
password: string;
|
|
||||||
passwordConfirmation: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PasswordResetResponse {
|
|
||||||
redirectTo?: string | null;
|
|
||||||
sendToLogin: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (email: string, data: Data): Promise<PasswordResetResponse> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post('/auth/password/reset', {
|
|
||||||
email,
|
|
||||||
token: data.token,
|
|
||||||
password: data.password,
|
|
||||||
password_confirmation: data.passwordConfirmation,
|
|
||||||
})
|
|
||||||
.then((response) =>
|
|
||||||
resolve({
|
|
||||||
redirectTo: response.data.redirect_to,
|
|
||||||
sendToLogin: response.data.send_to_login,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (email: string, recaptchaData?: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post('/auth/password', { email, 'cf-turnstile-response': recaptchaData })
|
|
||||||
.then((response) => resolve(response.data.status || ''))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,55 +0,0 @@
|
|||||||
import {
|
|
||||||
FractalPaginatedResponse,
|
|
||||||
FractalResponseData,
|
|
||||||
FractalResponseList,
|
|
||||||
getPaginationSet,
|
|
||||||
PaginatedResult,
|
|
||||||
} from '@/api/http';
|
|
||||||
import { Model } from '@definitions/index';
|
|
||||||
|
|
||||||
type TransformerFunc<T> = (callback: FractalResponseData) => T;
|
|
||||||
|
|
||||||
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
|
||||||
|
|
||||||
function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>, missing?: M): M;
|
|
||||||
function transform<T, M>(
|
|
||||||
data: FractalResponseData | null | undefined,
|
|
||||||
transformer: TransformerFunc<T>,
|
|
||||||
missing?: M
|
|
||||||
): T | M;
|
|
||||||
function transform<T, M>(
|
|
||||||
data: FractalResponseList | FractalPaginatedResponse | null | undefined,
|
|
||||||
transformer: TransformerFunc<T>,
|
|
||||||
missing?: M
|
|
||||||
): T[] | M;
|
|
||||||
function transform<T>(
|
|
||||||
data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined,
|
|
||||||
transformer: TransformerFunc<T>,
|
|
||||||
missing = undefined
|
|
||||||
) {
|
|
||||||
if (data === undefined || data === null) {
|
|
||||||
return missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isList(data)) {
|
|
||||||
return data.data.map(transformer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || !data.attributes || data.object === 'null_resource') {
|
|
||||||
return missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformer(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPaginatedSet<T extends TransformerFunc<Model>>(
|
|
||||||
response: FractalPaginatedResponse,
|
|
||||||
transformer: T
|
|
||||||
): PaginatedResult<ReturnType<T>> {
|
|
||||||
return {
|
|
||||||
items: transform(response, transformer) as ReturnType<T>[],
|
|
||||||
pagination: getPaginationSet(response.meta.pagination),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { transform, toPaginatedSet };
|
|
33
resources/scripts/api/definitions/index.d.ts
vendored
33
resources/scripts/api/definitions/index.d.ts
vendored
@ -1,33 +0,0 @@
|
|||||||
import { MarkRequired } from 'ts-essentials';
|
|
||||||
import { FractalResponseData, FractalResponseList } from '../http';
|
|
||||||
|
|
||||||
export type UUID = string;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
export interface Model {}
|
|
||||||
|
|
||||||
interface ModelWithRelationships extends Model {
|
|
||||||
relationships: Record<string, FractalResponseData | FractalResponseList | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows a model to have optional relationships that are marked as being
|
|
||||||
* present in a given pathway. This allows different API calls to specify the
|
|
||||||
* "completeness" of a response object without having to make every API return
|
|
||||||
* the same information, or every piece of logic do explicit null checking.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* >> const user: WithLoaded<User, 'servers'> = {};
|
|
||||||
* >> // "user.servers" is no longer potentially undefined.
|
|
||||||
*/
|
|
||||||
type WithLoaded<M extends ModelWithRelationships, R extends keyof M['relationships']> = M & {
|
|
||||||
relationships: MarkRequired<M['relationships'], R>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper type that allows you to infer the type of an object by giving
|
|
||||||
* it the specific API request function with a return type. For example:
|
|
||||||
*
|
|
||||||
* type Egg = InferModel<typeof getEgg>;
|
|
||||||
*/
|
|
||||||
export type InferModel<T extends (...args: any) => any> = ReturnType<T> extends Promise<infer U> ? U : T;
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './models.d';
|
|
||||||
export { default as Transformers, MetaTransformers } from './transformers';
|
|
@ -1,35 +0,0 @@
|
|||||||
import { Model, UUID } from '@/api/definitions';
|
|
||||||
import { SubuserPermission } from '@/state/server/subusers';
|
|
||||||
|
|
||||||
interface User extends Model {
|
|
||||||
uuid: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
image: string;
|
|
||||||
twoFactorEnabled: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
permissions: SubuserPermission[];
|
|
||||||
can(permission: SubuserPermission): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SSHKey extends Model {
|
|
||||||
name: string;
|
|
||||||
publicKey: string;
|
|
||||||
fingerprint: string;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityLog extends Model<'actor'> {
|
|
||||||
id: string;
|
|
||||||
batch: UUID | null;
|
|
||||||
event: string;
|
|
||||||
ip: string | null;
|
|
||||||
isApi: boolean;
|
|
||||||
description: string | null;
|
|
||||||
properties: Record<string, string | unknown>;
|
|
||||||
hasAdditionalMetadata: boolean;
|
|
||||||
timestamp: Date;
|
|
||||||
relationships: {
|
|
||||||
actor: User | null;
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import * as Models from '@definitions/user/models';
|
|
||||||
import { FractalResponseData } from '@/api/http';
|
|
||||||
import { transform } from '@definitions/helpers';
|
|
||||||
|
|
||||||
export default class Transformers {
|
|
||||||
static toSSHKey = (data: Record<any, any>): Models.SSHKey => {
|
|
||||||
return {
|
|
||||||
name: data.name,
|
|
||||||
publicKey: data.public_key,
|
|
||||||
fingerprint: data.fingerprint,
|
|
||||||
createdAt: new Date(data.created_at),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
static toUser = ({ attributes }: FractalResponseData): Models.User => {
|
|
||||||
return {
|
|
||||||
uuid: attributes.uuid,
|
|
||||||
username: attributes.username,
|
|
||||||
email: attributes.email,
|
|
||||||
image: attributes.image,
|
|
||||||
twoFactorEnabled: attributes['2fa_enabled'],
|
|
||||||
permissions: attributes.permissions || [],
|
|
||||||
createdAt: new Date(attributes.created_at),
|
|
||||||
can(permission): boolean {
|
|
||||||
return this.permissions.includes(permission);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
|
|
||||||
const { actor } = attributes.relationships || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: attributes.id,
|
|
||||||
batch: attributes.batch,
|
|
||||||
event: attributes.event,
|
|
||||||
ip: attributes.ip,
|
|
||||||
isApi: attributes.is_api,
|
|
||||||
description: attributes.description,
|
|
||||||
properties: attributes.properties,
|
|
||||||
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
|
|
||||||
timestamp: new Date(attributes.timestamp),
|
|
||||||
relationships: {
|
|
||||||
actor: transform(actor as FractalResponseData, this.toUser, null),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MetaTransformers {}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { rawDataToServerObject, Server } from '@/api/server/getServer';
|
|
||||||
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
|
||||||
|
|
||||||
interface QueryParams {
|
|
||||||
query?: string;
|
|
||||||
page?: number;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Server>> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get('/api/client', {
|
|
||||||
params: {
|
|
||||||
'filter[*]': query,
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) =>
|
|
||||||
resolve({
|
|
||||||
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
|
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { PanelPermissions } from '@/state/permissions';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (): Promise<PanelPermissions> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get('/api/client/permissions')
|
|
||||||
.then(({ data }) => resolve(data.attributes.permissions))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,160 +0,0 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
|
||||||
import { store } from '@/state';
|
|
||||||
|
|
||||||
const http: AxiosInstance = axios.create({
|
|
||||||
withCredentials: true,
|
|
||||||
timeout: 20000,
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
http.interceptors.request.use((req) => {
|
|
||||||
if (!req.url?.endsWith('/resources')) {
|
|
||||||
store.getActions().progress.startContinuous();
|
|
||||||
}
|
|
||||||
|
|
||||||
return req;
|
|
||||||
});
|
|
||||||
|
|
||||||
http.interceptors.response.use(
|
|
||||||
(resp) => {
|
|
||||||
if (!resp.request?.url?.endsWith('/resources')) {
|
|
||||||
store.getActions().progress.setComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
store.getActions().progress.setComplete();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default http;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an error into a human readable response. Mostly just a generic helper to
|
|
||||||
* make sure we display the message from the server back to the user if we can.
|
|
||||||
*/
|
|
||||||
export function httpErrorToHuman(error: any): string {
|
|
||||||
if (error.response && error.response.data) {
|
|
||||||
let { data } = error.response;
|
|
||||||
|
|
||||||
// Some non-JSON requests can still return the error as a JSON block. In those cases, attempt
|
|
||||||
// to parse it into JSON so we can display an actual error.
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing, bad json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors && data.errors[0] && data.errors[0].detail) {
|
|
||||||
return data.errors[0].detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errors from daemon directory, mostly just for file uploads.
|
|
||||||
if (data.error && typeof data.error === 'string') {
|
|
||||||
return data.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FractalResponseData {
|
|
||||||
object: string;
|
|
||||||
attributes: {
|
|
||||||
[k: string]: any;
|
|
||||||
relationships?: Record<string, FractalResponseData | FractalResponseList | null | undefined>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FractalResponseList {
|
|
||||||
object: 'list';
|
|
||||||
data: FractalResponseData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FractalPaginatedResponse extends FractalResponseList {
|
|
||||||
meta: {
|
|
||||||
pagination: {
|
|
||||||
total: number;
|
|
||||||
count: number;
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
per_page: number;
|
|
||||||
current_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedResult<T> {
|
|
||||||
items: T[];
|
|
||||||
pagination: PaginationDataSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationDataSet {
|
|
||||||
total: number;
|
|
||||||
count: number;
|
|
||||||
perPage: number;
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPaginationSet(data: any): PaginationDataSet {
|
|
||||||
return {
|
|
||||||
total: data.total,
|
|
||||||
count: data.count,
|
|
||||||
perPage: data.per_page,
|
|
||||||
currentPage: data.current_page,
|
|
||||||
totalPages: data.total_pages,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryBuilderFilterValue = string | number | boolean | null;
|
|
||||||
|
|
||||||
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
|
|
||||||
page?: number;
|
|
||||||
filters?: {
|
|
||||||
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
|
|
||||||
};
|
|
||||||
sorts?: {
|
|
||||||
[K in SortKeys]?: -1 | 0 | 1 | 'asc' | 'desc' | null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function that parses a data object provided and builds query parameters
|
|
||||||
* for the Laravel Query Builder package automatically. This will apply sorts and
|
|
||||||
* filters deterministically based on the provided values.
|
|
||||||
*/
|
|
||||||
export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string, unknown> => {
|
|
||||||
if (!data) return {};
|
|
||||||
|
|
||||||
const filters = Object.keys(data.filters || {}).reduce((obj, key) => {
|
|
||||||
const value = data.filters?.[key];
|
|
||||||
|
|
||||||
return !value || value === '' ? obj : { ...obj, [`filter[${key}]`]: value };
|
|
||||||
}, {} as NonNullable<QueryBuilderParams['filters']>);
|
|
||||||
|
|
||||||
const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => {
|
|
||||||
const value = data.sorts?.[key];
|
|
||||||
if (!value || !['asc', 'desc', 1, -1].includes(value)) {
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...arr, (value === -1 || value === 'desc' ? '-' : '') + key];
|
|
||||||
}, [] as string[]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...filters,
|
|
||||||
sort: !sorts.length ? undefined : sorts.join(','),
|
|
||||||
page: data.page,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { History } from 'history';
|
|
||||||
|
|
||||||
export const setupInterceptors = (history: History) => {
|
|
||||||
http.interceptors.response.use(
|
|
||||||
(resp) => resp,
|
|
||||||
(error: AxiosError) => {
|
|
||||||
if (error.response?.status === 400) {
|
|
||||||
if (
|
|
||||||
(error.response?.data as Record<string, any>).errors?.[0].code === 'TwoFactorAuthRequiredException'
|
|
||||||
) {
|
|
||||||
if (!window.location.pathname.startsWith('/account')) {
|
|
||||||
history.replace('/account', { twoFactorRedirect: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
import useSWR, { ConfigInterface, responseInterface } from 'swr';
|
|
||||||
import { ActivityLog, Transformers } from '@definitions/user';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
|
||||||
import { toPaginatedSet } from '@definitions/helpers';
|
|
||||||
import useFilteredObject from '@/plugins/useFilteredObject';
|
|
||||||
import { useServerSWRKey } from '@/plugins/useSWRKey';
|
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
|
|
||||||
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
|
||||||
|
|
||||||
const useActivityLogs = (
|
|
||||||
filters?: ActivityLogFilters,
|
|
||||||
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
|
|
||||||
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
|
||||||
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
|
|
||||||
const key = useServerSWRKey(['activity', useFilteredObject(filters || {})]);
|
|
||||||
|
|
||||||
return useSWR<PaginatedResult<ActivityLog>>(
|
|
||||||
key,
|
|
||||||
async () => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
|
|
||||||
params: {
|
|
||||||
...withQueryBuilderParams(filters),
|
|
||||||
include: ['actor'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return toPaginatedSet(data, Transformers.toActivityLog);
|
|
||||||
},
|
|
||||||
{ revalidateOnMount: false, ...(config || {}) }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useActivityLogs };
|
|
@ -1,19 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { ServerBackup } from '@/api/server/types';
|
|
||||||
import { rawDataToServerBackup } from '@/api/transformers';
|
|
||||||
|
|
||||||
interface RequestParameters {
|
|
||||||
name?: string;
|
|
||||||
ignored?: string;
|
|
||||||
isLocked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (uuid: string, params: RequestParameters): Promise<ServerBackup> => {
|
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/backups`, {
|
|
||||||
name: params.name,
|
|
||||||
ignored: params.ignored,
|
|
||||||
is_locked: params.isLocked,
|
|
||||||
});
|
|
||||||
|
|
||||||
return rawDataToServerBackup(data);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, backup: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/servers/${uuid}/backups/${backup}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, backup: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/backups/${backup}/download`)
|
|
||||||
.then(({ data }) => resolve(data.attributes.url))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export const restoreServerBackup = async (uuid: string, backup: string, truncate?: boolean): Promise<void> => {
|
|
||||||
await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`, {
|
|
||||||
truncate,
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/databases/getServerDatabases';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise<ServerDatabase> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(
|
|
||||||
`/api/client/servers/${uuid}/databases`,
|
|
||||||
{
|
|
||||||
database: data.databaseName,
|
|
||||||
remote: data.connectionsFrom,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: { include: 'password' },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, database: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/servers/${uuid}/databases/${database}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export interface ServerDatabase {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
username: string;
|
|
||||||
connectionString: string;
|
|
||||||
allowConnectionsFrom: string;
|
|
||||||
password?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
|
|
||||||
id: data.id,
|
|
||||||
name: data.name,
|
|
||||||
username: data.username,
|
|
||||||
connectionString: `${data.host.address}:${data.host.port}`,
|
|
||||||
allowConnectionsFrom: data.connections_from,
|
|
||||||
password: data.relationships.password?.attributes?.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (uuid: string, includePassword = true): Promise<ServerDatabase[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/databases`, {
|
|
||||||
params: includePassword ? { include: 'password' } : undefined,
|
|
||||||
})
|
|
||||||
.then((response) =>
|
|
||||||
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)))
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/databases/getServerDatabases';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, database: string): Promise<ServerDatabase> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`)
|
|
||||||
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,37 +0,0 @@
|
|||||||
/* The MIT License (MIT)
|
|
||||||
|
|
||||||
Pterodactyl®
|
|
||||||
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE. */
|
|
||||||
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
file: string;
|
|
||||||
mode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (uuid: string, directory: string, files: Data[]): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/files/chmod`, { root: directory, files })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToFileObject } from '@/api/transformers';
|
|
||||||
|
|
||||||
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
|
|
||||||
const { data } = await http.post(
|
|
||||||
`/api/client/servers/${uuid}/files/compress`,
|
|
||||||
{ root: directory, files },
|
|
||||||
{
|
|
||||||
timeout: 60000,
|
|
||||||
timeoutErrorMessage:
|
|
||||||
'It looks like this archive is taking a long time to generate. It will appear once completed.',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return rawDataToFileObject(data);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, location: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/files/copy`, { location })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, root: string, name: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/files/create-folder`, { root, name })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (uuid: string, directory: string, file: string): Promise<void> => {
|
|
||||||
await http.post(
|
|
||||||
`/api/client/servers/${uuid}/files/decompress`,
|
|
||||||
{ root: directory, file },
|
|
||||||
{
|
|
||||||
timeout: 300000,
|
|
||||||
timeoutErrorMessage:
|
|
||||||
'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, directory: string, files: string[]): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/files/delete`, { root: directory, files })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (server: string, file: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${server}/files/contents`, {
|
|
||||||
params: { file },
|
|
||||||
transformResponse: (res) => res,
|
|
||||||
responseType: 'text',
|
|
||||||
})
|
|
||||||
.then(({ data }) => resolve(data))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, file: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/files/download`, { params: { file } })
|
|
||||||
.then(({ data }) => resolve(data.attributes.url))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
/* The MIT License (MIT)
|
|
||||||
|
|
||||||
Pterodactyl®
|
|
||||||
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE. */
|
|
||||||
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/files/upload`)
|
|
||||||
.then(({ data }) => resolve(data.attributes.url))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToFileObject } from '@/api/transformers';
|
|
||||||
|
|
||||||
export interface FileObject {
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
mode: string;
|
|
||||||
modeBits: string;
|
|
||||||
size: number;
|
|
||||||
isFile: boolean;
|
|
||||||
isSymlink: boolean;
|
|
||||||
mimetype: string;
|
|
||||||
createdAt: Date;
|
|
||||||
modifiedAt: Date;
|
|
||||||
isArchiveType: () => boolean;
|
|
||||||
isEditable: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
|
|
||||||
params: { directory: directory ?? '/' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return (data.data || []).map(rawDataToFileObject);
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
to: string;
|
|
||||||
from: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (uuid: string, directory: string, files: Data[]): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.put(`/api/client/servers/${uuid}/files/rename`, { root: directory, files })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (uuid: string, file: string, content: string): Promise<void> => {
|
|
||||||
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
|
|
||||||
params: { file },
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,89 +0,0 @@
|
|||||||
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/transformers';
|
|
||||||
import { ServerEggVariable, ServerStatus } from '@/api/server/types';
|
|
||||||
|
|
||||||
export interface Allocation {
|
|
||||||
id: number;
|
|
||||||
ip: string;
|
|
||||||
alias: string | null;
|
|
||||||
port: number;
|
|
||||||
notes: string | null;
|
|
||||||
isDefault: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Server {
|
|
||||||
id: string;
|
|
||||||
internalId: number | string;
|
|
||||||
uuid: string;
|
|
||||||
name: string;
|
|
||||||
node: string;
|
|
||||||
isNodeUnderMaintenance: boolean;
|
|
||||||
status: ServerStatus;
|
|
||||||
sftpDetails: {
|
|
||||||
ip: string;
|
|
||||||
alias: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
invocation: string;
|
|
||||||
dockerImage: string;
|
|
||||||
description: string;
|
|
||||||
limits: {
|
|
||||||
memory: number;
|
|
||||||
swap: number;
|
|
||||||
disk: number;
|
|
||||||
io: number;
|
|
||||||
cpu: number;
|
|
||||||
threads: string;
|
|
||||||
};
|
|
||||||
eggFeatures: string[];
|
|
||||||
featureLimits: {
|
|
||||||
databases: number;
|
|
||||||
allocations: number;
|
|
||||||
backups: number;
|
|
||||||
};
|
|
||||||
isTransferring: boolean;
|
|
||||||
variables: ServerEggVariable[];
|
|
||||||
allocations: Allocation[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
|
||||||
id: data.identifier,
|
|
||||||
internalId: data.internal_id,
|
|
||||||
uuid: data.uuid,
|
|
||||||
name: data.name,
|
|
||||||
node: data.node,
|
|
||||||
isNodeUnderMaintenance: data.is_node_under_maintenance,
|
|
||||||
status: data.status,
|
|
||||||
invocation: data.invocation,
|
|
||||||
dockerImage: data.docker_image,
|
|
||||||
sftpDetails: {
|
|
||||||
ip: data.sftp_details.ip,
|
|
||||||
alias: data.sftp_details.alias,
|
|
||||||
port: data.sftp_details.port,
|
|
||||||
},
|
|
||||||
description: data.description ? (data.description.length > 0 ? data.description : null) : null,
|
|
||||||
limits: { ...data.limits },
|
|
||||||
eggFeatures: data.egg_features || [],
|
|
||||||
featureLimits: { ...data.feature_limits },
|
|
||||||
isTransferring: data.is_transferring,
|
|
||||||
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
|
|
||||||
rawDataToServerEggVariable
|
|
||||||
),
|
|
||||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
|
|
||||||
rawDataToServerAllocation
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (uuid: string): Promise<[Server, string[]]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}`)
|
|
||||||
.then(({ data }) =>
|
|
||||||
resolve([
|
|
||||||
rawDataToServerObject(data),
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,33 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export type ServerPowerState = 'offline' | 'starting' | 'running' | 'stopping';
|
|
||||||
|
|
||||||
export interface ServerStats {
|
|
||||||
status: ServerPowerState;
|
|
||||||
isSuspended: boolean;
|
|
||||||
memoryUsageInBytes: number;
|
|
||||||
cpuUsagePercent: number;
|
|
||||||
diskUsageInBytes: number;
|
|
||||||
networkRxInBytes: number;
|
|
||||||
networkTxInBytes: number;
|
|
||||||
uptime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (server: string): Promise<ServerStats> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${server}/resources`)
|
|
||||||
.then(({ data: { attributes } }) =>
|
|
||||||
resolve({
|
|
||||||
status: attributes.current_state,
|
|
||||||
isSuspended: attributes.is_suspended,
|
|
||||||
memoryUsageInBytes: attributes.resources.memory_bytes,
|
|
||||||
cpuUsagePercent: attributes.resources.cpu_absolute,
|
|
||||||
diskUsageInBytes: attributes.resources.disk_bytes,
|
|
||||||
networkRxInBytes: attributes.resources.network_rx_bytes,
|
|
||||||
networkTxInBytes: attributes.resources.network_tx_bytes,
|
|
||||||
uptime: attributes.resources.uptime,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Response {
|
|
||||||
token: string;
|
|
||||||
socket: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (server: string): Promise<Response> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${server}/websocket`)
|
|
||||||
.then(({ data }) =>
|
|
||||||
resolve({
|
|
||||||
token: data.data.token,
|
|
||||||
socket: data.data.socket,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
|
||||||
|
|
||||||
export default async (uuid: string): Promise<Allocation> => {
|
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`);
|
|
||||||
|
|
||||||
return rawDataToServerAllocation(data);
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (uuid: string, id: number): Promise<Allocation> =>
|
|
||||||
await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`);
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
|
||||||
|
|
||||||
export default async (uuid: string, id: number): Promise<Allocation> => {
|
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}/primary`);
|
|
||||||
|
|
||||||
return rawDataToServerAllocation(data);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
|
||||||
|
|
||||||
export default async (uuid: string, id: number, notes: string | null): Promise<Allocation> => {
|
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}`, { notes });
|
|
||||||
|
|
||||||
return rawDataToServerAllocation(data);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/settings/reinstall`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, name: string, description?: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/settings/rename`, { name, description })
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
type Data = Pick<Schedule, 'cron' | 'name' | 'onlyWhenOnline' | 'isActive'> & { id?: number };
|
|
||||||
|
|
||||||
export default async (uuid: string, schedule: Data): Promise<Schedule> => {
|
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
|
|
||||||
is_active: schedule.isActive,
|
|
||||||
only_when_online: schedule.onlyWhenOnline,
|
|
||||||
name: schedule.name,
|
|
||||||
minute: schedule.cron.minute,
|
|
||||||
hour: schedule.cron.hour,
|
|
||||||
day_of_month: schedule.cron.dayOfMonth,
|
|
||||||
month: schedule.cron.month,
|
|
||||||
day_of_week: schedule.cron.dayOfWeek,
|
|
||||||
});
|
|
||||||
|
|
||||||
return rawDataToServerSchedule(data.attributes);
|
|
||||||
};
|
|
@ -1,23 +0,0 @@
|
|||||||
import { rawDataToServerTask, Task } from '@/api/server/schedules/getServerSchedules';
|
|
||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
action: string;
|
|
||||||
payload: string;
|
|
||||||
timeOffset: string | number;
|
|
||||||
continueOnFailure: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (uuid: string, schedule: number, task: number | undefined, data: Data): Promise<Task> => {
|
|
||||||
const { data: response } = await http.post(
|
|
||||||
`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`,
|
|
||||||
{
|
|
||||||
action: data.action,
|
|
||||||
payload: data.payload,
|
|
||||||
continue_on_failure: data.continueOnFailure,
|
|
||||||
time_offset: data.timeOffset,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return rawDataToServerTask(response.attributes);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, schedule: number): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/servers/${uuid}/schedules/${schedule}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, scheduleId: number, taskId: number): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
|
|
||||||
|
|
||||||
export default (uuid: string, schedule: number): Promise<Schedule> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
|
||||||
params: {
|
|
||||||
include: ['tasks'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export interface Schedule {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
cron: {
|
|
||||||
dayOfWeek: string;
|
|
||||||
month: string;
|
|
||||||
dayOfMonth: string;
|
|
||||||
hour: string;
|
|
||||||
minute: string;
|
|
||||||
};
|
|
||||||
isActive: boolean;
|
|
||||||
isProcessing: boolean;
|
|
||||||
onlyWhenOnline: boolean;
|
|
||||||
lastRunAt: Date | null;
|
|
||||||
nextRunAt: Date | null;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
tasks: Task[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Task {
|
|
||||||
id: number;
|
|
||||||
sequenceId: number;
|
|
||||||
action: string;
|
|
||||||
payload: string;
|
|
||||||
timeOffset: number;
|
|
||||||
isQueued: boolean;
|
|
||||||
continueOnFailure: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawDataToServerTask = (data: any): Task => ({
|
|
||||||
id: data.id,
|
|
||||||
sequenceId: data.sequence_id,
|
|
||||||
action: data.action,
|
|
||||||
payload: data.payload,
|
|
||||||
timeOffset: data.time_offset,
|
|
||||||
isQueued: data.is_queued,
|
|
||||||
continueOnFailure: data.continue_on_failure,
|
|
||||||
createdAt: new Date(data.created_at),
|
|
||||||
updatedAt: new Date(data.updated_at),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rawDataToServerSchedule = (data: any): Schedule => ({
|
|
||||||
id: data.id,
|
|
||||||
name: data.name,
|
|
||||||
cron: {
|
|
||||||
dayOfWeek: data.cron.day_of_week,
|
|
||||||
month: data.cron.month,
|
|
||||||
dayOfMonth: data.cron.day_of_month,
|
|
||||||
hour: data.cron.hour,
|
|
||||||
minute: data.cron.minute,
|
|
||||||
},
|
|
||||||
isActive: data.is_active,
|
|
||||||
isProcessing: data.is_processing,
|
|
||||||
onlyWhenOnline: data.only_when_online,
|
|
||||||
lastRunAt: data.last_run_at ? new Date(data.last_run_at) : null,
|
|
||||||
nextRunAt: data.next_run_at ? new Date(data.next_run_at) : null,
|
|
||||||
createdAt: new Date(data.created_at),
|
|
||||||
updatedAt: new Date(data.updated_at),
|
|
||||||
|
|
||||||
tasks: (data.relationships?.tasks?.data || []).map((row: any) => rawDataToServerTask(row.attributes)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default async (uuid: string): Promise<Schedule[]> => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, {
|
|
||||||
params: {
|
|
||||||
include: ['tasks'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes));
|
|
||||||
};
|
|
@ -1,4 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (server: string, schedule: number): Promise<void> =>
|
|
||||||
await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`);
|
|
@ -1,5 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default async (uuid: string, image: string): Promise<void> => {
|
|
||||||
await http.put(`/api/client/servers/${uuid}/settings/docker-image`, { docker_image: image });
|
|
||||||
};
|
|
29
resources/scripts/api/server/types.d.ts
vendored
29
resources/scripts/api/server/types.d.ts
vendored
@ -1,29 +0,0 @@
|
|||||||
export type ServerStatus =
|
|
||||||
| 'installing'
|
|
||||||
| 'install_failed'
|
|
||||||
| 'reinstall_failed'
|
|
||||||
| 'suspended'
|
|
||||||
| 'restoring_backup'
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export interface ServerBackup {
|
|
||||||
uuid: string;
|
|
||||||
isSuccessful: boolean;
|
|
||||||
isLocked: boolean;
|
|
||||||
name: string;
|
|
||||||
ignoredFiles: string;
|
|
||||||
checksum: string;
|
|
||||||
bytes: number;
|
|
||||||
createdAt: Date;
|
|
||||||
completedAt: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerEggVariable {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
envVariable: string;
|
|
||||||
defaultValue: string;
|
|
||||||
serverValue: string | null;
|
|
||||||
isEditable: boolean;
|
|
||||||
rules: string[];
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { ServerEggVariable } from '@/api/server/types';
|
|
||||||
import { rawDataToServerEggVariable } from '@/api/transformers';
|
|
||||||
|
|
||||||
export default async (uuid: string, key: string, value: string): Promise<[ServerEggVariable, string]> => {
|
|
||||||
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
|
||||||
|
|
||||||
return [rawDataToServerEggVariable(data), data.meta.startup_command];
|
|
||||||
};
|
|
@ -1,18 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerSubuser } from '@/api/server/users/getServerSubusers';
|
|
||||||
import { Subuser } from '@/state/server/subusers';
|
|
||||||
|
|
||||||
interface Params {
|
|
||||||
email: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuser> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
|
|
||||||
...params,
|
|
||||||
})
|
|
||||||
.then((data) => resolve(rawDataToServerSubuser(data.data)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import http from '@/api/http';
|
|
||||||
|
|
||||||
export default (uuid: string, userId: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.delete(`/api/client/servers/${uuid}/users/${userId}`)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
import http, { FractalResponseData } from '@/api/http';
|
|
||||||
import { Subuser } from '@/state/server/subusers';
|
|
||||||
|
|
||||||
export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
|
|
||||||
uuid: data.attributes.uuid,
|
|
||||||
username: data.attributes.username,
|
|
||||||
email: data.attributes.email,
|
|
||||||
image: data.attributes.image,
|
|
||||||
twoFactorEnabled: data.attributes['2fa_enabled'],
|
|
||||||
createdAt: new Date(data.attributes.created_at),
|
|
||||||
permissions: data.attributes.permissions || [],
|
|
||||||
can: (permission) => (data.attributes.permissions || []).indexOf(permission) >= 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (uuid: string): Promise<Subuser[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
http.get(`/api/client/servers/${uuid}/users`)
|
|
||||||
.then(({ data }) => resolve((data.data || []).map(rawDataToServerSubuser)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
|
||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
|
||||||
|
|
||||||
return useSWR<Allocation[]>(
|
|
||||||
['server:allocations', uuid],
|
|
||||||
async () => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
|
||||||
|
|
||||||
return (data.data || []).map(rawDataToServerAllocation);
|
|
||||||
},
|
|
||||||
{ revalidateOnFocus: false, revalidateOnMount: false }
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
import useSWR from 'swr';
|
|
||||||
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
|
||||||
import { ServerBackup } from '@/api/server/types';
|
|
||||||
import { rawDataToServerBackup } from '@/api/transformers';
|
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import { createContext, useContext } from 'react';
|
|
||||||
|
|
||||||
interface ctx {
|
|
||||||
page: number;
|
|
||||||
setPage: (value: number | ((s: number) => number)) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
|
||||||
|
|
||||||
type BackupResponse = PaginatedResult<ServerBackup> & { backupCount: number };
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { page } = useContext(Context);
|
|
||||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
|
||||||
|
|
||||||
return useSWR<BackupResponse>(['server:backups', uuid, page], async () => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: (data.data || []).map(rawDataToServerBackup),
|
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
|
||||||
backupCount: data.meta.backup_count,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
import useSWR, { ConfigInterface } from 'swr';
|
|
||||||
import http, { FractalResponseList } from '@/api/http';
|
|
||||||
import { rawDataToServerEggVariable } from '@/api/transformers';
|
|
||||||
import { ServerEggVariable } from '@/api/server/types';
|
|
||||||
|
|
||||||
interface Response {
|
|
||||||
invocation: string;
|
|
||||||
variables: ServerEggVariable[];
|
|
||||||
dockerImages: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) =>
|
|
||||||
useSWR(
|
|
||||||
[uuid, '/startup'],
|
|
||||||
async (): Promise<Response> => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
|
||||||
|
|
||||||
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
|
||||||
|
|
||||||
return {
|
|
||||||
variables,
|
|
||||||
invocation: data.meta.startup_command,
|
|
||||||
dockerImages: data.meta.docker_images || {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) }
|
|
||||||
);
|
|
@ -1,77 +0,0 @@
|
|||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import { FractalResponseData } from '@/api/http';
|
|
||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
|
||||||
import { ServerBackup, ServerEggVariable } from '@/api/server/types';
|
|
||||||
|
|
||||||
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
|
||||||
id: data.attributes.id,
|
|
||||||
ip: data.attributes.ip,
|
|
||||||
alias: data.attributes.ip_alias,
|
|
||||||
port: data.attributes.port,
|
|
||||||
notes: data.attributes.notes,
|
|
||||||
isDefault: data.attributes.is_default,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
|
|
||||||
key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
|
|
||||||
name: data.attributes.name,
|
|
||||||
mode: data.attributes.mode,
|
|
||||||
modeBits: data.attributes.mode_bits,
|
|
||||||
size: Number(data.attributes.size),
|
|
||||||
isFile: data.attributes.is_file,
|
|
||||||
isSymlink: data.attributes.is_symlink,
|
|
||||||
mimetype: data.attributes.mimetype,
|
|
||||||
createdAt: new Date(data.attributes.created_at),
|
|
||||||
modifiedAt: new Date(data.attributes.modified_at),
|
|
||||||
|
|
||||||
isArchiveType: function () {
|
|
||||||
return (
|
|
||||||
this.isFile &&
|
|
||||||
[
|
|
||||||
'application/vnd.rar', // .rar
|
|
||||||
'application/x-rar-compressed', // .rar (2)
|
|
||||||
'application/x-tar', // .tar
|
|
||||||
'application/x-br', // .tar.br
|
|
||||||
'application/x-bzip2', // .tar.bz2, .bz2
|
|
||||||
'application/gzip', // .tar.gz, .gz
|
|
||||||
'application/x-gzip',
|
|
||||||
'application/x-lzip', // .tar.lz4, .lz4 (not sure if this mime type is correct)
|
|
||||||
'application/x-sz', // .tar.sz, .sz (not sure if this mime type is correct)
|
|
||||||
'application/x-xz', // .tar.xz, .xz
|
|
||||||
'application/x-7z-compressed', // .7z
|
|
||||||
'application/zstd', // .tar.zst, .zst
|
|
||||||
'application/zip', // .zip
|
|
||||||
].indexOf(this.mimetype) >= 0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
isEditable: function () {
|
|
||||||
if (this.isArchiveType() || !this.isFile) return false;
|
|
||||||
|
|
||||||
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\/(?!svg\+xml)/];
|
|
||||||
|
|
||||||
return matches.every((m) => !this.mimetype.match(m));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({
|
|
||||||
uuid: attributes.uuid,
|
|
||||||
isSuccessful: attributes.is_successful,
|
|
||||||
isLocked: attributes.is_locked,
|
|
||||||
name: attributes.name,
|
|
||||||
ignoredFiles: attributes.ignored_files,
|
|
||||||
checksum: attributes.checksum,
|
|
||||||
bytes: attributes.bytes,
|
|
||||||
createdAt: new Date(attributes.created_at),
|
|
||||||
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): ServerEggVariable => ({
|
|
||||||
name: attributes.name,
|
|
||||||
description: attributes.description,
|
|
||||||
envVariable: attributes.env_variable,
|
|
||||||
defaultValue: attributes.default_value,
|
|
||||||
serverValue: attributes.server_value,
|
|
||||||
isEditable: attributes.is_editable,
|
|
||||||
rules: attributes.rules.split('|'),
|
|
||||||
});
|
|
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