diff --git a/resources/scripts/TransitionRouter.tsx b/resources/scripts/TransitionRouter.tsx deleted file mode 100644 index 040097eaa..000000000 --- a/resources/scripts/TransitionRouter.tsx +++ /dev/null @@ -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 ( - ( - - -
{children}
-
-
- )} - /> - ); -}; - -export default TransitionRouter; diff --git a/resources/scripts/__mocks__/file.ts b/resources/scripts/__mocks__/file.ts deleted file mode 100644 index 86059f362..000000000 --- a/resources/scripts/__mocks__/file.ts +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'test-file-stub'; diff --git a/resources/scripts/api/account/activity.ts b/resources/scripts/api/account/activity.ts deleted file mode 100644 index eef215696..000000000 --- a/resources/scripts/api/account/activity.ts +++ /dev/null @@ -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, AxiosError> -): responseInterface, AxiosError> => { - const key = useUserSWRKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]); - - return useSWR>( - 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 }; diff --git a/resources/scripts/api/account/createApiKey.ts b/resources/scripts/api/account/createApiKey.ts deleted file mode 100644 index b20760e62..000000000 --- a/resources/scripts/api/account/createApiKey.ts +++ /dev/null @@ -1,19 +0,0 @@ -import http from '@/api/http'; -import { ApiKey, rawDataToApiKey } from '@/api/account/getApiKeys'; - -export default (description: string, allowedIps: string): Promise => { - 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); - }); -}; diff --git a/resources/scripts/api/account/deleteApiKey.ts b/resources/scripts/api/account/deleteApiKey.ts deleted file mode 100644 index e34350d0f..000000000 --- a/resources/scripts/api/account/deleteApiKey.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (identifier: string): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/account/api-keys/${identifier}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/account/disableAccountTwoFactor.ts b/resources/scripts/api/account/disableAccountTwoFactor.ts deleted file mode 100644 index 2b41fe20f..000000000 --- a/resources/scripts/api/account/disableAccountTwoFactor.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (password: string): Promise => { - return new Promise((resolve, reject) => { - http.delete('/api/client/account/two-factor', { params: { password } }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/account/enableAccountTwoFactor.ts b/resources/scripts/api/account/enableAccountTwoFactor.ts deleted file mode 100644 index 25c63ad47..000000000 --- a/resources/scripts/api/account/enableAccountTwoFactor.ts +++ /dev/null @@ -1,7 +0,0 @@ -import http from '@/api/http'; - -export default async (code: string, password: string): Promise => { - const { data } = await http.post('/api/client/account/two-factor', { code, password }); - - return data.attributes.tokens; -}; diff --git a/resources/scripts/api/account/getApiKeys.ts b/resources/scripts/api/account/getApiKeys.ts deleted file mode 100644 index f8937ddd6..000000000 --- a/resources/scripts/api/account/getApiKeys.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/account/getTwoFactorTokenData.ts b/resources/scripts/api/account/getTwoFactorTokenData.ts deleted file mode 100644 index 11d622aff..000000000 --- a/resources/scripts/api/account/getTwoFactorTokenData.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* The MIT License (MIT) - - Pterodactyl® - Copyright © Dane Everitt 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 => { - return new Promise((resolve, reject) => { - http.get('/api/client/account/two-factor') - .then(({ data }) => resolve(data.data)) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/account/ssh-keys.ts b/resources/scripts/api/account/ssh-keys.ts deleted file mode 100644 index f35ff1e12..000000000 --- a/resources/scripts/api/account/ssh-keys.ts +++ /dev/null @@ -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) => { - 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 => { - 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 => - await http.post('/api/client/account/ssh-keys/remove', { fingerprint }); - -export { useSSHKeys, createSSHKey, deleteSSHKey }; diff --git a/resources/scripts/api/account/updateAccountEmail.ts b/resources/scripts/api/account/updateAccountEmail.ts deleted file mode 100644 index 5ff230265..000000000 --- a/resources/scripts/api/account/updateAccountEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (email: string, password: string): Promise => { - return new Promise((resolve, reject) => { - http.put('/api/client/account/email', { email, password }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/account/updateAccountPassword.ts b/resources/scripts/api/account/updateAccountPassword.ts deleted file mode 100644 index d59e85e9c..000000000 --- a/resources/scripts/api/account/updateAccountPassword.ts +++ /dev/null @@ -1,19 +0,0 @@ -import http from '@/api/http'; - -interface Data { - current: string; - password: string; - confirmPassword: string; -} - -export default ({ current, password, confirmPassword }: Data): Promise => { - return new Promise((resolve, reject) => { - http.put('/api/client/account/password', { - current_password: current, - password: password, - password_confirmation: confirmPassword, - }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts deleted file mode 100644 index d42f9b276..000000000 --- a/resources/scripts/api/auth/login.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/auth/loginCheckpoint.ts b/resources/scripts/api/auth/loginCheckpoint.ts deleted file mode 100644 index 73ffb2111..000000000 --- a/resources/scripts/api/auth/loginCheckpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import http from '@/api/http'; -import { LoginResponse } from '@/api/auth/login'; - -export default (token: string, code: string, recoveryToken?: string): Promise => { - 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); - }); -}; diff --git a/resources/scripts/api/auth/performPasswordReset.ts b/resources/scripts/api/auth/performPasswordReset.ts deleted file mode 100644 index 0dd12f470..000000000 --- a/resources/scripts/api/auth/performPasswordReset.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/auth/requestPasswordResetEmail.ts b/resources/scripts/api/auth/requestPasswordResetEmail.ts deleted file mode 100644 index cf3c3c49f..000000000 --- a/resources/scripts/api/auth/requestPasswordResetEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (email: string, recaptchaData?: string): Promise => { - return new Promise((resolve, reject) => { - http.post('/auth/password', { email, 'cf-turnstile-response': recaptchaData }) - .then((response) => resolve(response.data.status || '')) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/definitions/helpers.ts b/resources/scripts/api/definitions/helpers.ts deleted file mode 100644 index eeb933f5a..000000000 --- a/resources/scripts/api/definitions/helpers.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - FractalPaginatedResponse, - FractalResponseData, - FractalResponseList, - getPaginationSet, - PaginatedResult, -} from '@/api/http'; -import { Model } from '@definitions/index'; - -type TransformerFunc = (callback: FractalResponseData) => T; - -const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list'; - -function transform(data: null | undefined, transformer: TransformerFunc, missing?: M): M; -function transform( - data: FractalResponseData | null | undefined, - transformer: TransformerFunc, - missing?: M -): T | M; -function transform( - data: FractalResponseList | FractalPaginatedResponse | null | undefined, - transformer: TransformerFunc, - missing?: M -): T[] | M; -function transform( - data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined, - transformer: TransformerFunc, - 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>( - response: FractalPaginatedResponse, - transformer: T -): PaginatedResult> { - return { - items: transform(response, transformer) as ReturnType[], - pagination: getPaginationSet(response.meta.pagination), - }; -} - -export { transform, toPaginatedSet }; diff --git a/resources/scripts/api/definitions/index.d.ts b/resources/scripts/api/definitions/index.d.ts deleted file mode 100644 index d041884f6..000000000 --- a/resources/scripts/api/definitions/index.d.ts +++ /dev/null @@ -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; -} - -/** - * 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" is no longer potentially undefined. - */ -type WithLoaded = M & { - relationships: MarkRequired; -}; - -/** - * 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; - */ -export type InferModel any> = ReturnType extends Promise ? U : T; diff --git a/resources/scripts/api/definitions/user/index.ts b/resources/scripts/api/definitions/user/index.ts deleted file mode 100644 index 7d8db7094..000000000 --- a/resources/scripts/api/definitions/user/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './models.d'; -export { default as Transformers, MetaTransformers } from './transformers'; diff --git a/resources/scripts/api/definitions/user/models.d.ts b/resources/scripts/api/definitions/user/models.d.ts deleted file mode 100644 index a5b40ca67..000000000 --- a/resources/scripts/api/definitions/user/models.d.ts +++ /dev/null @@ -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; - hasAdditionalMetadata: boolean; - timestamp: Date; - relationships: { - actor: User | null; - }; -} diff --git a/resources/scripts/api/definitions/user/transformers.ts b/resources/scripts/api/definitions/user/transformers.ts deleted file mode 100644 index 1fa62d3f9..000000000 --- a/resources/scripts/api/definitions/user/transformers.ts +++ /dev/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): 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 {} diff --git a/resources/scripts/api/getServers.ts b/resources/scripts/api/getServers.ts deleted file mode 100644 index 4e29f1532..000000000 --- a/resources/scripts/api/getServers.ts +++ /dev/null @@ -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> => { - 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); - }); -}; diff --git a/resources/scripts/api/getSystemPermissions.ts b/resources/scripts/api/getSystemPermissions.ts deleted file mode 100644 index 0e7f27caa..000000000 --- a/resources/scripts/api/getSystemPermissions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PanelPermissions } from '@/state/permissions'; -import http from '@/api/http'; - -export default (): Promise => { - return new Promise((resolve, reject) => { - http.get('/api/client/permissions') - .then(({ data }) => resolve(data.attributes.permissions)) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts deleted file mode 100644 index c8a76e692..000000000 --- a/resources/scripts/api/http.ts +++ /dev/null @@ -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; - }; -} - -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 { - 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 { - page?: number; - filters?: { - [K in FilterKeys]?: QueryBuilderFilterValue | Readonly; - }; - 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 => { - 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); - - 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, - }; -}; diff --git a/resources/scripts/api/interceptors.ts b/resources/scripts/api/interceptors.ts deleted file mode 100644 index 7b8ac87da..000000000 --- a/resources/scripts/api/interceptors.ts +++ /dev/null @@ -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).errors?.[0].code === 'TwoFactorAuthRequiredException' - ) { - if (!window.location.pathname.startsWith('/account')) { - history.replace('/account', { twoFactorRedirect: true }); - } - } - } - throw error; - } - ); -}; diff --git a/resources/scripts/api/server/activity.ts b/resources/scripts/api/server/activity.ts deleted file mode 100644 index a7fa8d31b..000000000 --- a/resources/scripts/api/server/activity.ts +++ /dev/null @@ -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, AxiosError> -): responseInterface, AxiosError> => { - const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid); - const key = useServerSWRKey(['activity', useFilteredObject(filters || {})]); - - return useSWR>( - 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 }; diff --git a/resources/scripts/api/server/backups/createServerBackup.ts b/resources/scripts/api/server/backups/createServerBackup.ts deleted file mode 100644 index 3167b5b46..000000000 --- a/resources/scripts/api/server/backups/createServerBackup.ts +++ /dev/null @@ -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 => { - const { data } = await http.post(`/api/client/servers/${uuid}/backups`, { - name: params.name, - ignored: params.ignored, - is_locked: params.isLocked, - }); - - return rawDataToServerBackup(data); -}; diff --git a/resources/scripts/api/server/backups/deleteBackup.ts b/resources/scripts/api/server/backups/deleteBackup.ts deleted file mode 100644 index 01f48d23f..000000000 --- a/resources/scripts/api/server/backups/deleteBackup.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, backup: string): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/servers/${uuid}/backups/${backup}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/backups/getBackupDownloadUrl.ts b/resources/scripts/api/server/backups/getBackupDownloadUrl.ts deleted file mode 100644 index 70a3ae5e4..000000000 --- a/resources/scripts/api/server/backups/getBackupDownloadUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, backup: string): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/backups/${backup}/download`) - .then(({ data }) => resolve(data.attributes.url)) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/backups/index.ts b/resources/scripts/api/server/backups/index.ts deleted file mode 100644 index 4f1311fdf..000000000 --- a/resources/scripts/api/server/backups/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import http from '@/api/http'; - -export const restoreServerBackup = async (uuid: string, backup: string, truncate?: boolean): Promise => { - await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`, { - truncate, - }); -}; diff --git a/resources/scripts/api/server/databases/createServerDatabase.ts b/resources/scripts/api/server/databases/createServerDatabase.ts deleted file mode 100644 index cb0c25b9e..000000000 --- a/resources/scripts/api/server/databases/createServerDatabase.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/server/databases/deleteServerDatabase.ts b/resources/scripts/api/server/databases/deleteServerDatabase.ts deleted file mode 100644 index 23275bd36..000000000 --- a/resources/scripts/api/server/databases/deleteServerDatabase.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, database: string): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/servers/${uuid}/databases/${database}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/databases/getServerDatabases.ts b/resources/scripts/api/server/databases/getServerDatabases.ts deleted file mode 100644 index a03eb30a9..000000000 --- a/resources/scripts/api/server/databases/getServerDatabases.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/server/databases/rotateDatabasePassword.ts b/resources/scripts/api/server/databases/rotateDatabasePassword.ts deleted file mode 100644 index 0e0619a84..000000000 --- a/resources/scripts/api/server/databases/rotateDatabasePassword.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/databases/getServerDatabases'; -import http from '@/api/http'; - -export default (uuid: string, database: string): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`) - .then((response) => resolve(rawDataToServerDatabase(response.data.attributes))) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/chmodFiles.ts b/resources/scripts/api/server/files/chmodFiles.ts deleted file mode 100644 index 35dc3f174..000000000 --- a/resources/scripts/api/server/files/chmodFiles.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* The MIT License (MIT) - - Pterodactyl® - Copyright © Dane Everitt 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 => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/files/chmod`, { root: directory, files }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/compressFiles.ts b/resources/scripts/api/server/files/compressFiles.ts deleted file mode 100644 index b4c0a251e..000000000 --- a/resources/scripts/api/server/files/compressFiles.ts +++ /dev/null @@ -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 => { - 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); -}; diff --git a/resources/scripts/api/server/files/copyFile.ts b/resources/scripts/api/server/files/copyFile.ts deleted file mode 100644 index b19525c40..000000000 --- a/resources/scripts/api/server/files/copyFile.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, location: string): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/files/copy`, { location }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/createDirectory.ts b/resources/scripts/api/server/files/createDirectory.ts deleted file mode 100644 index 588868655..000000000 --- a/resources/scripts/api/server/files/createDirectory.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, root: string, name: string): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/files/create-folder`, { root, name }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/decompressFiles.ts b/resources/scripts/api/server/files/decompressFiles.ts deleted file mode 100644 index 37557a671..000000000 --- a/resources/scripts/api/server/files/decompressFiles.ts +++ /dev/null @@ -1,13 +0,0 @@ -import http from '@/api/http'; - -export default async (uuid: string, directory: string, file: string): Promise => { - 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.', - } - ); -}; diff --git a/resources/scripts/api/server/files/deleteFiles.ts b/resources/scripts/api/server/files/deleteFiles.ts deleted file mode 100644 index 1250463ed..000000000 --- a/resources/scripts/api/server/files/deleteFiles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, directory: string, files: string[]): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/files/delete`, { root: directory, files }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/getFileContents.ts b/resources/scripts/api/server/files/getFileContents.ts deleted file mode 100644 index 66df376d3..000000000 --- a/resources/scripts/api/server/files/getFileContents.ts +++ /dev/null @@ -1,13 +0,0 @@ -import http from '@/api/http'; - -export default (server: string, file: string): Promise => { - 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); - }); -}; diff --git a/resources/scripts/api/server/files/getFileDownloadUrl.ts b/resources/scripts/api/server/files/getFileDownloadUrl.ts deleted file mode 100644 index 39db97290..000000000 --- a/resources/scripts/api/server/files/getFileDownloadUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, file: string): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/files/download`, { params: { file } }) - .then(({ data }) => resolve(data.attributes.url)) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/getFileUploadUrl.ts b/resources/scripts/api/server/files/getFileUploadUrl.ts deleted file mode 100644 index 161f35c26..000000000 --- a/resources/scripts/api/server/files/getFileUploadUrl.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* The MIT License (MIT) - - Pterodactyl® - Copyright © Dane Everitt 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 => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/files/upload`) - .then(({ data }) => resolve(data.attributes.url)) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts deleted file mode 100644 index 1f90b8ac3..000000000 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ /dev/null @@ -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 => { - const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { - params: { directory: directory ?? '/' }, - }); - - return (data.data || []).map(rawDataToFileObject); -}; diff --git a/resources/scripts/api/server/files/renameFiles.ts b/resources/scripts/api/server/files/renameFiles.ts deleted file mode 100644 index 53f92c4c3..000000000 --- a/resources/scripts/api/server/files/renameFiles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import http from '@/api/http'; - -interface Data { - to: string; - from: string; -} - -export default (uuid: string, directory: string, files: Data[]): Promise => { - return new Promise((resolve, reject) => { - http.put(`/api/client/servers/${uuid}/files/rename`, { root: directory, files }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/files/saveFileContents.ts b/resources/scripts/api/server/files/saveFileContents.ts deleted file mode 100644 index b97e60a6b..000000000 --- a/resources/scripts/api/server/files/saveFileContents.ts +++ /dev/null @@ -1,10 +0,0 @@ -import http from '@/api/http'; - -export default async (uuid: string, file: string, content: string): Promise => { - await http.post(`/api/client/servers/${uuid}/files/write`, content, { - params: { file }, - headers: { - 'Content-Type': 'text/plain', - }, - }); -}; diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts deleted file mode 100644 index ff0abc026..000000000 --- a/resources/scripts/api/server/getServer.ts +++ /dev/null @@ -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); - }); -}; diff --git a/resources/scripts/api/server/getServerResourceUsage.ts b/resources/scripts/api/server/getServerResourceUsage.ts deleted file mode 100644 index 200ceb017..000000000 --- a/resources/scripts/api/server/getServerResourceUsage.ts +++ /dev/null @@ -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 => { - 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); - }); -}; diff --git a/resources/scripts/api/server/getWebsocketToken.ts b/resources/scripts/api/server/getWebsocketToken.ts deleted file mode 100644 index 4769e188b..000000000 --- a/resources/scripts/api/server/getWebsocketToken.ts +++ /dev/null @@ -1,19 +0,0 @@ -import http from '@/api/http'; - -interface Response { - token: string; - socket: string; -} - -export default (server: string): Promise => { - 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); - }); -}; diff --git a/resources/scripts/api/server/network/createServerAllocation.ts b/resources/scripts/api/server/network/createServerAllocation.ts deleted file mode 100644 index 3e94cf7f6..000000000 --- a/resources/scripts/api/server/network/createServerAllocation.ts +++ /dev/null @@ -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 => { - const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`); - - return rawDataToServerAllocation(data); -}; diff --git a/resources/scripts/api/server/network/deleteServerAllocation.ts b/resources/scripts/api/server/network/deleteServerAllocation.ts deleted file mode 100644 index 6e45e1d3d..000000000 --- a/resources/scripts/api/server/network/deleteServerAllocation.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Allocation } from '@/api/server/getServer'; -import http from '@/api/http'; - -export default async (uuid: string, id: number): Promise => - await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`); diff --git a/resources/scripts/api/server/network/setPrimaryServerAllocation.ts b/resources/scripts/api/server/network/setPrimaryServerAllocation.ts deleted file mode 100644 index 27c09b722..000000000 --- a/resources/scripts/api/server/network/setPrimaryServerAllocation.ts +++ /dev/null @@ -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 => { - const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}/primary`); - - return rawDataToServerAllocation(data); -}; diff --git a/resources/scripts/api/server/network/setServerAllocationNotes.ts b/resources/scripts/api/server/network/setServerAllocationNotes.ts deleted file mode 100644 index 4531dc751..000000000 --- a/resources/scripts/api/server/network/setServerAllocationNotes.ts +++ /dev/null @@ -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 => { - const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}`, { notes }); - - return rawDataToServerAllocation(data); -}; diff --git a/resources/scripts/api/server/reinstallServer.ts b/resources/scripts/api/server/reinstallServer.ts deleted file mode 100644 index 5cb2ca5e7..000000000 --- a/resources/scripts/api/server/reinstallServer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/settings/reinstall`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/renameServer.ts b/resources/scripts/api/server/renameServer.ts deleted file mode 100644 index 4622f9d4d..000000000 --- a/resources/scripts/api/server/renameServer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, name: string, description?: string): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/settings/rename`, { name, description }) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts b/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts deleted file mode 100644 index d864dee59..000000000 --- a/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules'; -import http from '@/api/http'; - -type Data = Pick & { id?: number }; - -export default async (uuid: string, schedule: Data): Promise => { - 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); -}; diff --git a/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts b/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts deleted file mode 100644 index 388d8d6d5..000000000 --- a/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts +++ /dev/null @@ -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 => { - 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); -}; diff --git a/resources/scripts/api/server/schedules/deleteSchedule.ts b/resources/scripts/api/server/schedules/deleteSchedule.ts deleted file mode 100644 index c3669988d..000000000 --- a/resources/scripts/api/server/schedules/deleteSchedule.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, schedule: number): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/servers/${uuid}/schedules/${schedule}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/schedules/deleteScheduleTask.ts b/resources/scripts/api/server/schedules/deleteScheduleTask.ts deleted file mode 100644 index 8867677b2..000000000 --- a/resources/scripts/api/server/schedules/deleteScheduleTask.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, scheduleId: number, taskId: number): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/schedules/getServerSchedule.ts b/resources/scripts/api/server/schedules/getServerSchedule.ts deleted file mode 100644 index 537124bd6..000000000 --- a/resources/scripts/api/server/schedules/getServerSchedule.ts +++ /dev/null @@ -1,14 +0,0 @@ -import http from '@/api/http'; -import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules'; - -export default (uuid: string, schedule: number): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, { - params: { - include: ['tasks'], - }, - }) - .then(({ data }) => resolve(rawDataToServerSchedule(data.attributes))) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/schedules/getServerSchedules.ts b/resources/scripts/api/server/schedules/getServerSchedules.ts deleted file mode 100644 index aef756a0e..000000000 --- a/resources/scripts/api/server/schedules/getServerSchedules.ts +++ /dev/null @@ -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 => { - const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, { - params: { - include: ['tasks'], - }, - }); - - return (data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes)); -}; diff --git a/resources/scripts/api/server/schedules/triggerScheduleExecution.ts b/resources/scripts/api/server/schedules/triggerScheduleExecution.ts deleted file mode 100644 index 92f7a589f..000000000 --- a/resources/scripts/api/server/schedules/triggerScheduleExecution.ts +++ /dev/null @@ -1,4 +0,0 @@ -import http from '@/api/http'; - -export default async (server: string, schedule: number): Promise => - await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`); diff --git a/resources/scripts/api/server/setSelectedDockerImage.ts b/resources/scripts/api/server/setSelectedDockerImage.ts deleted file mode 100644 index 70042f3a6..000000000 --- a/resources/scripts/api/server/setSelectedDockerImage.ts +++ /dev/null @@ -1,5 +0,0 @@ -import http from '@/api/http'; - -export default async (uuid: string, image: string): Promise => { - await http.put(`/api/client/servers/${uuid}/settings/docker-image`, { docker_image: image }); -}; diff --git a/resources/scripts/api/server/types.d.ts b/resources/scripts/api/server/types.d.ts deleted file mode 100644 index c4a01e921..000000000 --- a/resources/scripts/api/server/types.d.ts +++ /dev/null @@ -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[]; -} diff --git a/resources/scripts/api/server/updateStartupVariable.ts b/resources/scripts/api/server/updateStartupVariable.ts deleted file mode 100644 index 510217282..000000000 --- a/resources/scripts/api/server/updateStartupVariable.ts +++ /dev/null @@ -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]; -}; diff --git a/resources/scripts/api/server/users/createOrUpdateSubuser.ts b/resources/scripts/api/server/users/createOrUpdateSubuser.ts deleted file mode 100644 index 019d35c7a..000000000 --- a/resources/scripts/api/server/users/createOrUpdateSubuser.ts +++ /dev/null @@ -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 => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, { - ...params, - }) - .then((data) => resolve(rawDataToServerSubuser(data.data))) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/users/deleteSubuser.ts b/resources/scripts/api/server/users/deleteSubuser.ts deleted file mode 100644 index dccd98e69..000000000 --- a/resources/scripts/api/server/users/deleteSubuser.ts +++ /dev/null @@ -1,9 +0,0 @@ -import http from '@/api/http'; - -export default (uuid: string, userId: string): Promise => { - return new Promise((resolve, reject) => { - http.delete(`/api/client/servers/${uuid}/users/${userId}`) - .then(() => resolve()) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/users/getServerSubusers.ts b/resources/scripts/api/server/users/getServerSubusers.ts deleted file mode 100644 index dae2ce580..000000000 --- a/resources/scripts/api/server/users/getServerSubusers.ts +++ /dev/null @@ -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 => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/users`) - .then(({ data }) => resolve((data.data || []).map(rawDataToServerSubuser))) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/swr/getServerAllocations.ts b/resources/scripts/api/swr/getServerAllocations.ts deleted file mode 100644 index b254b6505..000000000 --- a/resources/scripts/api/swr/getServerAllocations.ts +++ /dev/null @@ -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( - ['server:allocations', uuid], - async () => { - const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`); - - return (data.data || []).map(rawDataToServerAllocation); - }, - { revalidateOnFocus: false, revalidateOnMount: false } - ); -}; diff --git a/resources/scripts/api/swr/getServerBackups.ts b/resources/scripts/api/swr/getServerBackups.ts deleted file mode 100644 index dbc7f5d98..000000000 --- a/resources/scripts/api/swr/getServerBackups.ts +++ /dev/null @@ -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({ page: 1, setPage: () => 1 }); - -type BackupResponse = PaginatedResult & { backupCount: number }; - -export default () => { - const { page } = useContext(Context); - const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); - - return useSWR(['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, - }; - }); -}; diff --git a/resources/scripts/api/swr/getServerStartup.ts b/resources/scripts/api/swr/getServerStartup.ts deleted file mode 100644 index efecefe08..000000000 --- a/resources/scripts/api/swr/getServerStartup.ts +++ /dev/null @@ -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; -} - -export default (uuid: string, initialData?: Response | null, config?: ConfigInterface) => - useSWR( - [uuid, '/startup'], - async (): Promise => { - 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 || {}) } - ); diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts deleted file mode 100644 index 72d04c846..000000000 --- a/resources/scripts/api/transformers.ts +++ /dev/null @@ -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('|'), -}); diff --git a/resources/scripts/assets/css/GlobalStylesheet.ts b/resources/scripts/assets/css/GlobalStylesheet.ts deleted file mode 100644 index 7f520be45..000000000 --- a/resources/scripts/assets/css/GlobalStylesheet.ts +++ /dev/null @@ -1,66 +0,0 @@ -import tw from 'twin.macro'; -import { createGlobalStyle } from 'styled-components/macro'; - -export default createGlobalStyle` - body { - ${tw`font-sans bg-neutral-800 text-neutral-200`}; - letter-spacing: 0.015em; - } - - h1, h2, h3, h4, h5, h6 { - ${tw`font-medium tracking-normal font-header`}; - } - - p { - ${tw`text-neutral-200 leading-snug font-sans`}; - } - - form { - ${tw`m-0`}; - } - - textarea, select, input, button, button:focus, button:focus-visible { - ${tw`outline-none`}; - } - - input[type=number]::-webkit-outer-spin-button, - input[type=number]::-webkit-inner-spin-button { - -webkit-appearance: none !important; - margin: 0; - } - - input[type=number] { - -moz-appearance: textfield !important; - } - - /* Scroll Bar Style */ - ::-webkit-scrollbar { - background: none; - width: 16px; - height: 16px; - } - - ::-webkit-scrollbar-thumb { - border: solid 0 rgb(0 0 0 / 0%); - border-right-width: 4px; - border-left-width: 4px; - -webkit-border-radius: 9px 4px; - -webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%); - } - - ::-webkit-scrollbar-track-piece { - margin: 4px 0; - } - - ::-webkit-scrollbar-thumb:horizontal { - border-right-width: 0; - border-left-width: 0; - border-top-width: 4px; - border-bottom-width: 4px; - -webkit-border-radius: 4px 9px; - } - - ::-webkit-scrollbar-corner { - background: transparent; - } -`; diff --git a/resources/scripts/assets/images/not_found.svg b/resources/scripts/assets/images/not_found.svg deleted file mode 100644 index 222a4152e..000000000 --- a/resources/scripts/assets/images/not_found.svg +++ /dev/null @@ -1 +0,0 @@ -not found \ No newline at end of file diff --git a/resources/scripts/assets/images/server_error.svg b/resources/scripts/assets/images/server_error.svg deleted file mode 100644 index 726fa106d..000000000 --- a/resources/scripts/assets/images/server_error.svg +++ /dev/null @@ -1 +0,0 @@ -server down \ No newline at end of file diff --git a/resources/scripts/assets/images/server_installing.svg b/resources/scripts/assets/images/server_installing.svg deleted file mode 100644 index d2a0ae48b..000000000 --- a/resources/scripts/assets/images/server_installing.svg +++ /dev/null @@ -1 +0,0 @@ -uploading \ No newline at end of file diff --git a/resources/scripts/assets/images/server_restore.svg b/resources/scripts/assets/images/server_restore.svg deleted file mode 100644 index ce36a8d44..000000000 --- a/resources/scripts/assets/images/server_restore.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/scripts/assets/tailwind.css b/resources/scripts/assets/tailwind.css deleted file mode 100644 index b5c61c956..000000000 --- a/resources/scripts/assets/tailwind.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx deleted file mode 100644 index 8a211b463..000000000 --- a/resources/scripts/components/App.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { lazy } from 'react'; -import { hot } from 'react-hot-loader/root'; -import { Route, Router, Switch } from 'react-router-dom'; -import { StoreProvider } from 'easy-peasy'; -import { store } from '@/state'; -import { SiteSettings } from '@/state/settings'; -import ProgressBar from '@/components/elements/ProgressBar'; -import { NotFound } from '@/components/elements/ScreenBlock'; -import tw from 'twin.macro'; -import GlobalStylesheet from '@/assets/css/GlobalStylesheet'; -import { history } from '@/components/history'; -import { setupInterceptors } from '@/api/interceptors'; -import AuthenticatedRoute from '@/components/elements/AuthenticatedRoute'; -import { ServerContext } from '@/state/server'; -import '@/assets/tailwind.css'; -import Spinner from '@/components/elements/Spinner'; - -const DashboardRouter = lazy(() => import(/* webpackChunkName: "dashboard" */ '@/routers/DashboardRouter')); -const ServerRouter = lazy(() => import(/* webpackChunkName: "server" */ '@/routers/ServerRouter')); -const AuthenticationRouter = lazy(() => import(/* webpackChunkName: "auth" */ '@/routers/AuthenticationRouter')); - -interface ExtendedWindow extends Window { - SiteConfiguration?: SiteSettings; - PanelUser?: { - uuid: string; - username: string; - email: string; - /* eslint-disable camelcase */ - root_admin: boolean; - admin: boolean; - use_totp: boolean; - language: string; - updated_at: string; - created_at: string; - /* eslint-enable camelcase */ - }; -} - -setupInterceptors(history); - -const App = () => { - const { PanelUser, SiteConfiguration } = window as ExtendedWindow; - if (PanelUser && !store.getState().user.data) { - store.getActions().user.setUserData({ - uuid: PanelUser.uuid, - username: PanelUser.username, - email: PanelUser.email, - language: PanelUser.language, - rootAdmin: PanelUser.root_admin, - admin: PanelUser.admin, - useTotp: PanelUser.use_totp, - createdAt: new Date(PanelUser.created_at), - updatedAt: new Date(PanelUser.updated_at), - }); - } - - if (!store.getState().settings.data) { - store.getActions().settings.setSettings(SiteConfiguration!); - } - - return ( - <> - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - ); -}; - -export default hot(App); diff --git a/resources/scripts/components/Avatar.tsx b/resources/scripts/components/Avatar.tsx deleted file mode 100644 index 4d9d254db..000000000 --- a/resources/scripts/components/Avatar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import BoringAvatar, { AvatarProps } from 'boring-avatars'; -import { useStoreState } from '@/state/hooks'; - -const palette = ['#FFAD08', '#EDD75A', '#73B06F', '#0C8F8F', '#587291']; - -type Props = Omit; - -const _Avatar = ({ variant = 'beam', ...props }: AvatarProps) => ( - -); - -const _UserAvatar = ({ variant = 'beam', ...props }: Omit) => { - const uuid = useStoreState((state) => state.user.data?.uuid); - - return ; -}; - -_Avatar.displayName = 'Avatar'; -_UserAvatar.displayName = 'Avatar.User'; - -const Avatar = Object.assign(_Avatar, { - User: _UserAvatar, -}); - -export default Avatar; diff --git a/resources/scripts/components/FlashMessageRender.tsx b/resources/scripts/components/FlashMessageRender.tsx deleted file mode 100644 index 8d0b43f2b..000000000 --- a/resources/scripts/components/FlashMessageRender.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import MessageBox from '@/components/MessageBox'; -import { useStoreState } from 'easy-peasy'; -import tw from 'twin.macro'; - -type Props = Readonly<{ - byKey?: string; - className?: string; -}>; - -const FlashMessageRender = ({ byKey, className }: Props) => { - const flashes = useStoreState((state) => - state.flashes.items.filter((flash) => (byKey ? flash.key === byKey : true)) - ); - - return flashes.length ? ( -
- {flashes.map((flash, index) => ( - - {index > 0 &&
} - - {flash.message} - -
- ))} -
- ) : null; -}; - -export default FlashMessageRender; diff --git a/resources/scripts/components/MessageBox.tsx b/resources/scripts/components/MessageBox.tsx deleted file mode 100644 index 57a5cacb5..000000000 --- a/resources/scripts/components/MessageBox.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import tw, { TwStyle } from 'twin.macro'; -import styled from 'styled-components/macro'; - -export type FlashMessageType = 'success' | 'info' | 'warning' | 'error'; - -interface Props { - title?: string; - children: string; - type?: FlashMessageType; -} - -const styling = (type?: FlashMessageType): TwStyle | string => { - switch (type) { - case 'error': - return tw`bg-red-600 border-red-800`; - case 'info': - return tw`bg-primary-600 border-primary-800`; - case 'success': - return tw`bg-green-600 border-green-800`; - case 'warning': - return tw`bg-yellow-600 border-yellow-800`; - default: - return ''; - } -}; - -const getBackground = (type?: FlashMessageType): TwStyle | string => { - switch (type) { - case 'error': - return tw`bg-red-500`; - case 'info': - return tw`bg-primary-500`; - case 'success': - return tw`bg-green-500`; - case 'warning': - return tw`bg-yellow-500`; - default: - return ''; - } -}; - -const Container = styled.div<{ $type?: FlashMessageType }>` - ${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`}; - ${(props) => styling(props.$type)}; -`; -Container.displayName = 'MessageBox.Container'; - -const MessageBox = ({ title, children, type }: Props) => ( - - {title && ( - - {title} - - )} - {children} - -); -MessageBox.displayName = 'MessageBox'; - -export default MessageBox; diff --git a/resources/scripts/components/NavigationBar.tsx b/resources/scripts/components/NavigationBar.tsx deleted file mode 100644 index 63f49f6f8..000000000 --- a/resources/scripts/components/NavigationBar.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import { Link, NavLink } from 'react-router-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCogs, faHandSparkles, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; -import { useStoreState } from 'easy-peasy'; -import { useTranslation } from 'react-i18next'; -import { ApplicationStore } from '@/state'; -import SearchContainer from '@/components/dashboard/search/SearchContainer'; -import tw, { theme } from 'twin.macro'; -import styled from 'styled-components/macro'; -import http from '@/api/http'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import Tooltip from '@/components/elements/tooltip/Tooltip'; -import Avatar from '@/components/Avatar'; - -const RightNavigation = styled.div` - & > a, - & > button, - & > .navigation-link { - ${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`}; - - &:active, - &:hover { - ${tw`text-neutral-100 bg-black`}; - } - - &:active, - &:hover, - &.active { - box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()}; - } - } -`; - -export default () => { - const { t } = useTranslation('strings'); - - const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); - const isAdmin = useStoreState((state: ApplicationStore) => state.user.data!.admin); - const [isLoggingOut, setIsLoggingOut] = useState(false); - - const onTriggerLogout = () => { - setIsLoggingOut(true); - http.post('/auth/logout').finally(() => { - // @ts-expect-error this is valid - window.location = '/'; - }); - }; - - return ( -
- -
-
- - {name} - -
- - - - - - - - ('dashboard')}> - - - - - {isAdmin && ( - ('admin')}> - - - - - )} - ('account_settings')}> - - - - - - - ('sign_out')}> - - - -
-
- ); -}; diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx deleted file mode 100644 index ce3149d49..000000000 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; -import { httpErrorToHuman } from '@/api/http'; -import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { useStoreState } from 'easy-peasy'; -import Field from '@/components/elements/Field'; -import { Formik, FormikHelpers } from 'formik'; -import { object, string } from 'yup'; -import { useTranslation } from 'react-i18next'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; -import Turnstile, { useTurnstile } from 'react-turnstile'; -import useFlash from '@/plugins/useFlash'; - -interface Values { - email: string; -} - -export default () => { - const { t } = useTranslation('auth'); - - const turnstile = useTurnstile(); - const [token, setToken] = useState(''); - - const { clearFlashes, addFlash, addError } = useFlash(); - const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha); - - useEffect(() => { - clearFlashes(); - }, []); - - const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers) => { - clearFlashes(); - - if (recaptchaEnabled && !token) { - addError({ message: 'No captcha token found.' }); - - setSubmitting(false); - return; - } - - requestPasswordResetEmail(email, token) - .then((response) => { - resetForm(); - addFlash({ type: 'success', title: 'Success', message: response }); - }) - .catch((error) => { - console.error(error); - addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); - }) - .then(() => { - setToken(''); - turnstile.reset(); - - setSubmitting(false); - }); - }; - - return ( - - {({ isSubmitting, setSubmitting }) => ( - - - {recaptchaEnabled && ( - { - setToken(token); - }} - onError={(error) => { - console.error('Error verifying captcha: ' + error); - addError({ message: 'Error verifying captcha: ' + error }); - - setSubmitting(false); - setToken(''); - }} - onExpire={() => { - setSubmitting(false); - setToken(''); - }} - /> - )} -
- -
-
- - {t('return_to_login')} - -
-
- )} -
- ); -}; diff --git a/resources/scripts/components/auth/LoginCheckpointContainer.tsx b/resources/scripts/components/auth/LoginCheckpointContainer.tsx deleted file mode 100644 index 5de3a3b4b..000000000 --- a/resources/scripts/components/auth/LoginCheckpointContainer.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useState } from 'react'; -import { Link, RouteComponentProps } from 'react-router-dom'; -import loginCheckpoint from '@/api/auth/loginCheckpoint'; -import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { ActionCreator } from 'easy-peasy'; -import { StaticContext } from 'react-router'; -import { useFormikContext, withFormik } from 'formik'; -import { useTranslation } from 'react-i18next'; -import useFlash from '@/plugins/useFlash'; -import { FlashStore } from '@/state/flashes'; -import Field from '@/components/elements/Field'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; - -interface Values { - code: string; - recoveryCode: ''; -} - -type OwnProps = RouteComponentProps, StaticContext, { token?: string }>; - -type Props = OwnProps & { - clearAndAddHttpError: ActionCreator; -}; - -const LoginCheckpointContainer = () => { - const { t } = useTranslation('auth'); - - const { isSubmitting, setFieldValue } = useFormikContext(); - const [isMissingDevice, setIsMissingDevice] = useState(false); - - return ( - -
- -
-
- -
-
- { - setFieldValue('code', ''); - setFieldValue('recoveryCode', ''); - setIsMissingDevice((s) => !s); - }} - css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`} - > - {!isMissingDevice ? t('checkpoint.lost_device') : t('checkpoint.have_device')} - -
-
- - {t('return_to_login')} - -
-
- ); -}; - -const EnhancedForm = withFormik({ - handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => { - loginCheckpoint(location.state?.token || '', code, recoveryCode) - .then((response) => { - if (response.complete) { - // @ts-expect-error this is valid - window.location = response.intended || '/'; - return; - } - - setSubmitting(false); - }) - .catch((error) => { - console.error(error); - setSubmitting(false); - clearAndAddHttpError({ error }); - }); - }, - - mapPropsToValues: () => ({ - code: '', - recoveryCode: '', - }), -})(LoginCheckpointContainer); - -export default ({ history, location, ...props }: OwnProps) => { - const { clearAndAddHttpError } = useFlash(); - - if (!location.state?.token) { - history.replace('/auth/login'); - - return null; - } - - return ( - - ); -}; diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx deleted file mode 100644 index ce84f0274..000000000 --- a/resources/scripts/components/auth/LoginContainer.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Link, RouteComponentProps } from 'react-router-dom'; -import login from '@/api/auth/login'; -import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { useStoreState } from 'easy-peasy'; -import { Formik, FormikHelpers } from 'formik'; -import { object, string } from 'yup'; -import { useTranslation } from 'react-i18next'; -import Field from '@/components/elements/Field'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; -import Turnstile, { useTurnstile } from 'react-turnstile'; -import useFlash from '@/plugins/useFlash'; - -interface Values { - username: string; - password: string; -} - -const LoginContainer = ({ history }: RouteComponentProps) => { - const { t } = useTranslation(['auth', 'strings']); - - const turnstile = useTurnstile(); - const [token, setToken] = useState(''); - - const { clearFlashes, clearAndAddHttpError, addError } = useFlash(); - const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha); - - useEffect(() => { - clearFlashes(); - }, []); - - const onSubmit = (values: Values, { setSubmitting }: FormikHelpers) => { - clearFlashes(); - - if (recaptchaEnabled && !token) { - addError({ message: 'No captcha token found.' }); - - setSubmitting(false); - return; - } - - login({ ...values, recaptchaData: token }) - .then((response) => { - if (response.complete) { - // @ts-expect-error this is valid - window.location = response.intended || '/'; - return; - } - - history.replace('/auth/login/checkpoint', { token: response.confirmationToken }); - }) - .catch((error) => { - setSubmitting(false); - addError({ message: 'Invalid login, please try again.' }); - - setToken(''); - if (turnstile) { - turnstile.reset(); - } - - setSubmitting(false); - clearAndAddHttpError({ error }); - }); - }; - - return ( - - {({ isSubmitting, setSubmitting }) => ( - - -
- -
- {recaptchaEnabled && ( - { - setToken(token); - }} - onError={(error) => { - console.error('Error verifying captcha: ' + error); - addError({ message: 'Error verifying captcha: ' + error }); - - setSubmitting(false); - setToken(''); - }} - onExpire={() => { - setSubmitting(false); - setToken(''); - }} - /> - )} -
- -
-
- - {t('forgot_password.label')} - -
-
- )} -
- ); -}; - -export default LoginContainer; diff --git a/resources/scripts/components/auth/LoginFormContainer.tsx b/resources/scripts/components/auth/LoginFormContainer.tsx deleted file mode 100644 index 4e4db201e..000000000 --- a/resources/scripts/components/auth/LoginFormContainer.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { forwardRef } from 'react'; -import { Form } from 'formik'; -import styled from 'styled-components/macro'; -import { breakpoint } from '@/theme'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import tw from 'twin.macro'; - -type Props = React.DetailedHTMLProps, HTMLFormElement> & { - title?: string; -}; - -const Container = styled.div` - ${breakpoint('sm')` - ${tw`w-4/5 mx-auto`} - `}; - - ${breakpoint('md')` - ${tw`p-10`} - `}; - - ${breakpoint('lg')` - ${tw`w-3/5`} - `}; - - ${breakpoint('xl')` - ${tw`w-full`} - max-width: 700px; - `}; -`; - -export default forwardRef(({ title, ...props }, ref) => ( - - {title &&

{title}

} - -
-
-
- -
-
{props.children}
-
-
-

- Pelican © 2024 - {new Date().getFullYear()}  -

-
-)); diff --git a/resources/scripts/components/auth/ResetPasswordContainer.tsx b/resources/scripts/components/auth/ResetPasswordContainer.tsx deleted file mode 100644 index 8546bdbd4..000000000 --- a/resources/scripts/components/auth/ResetPasswordContainer.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useState } from 'react'; -import { RouteComponentProps } from 'react-router'; -import { Link } from 'react-router-dom'; -import performPasswordReset from '@/api/auth/performPasswordReset'; -import { httpErrorToHuman } from '@/api/http'; -import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import { Formik, FormikHelpers } from 'formik'; -import { object, ref, string } from 'yup'; -import { useTranslation } from 'react-i18next'; -import Field from '@/components/elements/Field'; -import Input from '@/components/elements/Input'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; - -interface Values { - password: string; - passwordConfirmation: string; -} - -export default ({ match, location }: RouteComponentProps<{ token: string }>) => { - const { t } = useTranslation('auth'); - - const [email, setEmail] = useState(''); - - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); - - const parsed = new URLSearchParams(location.search); - if (email.length === 0 && parsed.get('email')) { - setEmail(parsed.get('email') || ''); - } - - const submit = ({ password, passwordConfirmation }: Values, { setSubmitting }: FormikHelpers) => { - clearFlashes(); - performPasswordReset(email, { token: match.params.token, password, passwordConfirmation }) - .then(() => { - // @ts-expect-error this is valid - window.location = '/'; - }) - .catch((error) => { - console.error(error); - - setSubmitting(false); - addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); - }); - }; - - return ( - - {({ isSubmitting }) => ( - -
- - -
-
- -
-
- -
-
- -
-
- - {t('return_to_login')} - -
-
- )} -
- ); -}; diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx deleted file mode 100644 index 0cd521187..000000000 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import ContentBox from '@/components/elements/ContentBox'; -import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm'; -import getApiKeys, { ApiKey } from '@/api/account/getApiKeys'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; -import deleteApiKey from '@/api/account/deleteApiKey'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { format } from 'date-fns'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import tw from 'twin.macro'; -import { useTranslation } from 'react-i18next'; -import GreyRowBox from '@/components/elements/GreyRowBox'; -import { Dialog } from '@/components/elements/dialog'; -import { useFlashKey } from '@/plugins/useFlash'; -import Code from '@/components/elements/Code'; - -export default () => { - const { t } = useTranslation(['dashboard/account', 'strings']); - - const [deleteIdentifier, setDeleteIdentifier] = useState(''); - const [keys, setKeys] = useState([]); - const [loading, setLoading] = useState(true); - const { clearAndAddHttpError } = useFlashKey('account'); - - useEffect(() => { - getApiKeys() - .then((keys) => setKeys(keys)) - .then(() => setLoading(false)) - .catch((error) => clearAndAddHttpError(error)); - }, []); - - const doDeletion = (identifier: string) => { - setLoading(true); - - clearAndAddHttpError(); - deleteApiKey(identifier) - .then(() => setKeys((s) => [...(s || []).filter((key) => key.identifier !== identifier)])) - .catch((error) => clearAndAddHttpError(error)) - .then(() => { - setLoading(false); - setDeleteIdentifier(''); - }); - }; - - return ( - - -
- - setKeys((s) => [...s!, key])} /> - - - - setDeleteIdentifier('')} - onConfirmed={() => doDeletion(deleteIdentifier)} - > - All requests using the {deleteIdentifier} key will be invalidated. - - {keys.length === 0 ? ( -

- {loading ? t('loading', { ns: 'strings' }) : 'No API keys exist for this account.'} -

- ) : ( - keys.map((key, index) => ( - 0 && tw`mt-2`]} - > - -
-

{key.description}

-

- {t('last_used', { ns: 'strings' })}:  - {key.lastUsedAt - ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') - : t('never', { ns: 'strings' })} -

-
- - -
- )) - )} -
-
-
- ); -}; diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx deleted file mode 100644 index 7ccf64613..000000000 --- a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import ContentBox from '@/components/elements/ContentBox'; -import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm'; -import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm'; -import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import tw from 'twin.macro'; -import { breakpoint } from '@/theme'; -import styled from 'styled-components/macro'; -import MessageBox from '@/components/MessageBox'; -import { useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -const Container = styled.div` - ${tw`flex flex-wrap`}; - - & > div { - ${tw`w-full`}; - - ${breakpoint('sm')` - width: calc(50% - 1rem); - `} - - ${breakpoint('md')` - ${tw`w-auto flex-1`}; - `} - } -`; - -export default () => { - const { t } = useTranslation('dashboard/account'); - const { state } = useLocation(); - - return ( - - {state?.twoFactorRedirect && ( - - {t('two_factor.required.description')} - - )} - - - - - - - - - - - - - - ); -}; diff --git a/resources/scripts/components/dashboard/ApiKeyModal.tsx b/resources/scripts/components/dashboard/ApiKeyModal.tsx deleted file mode 100644 index 6d01029e2..000000000 --- a/resources/scripts/components/dashboard/ApiKeyModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useContext } from 'react'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; -import asModal from '@/hoc/asModal'; -import ModalContext from '@/context/ModalContext'; -import CopyOnClick from '@/components/elements/CopyOnClick'; - -interface Props { - apiKey: string; -} - -const ApiKeyModal = ({ apiKey }: Props) => { - const { dismiss } = useContext(ModalContext); - - return ( - <> -

Your API Key

-

- The API key you have requested is shown below. Please store this in a safe location, it will not be - shown again. -

-
-                
-                    {apiKey}
-                
-            
-
- -
- - ); -}; - -ApiKeyModal.displayName = 'ApiKeyModal'; - -export default asModal({ - closeOnEscape: false, - closeOnBackground: false, -})(ApiKeyModal); diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx deleted file mode 100644 index 7f20383a0..000000000 --- a/resources/scripts/components/dashboard/DashboardContainer.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Server } from '@/api/server/getServer'; -import getServers from '@/api/getServers'; -import ServerRow from '@/components/dashboard/ServerRow'; -import Spinner from '@/components/elements/Spinner'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import useFlash from '@/plugins/useFlash'; -import { useStoreState } from 'easy-peasy'; -import { usePersistedState } from '@/plugins/usePersistedState'; -import Switch from '@/components/elements/Switch'; -import tw from 'twin.macro'; -import useSWR from 'swr'; -import { PaginatedResult } from '@/api/http'; -import Pagination from '@/components/elements/Pagination'; -import { useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -export default () => { - const { t } = useTranslation('dashboard/index'); - - const { search } = useLocation(); - const defaultPage = Number(new URLSearchParams(search).get('page') || '1'); - - const [page, setPage] = useState(!isNaN(defaultPage) && defaultPage > 0 ? defaultPage : 1); - const { clearFlashes, clearAndAddHttpError } = useFlash(); - const uuid = useStoreState((state) => state.user.data!.uuid); - const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin); - const [showOnlyAdmin, setShowOnlyAdmin] = usePersistedState(`${uuid}:show_all_servers`, false); - - const { data: servers, error } = useSWR>( - ['/api/client/servers', showOnlyAdmin && rootAdmin, page], - () => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined }) - ); - - useEffect(() => { - if (!servers) return; - if (servers.pagination.currentPage > 1 && !servers.items.length) { - setPage(1); - } - }, [servers?.pagination.currentPage]); - - useEffect(() => { - // Don't use react-router to handle changing this part of the URL, otherwise it - // triggers a needless re-render. We just want to track this in the URL incase the - // user refreshes the page. - window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`); - }, [page]); - - useEffect(() => { - if (error) clearAndAddHttpError({ key: 'dashboard', error }); - if (!error) clearFlashes('dashboard'); - }, [error]); - - return ( - - {rootAdmin && ( -
-

- {showOnlyAdmin ? t('showing-others-servers') : t('showing-your-servers')} -

- setShowOnlyAdmin((s) => !s)} - /> -
- )} - {!servers ? ( - - ) : ( - - {({ items }) => - items.length > 0 ? ( - items.map((server, index) => ( - 0 ? tw`mt-2` : undefined} /> - )) - ) : ( -

- {showOnlyAdmin ? t('no-other-servers') : t('no-servers-associated')} -

- ) - } -
- )} -
- ); -}; diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx deleted file mode 100644 index 08637bfc2..000000000 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { memo, useEffect, useRef, useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons'; -import { Link } from 'react-router-dom'; -import { Server } from '@/api/server/getServer'; -import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage'; -import { bytesToString, ip, mbToBytes } from '@/lib/formatters'; -import tw from 'twin.macro'; -import GreyRowBox from '@/components/elements/GreyRowBox'; -import Spinner from '@/components/elements/Spinner'; -import styled from 'styled-components/macro'; -import isEqual from 'react-fast-compare'; - -// Determines if the current value is in an alarm threshold so we can show it in red rather -// than the more faded default style. -const isAlarmState = (current: number, limit: number): boolean => limit > 0 && current / (limit * 1024 * 1024) >= 0.9; - -const Icon = memo( - styled(FontAwesomeIcon)<{ $alarm: boolean }>` - ${(props) => (props.$alarm ? tw`text-red-400` : tw`text-neutral-500`)}; - `, - isEqual -); - -const IconDescription = styled.p<{ $alarm: boolean }>` - ${tw`text-sm ml-2`}; - ${(props) => (props.$alarm ? tw`text-white` : tw`text-neutral-400`)}; -`; - -const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>` - ${tw`grid grid-cols-12 gap-4 relative`}; - - & .status-bar { - ${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`}; - height: calc(100% - 0.5rem); - - ${({ $status }) => - !$status || $status === 'offline' - ? tw`bg-red-500` - : $status === 'running' - ? tw`bg-green-500` - : tw`bg-yellow-500`}; - } - - &:hover .status-bar { - ${tw`opacity-75`}; - } -`; - -type Timer = ReturnType; - -export default ({ server, className }: { server: Server; className?: string }) => { - const interval = useRef(null) as React.MutableRefObject; - const [isSuspended, setIsSuspended] = useState(server.status === 'suspended'); - const [stats, setStats] = useState(null); - - const getStats = () => - getServerResourceUsage(server.uuid) - .then((data) => setStats(data)) - .catch((error) => console.error(error)); - - useEffect(() => { - setIsSuspended(stats?.isSuspended || server.status === 'suspended'); - }, [stats?.isSuspended, server.status]); - - useEffect(() => { - // Don't waste a HTTP request if there is nothing important to show to the user because - // the server is suspended. - if (isSuspended) return; - - getStats().then(() => { - interval.current = setInterval(() => getStats(), 30000); - }); - - return () => { - interval.current && clearInterval(interval.current); - }; - }, [isSuspended]); - - const alarms = { cpu: false, memory: false, disk: false }; - if (stats) { - alarms.cpu = server.limits.cpu === 0 ? false : stats.cpuUsagePercent >= server.limits.cpu * 0.9; - alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory); - alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk); - } - - const diskLimit = server.limits.disk !== 0 ? bytesToString(mbToBytes(server.limits.disk)) : 'Unlimited'; - const memoryLimit = server.limits.memory !== 0 ? bytesToString(mbToBytes(server.limits.memory)) : 'Unlimited'; - const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited'; - - return ( - -
-
- -
-
-

{server.name}

- {!!server.description && ( -

{server.description}

- )} -
-
-
-
- -

- {server.allocations - .filter((alloc) => alloc.isDefault) - .map((allocation) => ( - - {allocation.alias || ip(allocation.ip)}:{allocation.port} - - ))} -

-
-
-
- {!stats || isSuspended ? ( - isSuspended ? ( -
- - {server.status === 'suspended' ? 'Suspended' : 'Connection Error'} - -
- ) : server.isTransferring || server.status ? ( -
- - {server.isTransferring - ? 'Transferring' - : server.status === 'installing' - ? 'Installing' - : server.status === 'restoring_backup' - ? 'Restoring Backup' - : 'Unavailable'} - -
- ) : ( - - ) - ) : ( - -
-
- - - {stats.cpuUsagePercent.toFixed(2)} % - -
-

of {cpuLimit}

-
-
-
- - - {bytesToString(stats.memoryUsageInBytes)} - -
-

of {memoryLimit}

-
-
-
- - - {bytesToString(stats.diskUsageInBytes)} - -
-

of {diskLimit}

-
-
- )} -
-
- - ); -}; diff --git a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx b/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx deleted file mode 100644 index f10701f81..000000000 --- a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity'; -import { useFlashKey } from '@/plugins/useFlash'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { Link } from 'react-router-dom'; -import PaginationFooter from '@/components/elements/table/PaginationFooter'; -import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid'; -import Spinner from '@/components/elements/Spinner'; -import { styles as btnStyles } from '@/components/elements/button/index'; -import classNames from 'classnames'; -import ActivityLogEntry from '@/components/elements/activity/ActivityLogEntry'; -import Tooltip from '@/components/elements/tooltip/Tooltip'; -import useLocationHash from '@/plugins/useLocationHash'; - -export default () => { - const { hash } = useLocationHash(); - const { clearAndAddHttpError } = useFlashKey('account'); - const [filters, setFilters] = useState({ page: 1, sorts: { timestamp: -1 } }); - const { data, isValidating, error } = useActivityLogs(filters, { - revalidateOnMount: true, - revalidateOnFocus: false, - }); - - useEffect(() => { - setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } })); - }, [hash]); - - useEffect(() => { - clearAndAddHttpError(error); - }, [error]); - - return ( - - - {(filters.filters?.event || filters.filters?.ip) && ( -
- setFilters((value) => ({ ...value, filters: {} }))} - > - Clear Filters - -
- )} - {!data && isValidating ? ( - - ) : ( -
- {data?.items.map((activity) => ( - - {typeof activity.properties.useragent === 'string' && ( - - - - - - )} - - ))} -
- )} - {data && ( - setFilters((value) => ({ ...value, page }))} - /> - )} -
- ); -}; diff --git a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx deleted file mode 100644 index fa572b062..000000000 --- a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useStoreState } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import tw from 'twin.macro'; -import { useTranslation } from 'react-i18next'; -import { Button } from '@/components/elements/button/index'; -import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog'; -import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog'; -import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog'; -import { useFlashKey } from '@/plugins/useFlash'; - -export default () => { - const { t } = useTranslation('dashboard/account'); - - const [tokens, setTokens] = useState([]); - const [visible, setVisible] = useState<'enable' | 'disable' | null>(null); - const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp); - const { clearAndAddHttpError } = useFlashKey('account:two-step'); - - useEffect(() => { - return () => { - clearAndAddHttpError(); - }; - }, [visible]); - - const onTokens = (tokens: string[]) => { - setTokens(tokens); - setVisible(null); - }; - - return ( -
- setVisible(null)} onTokens={onTokens} /> - 0} onClose={() => setTokens([])} /> - setVisible(null)} /> -

{isEnabled ? t('two_factor.disable.help') : t('two_factor.enable.help')}

-
- {isEnabled ? ( - setVisible('disable')}> - {t('two_factor.disable.button')} - - ) : ( - - )} -
-
- ); -}; diff --git a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx deleted file mode 100644 index 9c273044c..000000000 --- a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; -import { Field, Form, Formik, FormikHelpers } from 'formik'; -import { object, string } from 'yup'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import createApiKey from '@/api/account/createApiKey'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import { httpErrorToHuman } from '@/api/http'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { ApiKey } from '@/api/account/getApiKeys'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; -import Input, { Textarea } from '@/components/elements/Input'; -import styled from 'styled-components/macro'; -import ApiKeyModal from '@/components/dashboard/ApiKeyModal'; - -interface Values { - description: string; - allowedIps: string; -} - -const CustomTextarea = styled(Textarea)` - ${tw`h-32`} -`; - -export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { - const [apiKey, setApiKey] = useState(''); - const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); - - const submit = (values: Values, { setSubmitting, resetForm }: FormikHelpers) => { - clearFlashes('account'); - createApiKey(values.description, values.allowedIps) - .then(({ secretToken, ...key }) => { - resetForm(); - setSubmitting(false); - setApiKey(`${key.identifier}${secretToken}`); - onKeyCreated(key); - }) - .catch((error) => { - console.error(error); - - addError({ key: 'account', message: httpErrorToHuman(error) }); - setSubmitting(false); - }); - }; - - return ( - <> - 0} onModalDismissed={() => setApiKey('')} apiKey={apiKey} /> - - {({ isSubmitting }) => ( -
- - - - - - - -
- -
- - )} -
- - ); -}; diff --git a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx deleted file mode 100644 index 547bab4ff..000000000 --- a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import asDialog from '@/hoc/asDialog'; -import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; -import { Input } from '@/components/elements/inputs'; -import Tooltip from '@/components/elements/tooltip/Tooltip'; -import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor'; -import { useFlashKey } from '@/plugins/useFlash'; -import { useStoreActions } from '@/state/hooks'; -import FlashMessageRender from '@/components/FlashMessageRender'; - -const DisableTOTPDialog = () => { - const { t } = useTranslation(['dashboard/account', 'strings']); - - const [submitting, setSubmitting] = useState(false); - const [password, setPassword] = useState(''); - const { clearAndAddHttpError } = useFlashKey('account:two-step'); - const { close, setProps } = useContext(DialogWrapperContext); - const updateUserData = useStoreActions((actions) => actions.user.updateUserData); - - useEffect(() => { - setProps((state) => ({ ...state, preventExternalClose: submitting })); - }, [submitting]); - - const submit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (submitting) return; - - setSubmitting(true); - clearAndAddHttpError(); - disableAccountTwoFactor(password) - .then(() => { - updateUserData({ useTotp: false }); - close(); - }) - .catch(clearAndAddHttpError) - .then(() => setSubmitting(false)); - }; - - return ( -
- - - setPassword(e.currentTarget.value)} - /> - - {t('cancel', { ns: 'strings' })} - 0} - content={t('password.validation.account_password')} - > - - {t('disable', { ns: 'strings' })} - - - - - ); -}; - -export default asDialog({ - title: 'Disable Two-Step Verification', - description: 'Disabling two-step verification will make your account less secure.', -})(DisableTOTPDialog); diff --git a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx deleted file mode 100644 index 3e2d0ba20..000000000 --- a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { Dialog, DialogProps } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import { Alert } from '@/components/elements/alert'; - -interface RecoveryTokenDialogProps extends DialogProps { - tokens: string[]; -} - -export default ({ tokens, open, onClose }: RecoveryTokenDialogProps) => { - const grouped = [] as [string, string][]; - tokens.forEach((token, index) => { - if (index % 2 === 0) { - grouped.push([token, tokens[index + 1] || '']); - } - }); - - return ( - - - -
-                    {grouped.map((value) => (
-                        
-                            {value[0]}
-                             
-                            {value[1]}
-                             
-                        
-                    ))}
-                
-
- - These codes will not be shown again. - - - Done - -
- ); -}; diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx deleted file mode 100644 index ec8326aa8..000000000 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; -import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData'; -import { useFlashKey } from '@/plugins/useFlash'; -import tw from 'twin.macro'; -import { useTranslation } from 'react-i18next'; -import QRCode from 'qrcode.react'; -import { Button } from '@/components/elements/button/index'; -import Spinner from '@/components/elements/Spinner'; -import { Input } from '@/components/elements/inputs'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import Tooltip from '@/components/elements/tooltip/Tooltip'; -import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import asDialog from '@/hoc/asDialog'; - -interface Props { - onTokens: (tokens: string[]) => void; -} - -const ConfigureTwoFactorForm = ({ onTokens }: Props) => { - const { t } = useTranslation(['dashboard/account', 'strings']); - - const [submitting, setSubmitting] = useState(false); - const [value, setValue] = useState(''); - const [password, setPassword] = useState(''); - const [token, setToken] = useState(null); - const { clearAndAddHttpError } = useFlashKey('account:two-step'); - const updateUserData = useStoreActions((actions: Actions) => actions.user.updateUserData); - - const { close, setProps } = useContext(DialogWrapperContext); - - useEffect(() => { - getTwoFactorTokenData() - .then(setToken) - .catch((error) => clearAndAddHttpError(error)); - }, []); - - useEffect(() => { - setProps((state) => ({ ...state, preventExternalClose: submitting })); - }, [submitting]); - - const submit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (submitting) return; - - setSubmitting(true); - clearAndAddHttpError(); - enableAccountTwoFactor(value, password) - .then((tokens) => { - updateUserData({ useTotp: true }); - onTokens(tokens); - }) - .catch((error) => { - clearAndAddHttpError(error); - setSubmitting(false); - }); - }; - - return ( -
- -
- {!token ? ( - - ) : ( - - )} -
- -

- {token?.secret.match(/.{1,4}/g)!.join(' ') || t('loading', { ns: 'strings' })} -

-
-

- {t('two_factor.setup.help')} -

- setValue(e.currentTarget.value)} - className={'mt-3'} - placeholder={'000000'} - type={'text'} - inputMode={'numeric'} - autoComplete={'one-time-code'} - pattern={'\\d{6}'} - /> - - setPassword(e.currentTarget.value)} - /> - - {t('cancel', { ns: 'strings' })} - 0 && value.length === 6} - content={ - !token - ? 'Waiting for QR code to load...' - : 'You must enter the 6-digit code and your password to continue.' - } - delay={100} - > - - - - - ); -}; - -export default asDialog({ - title: 'Enable Two-Step Verification', - description: - "Help protect your account from unauthorized access. You'll be prompted for a verification code each time you sign in.", -})(ConfigureTwoFactorForm); diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx deleted file mode 100644 index 7980f0d7a..000000000 --- a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; -import { Form, Formik, FormikHelpers } from 'formik'; -import * as Yup from 'yup'; -import { useTranslation } from 'react-i18next'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import Field from '@/components/elements/Field'; -import { httpErrorToHuman } from '@/api/http'; -import { ApplicationStore } from '@/state'; -import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; - -interface Values { - email: string; - password: string; -} - -export default () => { - const { t } = useTranslation(['dashboard/account', 'strings']); - - const schema = Yup.object().shape({ - email: Yup.string().email().required(), - password: Yup.string().required(t('password.validation.account_password')), - }); - - const user = useStoreState((state: State) => state.user.data); - const updateEmail = useStoreActions((state: Actions) => state.user.updateUserEmail); - - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); - - const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers) => { - clearFlashes('account:email'); - - updateEmail({ ...values }) - .then(() => - addFlash({ - type: 'success', - key: 'account:email', - message: t('email.updated'), - }) - ) - .catch((error) => - addFlash({ - type: 'error', - key: 'account:email', - title: t('error', { ns: 'strings' }), - message: httpErrorToHuman(error), - }) - ) - .then(() => { - resetForm(); - setSubmitting(false); - }); - }; - - return ( - - {({ isSubmitting, isValid }) => ( - - -
- -
- -
-
- -
- -
- )} -
- ); -}; diff --git a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx deleted file mode 100644 index ef15deb2c..000000000 --- a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; -import { Form, Formik, FormikHelpers } from 'formik'; -import Field from '@/components/elements/Field'; -import * as Yup from 'yup'; -import { useTranslation } from 'react-i18next'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import updateAccountPassword from '@/api/account/updateAccountPassword'; -import { httpErrorToHuman } from '@/api/http'; -import { ApplicationStore } from '@/state'; -import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; - -interface Values { - current: string; - password: string; - confirmPassword: string; -} - -export default () => { - const { t } = useTranslation(['dashboard/account', 'strings']); - - const schema = Yup.object().shape({ - current: Yup.string().min(1).required('You must provide your current password.'), - password: Yup.string().min(8).required(), - confirmPassword: Yup.string().test( - 'password', - 'Password confirmation does not match the password you entered.', - function (value) { - return value === this.parent.password; - } - ), - }); - - const user = useStoreState((state: State) => state.user.data); - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); - - if (!user) { - return null; - } - - const submit = (values: Values, { setSubmitting }: FormikHelpers) => { - clearFlashes('account:password'); - updateAccountPassword({ ...values }) - .then(() => { - // @ts-expect-error this is valid - window.location = '/auth/login'; - }) - .catch((error) => - addFlash({ - key: 'account:password', - type: 'error', - title: t('error', { ns: 'strings' }), - message: httpErrorToHuman(error), - }) - ) - .then(() => setSubmitting(false)); - }; - - return ( - - - {({ isSubmitting, isValid }) => ( - - -
- -
- -
-
- -
-
- -
- -
- )} -
-
- ); -}; diff --git a/resources/scripts/components/dashboard/search/SearchContainer.tsx b/resources/scripts/components/dashboard/search/SearchContainer.tsx deleted file mode 100644 index 1ec262128..000000000 --- a/resources/scripts/components/dashboard/search/SearchContainer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSearch } from '@fortawesome/free-solid-svg-icons'; -import useEventListener from '@/plugins/useEventListener'; -import SearchModal from '@/components/dashboard/search/SearchModal'; -import Tooltip from '@/components/elements/tooltip/Tooltip'; -import { useTranslation } from 'react-i18next'; - -export default () => { - const { t } = useTranslation('strings'); - - const [visible, setVisible] = useState(false); - - useEventListener('keydown', (e: KeyboardEvent) => { - if (['input', 'textarea'].indexOf(((e.target as HTMLElement).tagName || 'input').toLowerCase()) < 0) { - if (!visible && e.metaKey && e.key.toLowerCase() === '/') { - setVisible(true); - } - } - }); - - return ( - <> - {visible && setVisible(false)} />} - ('search')}> -
setVisible(true)}> - -
-
- - ); -}; diff --git a/resources/scripts/components/dashboard/search/SearchModal.tsx b/resources/scripts/components/dashboard/search/SearchModal.tsx deleted file mode 100644 index c6512b8e7..000000000 --- a/resources/scripts/components/dashboard/search/SearchModal.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import Modal, { RequiredModalProps } from '@/components/elements/Modal'; -import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik'; -import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; -import { object, string } from 'yup'; -import { useTranslation } from 'react-i18next'; -import debounce from 'debounce'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import InputSpinner from '@/components/elements/InputSpinner'; -import getServers from '@/api/getServers'; -import { Server } from '@/api/server/getServer'; -import { ApplicationStore } from '@/state'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components/macro'; -import tw from 'twin.macro'; -import Input from '@/components/elements/Input'; -import { ip } from '@/lib/formatters'; - -type Props = RequiredModalProps; - -interface Values { - term: string; -} - -const ServerResult = styled(Link)` - ${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline transition-all duration-150`}; - - &:hover { - ${tw`shadow border-cyan-500`}; - } - - &:not(:last-of-type) { - ${tw`mb-2`}; - } -`; - -const SearchWatcher = () => { - const { values, submitForm } = useFormikContext(); - - useEffect(() => { - if (values.term.length >= 3) { - submitForm(); - } - }, [values.term]); - - return null; -}; - -export default ({ ...props }: Props) => { - const { t } = useTranslation('search'); - - const ref = useRef(null); - const isAdmin = useStoreState((state) => state.user.data!.rootAdmin); - const [servers, setServers] = useState([]); - const { clearAndAddHttpError, clearFlashes } = useStoreActions( - (actions: Actions) => actions.flashes - ); - - const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers) => { - clearFlashes('search'); - - // if (ref.current) ref.current.focus(); - getServers({ query: term, type: isAdmin ? 'admin-all' : undefined }) - .then((servers) => setServers(servers.items.filter((_, index) => index < 5))) - .catch((error) => { - console.error(error); - clearAndAddHttpError({ key: 'search', error }); - }) - .then(() => setSubmitting(false)) - .then(() => ref.current?.focus()); - }, 500); - - useEffect(() => { - if (props.visible) { - if (ref.current) ref.current.focus(); - } - }, [props.visible]); - - // Formik does not support an innerRef on custom components. - const InputWithRef = (props: any) => ; - - return ( - - {({ isSubmitting }) => ( - -
- - - - - - -
- {servers.length > 0 && ( -
- {servers.map((server) => ( - props.onDismissed()} - > -
-

{server.name}

-

- {server.allocations - .filter((alloc) => alloc.isDefault) - .map((allocation) => ( - - {allocation.alias || ip(allocation.ip)}:{allocation.port} - - ))} -

-
-
- - {server.node} - -
-
- ))} -
- )} -
- )} -
- ); -}; diff --git a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx b/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx deleted file mode 100644 index 0308b3911..000000000 --- a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useEffect } from 'react'; -import ContentBox from '@/components/elements/ContentBox'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import tw from 'twin.macro'; -import GreyRowBox from '@/components/elements/GreyRowBox'; -import { useSSHKeys } from '@/api/account/ssh-keys'; -import { useFlashKey } from '@/plugins/useFlash'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faKey } from '@fortawesome/free-solid-svg-icons'; -import { format } from 'date-fns'; -import CreateSSHKeyForm from '@/components/dashboard/ssh/CreateSSHKeyForm'; -import DeleteSSHKeyButton from '@/components/dashboard/ssh/DeleteSSHKeyButton'; - -export default () => { - const { clearAndAddHttpError } = useFlashKey('account'); - const { data, isValidating, error } = useSSHKeys({ - revalidateOnMount: true, - revalidateOnFocus: false, - }); - - useEffect(() => { - clearAndAddHttpError(error); - }, [error]); - - return ( - - -
- - - - - - {!data || !data.length ? ( -

- {!data ? 'Loading...' : 'No SSH Keys exist for this account.'} -

- ) : ( - data.map((key, index) => ( - 0 && tw`mt-2`]} - > - -
-

{key.name}

-

SHA256:{key.fingerprint}

-

- Added on:  - {format(key.createdAt, 'MMM do, yyyy HH:mm')} -

-
- -
- )) - )} -
-
-
- ); -}; diff --git a/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx b/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx deleted file mode 100644 index 4b4f39cbd..000000000 --- a/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { Field, Form, Formik, FormikHelpers } from 'formik'; -import { object, string } from 'yup'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import tw from 'twin.macro'; -import Button from '@/components/elements/Button'; -import Input, { Textarea } from '@/components/elements/Input'; -import styled from 'styled-components/macro'; -import { useFlashKey } from '@/plugins/useFlash'; -import { createSSHKey, useSSHKeys } from '@/api/account/ssh-keys'; - -interface Values { - name: string; - publicKey: string; -} - -const CustomTextarea = styled(Textarea)` - ${tw`h-32`} -`; - -export default () => { - const { clearAndAddHttpError } = useFlashKey('account'); - const { mutate } = useSSHKeys(); - - const submit = (values: Values, { setSubmitting, resetForm }: FormikHelpers) => { - clearAndAddHttpError(); - - createSSHKey(values.name, values.publicKey) - .then((key) => { - resetForm(); - mutate((data) => (data || []).concat(key)); - }) - .catch((error) => clearAndAddHttpError(error)) - .then(() => setSubmitting(false)); - }; - - return ( - <> - - {({ isSubmitting }) => ( -
- - - - - - - -
- -
- - )} -
- - ); -}; diff --git a/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx b/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx deleted file mode 100644 index bd9aaf0d5..000000000 --- a/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import tw from 'twin.macro'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'; -import React, { useState } from 'react'; -import { useFlashKey } from '@/plugins/useFlash'; -import { deleteSSHKey, useSSHKeys } from '@/api/account/ssh-keys'; -import { Dialog } from '@/components/elements/dialog'; -import Code from '@/components/elements/Code'; - -export default ({ name, fingerprint }: { name: string; fingerprint: string }) => { - const { clearAndAddHttpError } = useFlashKey('account'); - const [visible, setVisible] = useState(false); - const { mutate } = useSSHKeys(); - - const onClick = () => { - clearAndAddHttpError(); - - Promise.all([ - mutate((data) => data?.filter((value) => value.fingerprint !== fingerprint), false), - deleteSSHKey(fingerprint), - ]).catch((error) => { - mutate(undefined, true).catch(console.error); - clearAndAddHttpError(error); - }); - }; - - return ( - <> - setVisible(false)} - > - Removing the {name} SSH key will invalidate its usage across the Panel. - - - - ); -}; diff --git a/resources/scripts/components/elements/AuthenticatedRoute.tsx b/resources/scripts/components/elements/AuthenticatedRoute.tsx deleted file mode 100644 index 2d3b6a6b9..000000000 --- a/resources/scripts/components/elements/AuthenticatedRoute.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { Redirect, Route, RouteProps } from 'react-router'; -import { useStoreState } from '@/state/hooks'; - -export default ({ children, ...props }: Omit) => { - const isAuthenticated = useStoreState((state) => !!state.user.data?.uuid); - - return ( - - isAuthenticated ? children : - } - /> - ); -}; diff --git a/resources/scripts/components/elements/Button.tsx b/resources/scripts/components/elements/Button.tsx deleted file mode 100644 index e02f00ff1..000000000 --- a/resources/scripts/components/elements/Button.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import styled, { css } from 'styled-components/macro'; -import tw from 'twin.macro'; -import Spinner from '@/components/elements/Spinner'; - -interface Props { - isLoading?: boolean; - size?: 'xsmall' | 'small' | 'large' | 'xlarge'; - color?: 'green' | 'red' | 'primary' | 'grey'; - isSecondary?: boolean; -} - -const ButtonStyle = styled.button>` - ${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`}; - - ${(props) => - ((!props.isSecondary && !props.color) || props.color === 'primary') && - css` - ${(props) => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`}; - - &:hover:not(:disabled) { - ${tw`bg-primary-600 border-primary-700`}; - } - `}; - - ${(props) => - props.color === 'grey' && - css` - ${tw`border-neutral-600 bg-neutral-500 text-neutral-50`}; - - &:hover:not(:disabled) { - ${tw`bg-neutral-600 border-neutral-700`}; - } - `}; - - ${(props) => - props.color === 'green' && - css` - ${tw`border-green-600 bg-green-500 text-green-50`}; - - &:hover:not(:disabled) { - ${tw`bg-green-600 border-green-700`}; - } - - ${(props) => - props.isSecondary && - css` - &:active:not(:disabled) { - ${tw`bg-green-600 border-green-700`}; - } - `}; - `}; - - ${(props) => - props.color === 'red' && - css` - ${tw`border-red-600 bg-red-500 text-red-50`}; - - &:hover:not(:disabled) { - ${tw`bg-red-600 border-red-700`}; - } - - ${(props) => - props.isSecondary && - css` - &:active:not(:disabled) { - ${tw`bg-red-600 border-red-700`}; - } - `}; - `}; - - ${(props) => props.size === 'xsmall' && tw`px-2 py-1 text-xs`}; - ${(props) => (!props.size || props.size === 'small') && tw`px-4 py-2`}; - ${(props) => props.size === 'large' && tw`p-4 text-sm`}; - ${(props) => props.size === 'xlarge' && tw`p-4 w-full`}; - - ${(props) => - props.isSecondary && - css` - ${tw`border-neutral-600 bg-transparent text-neutral-200`}; - - &:hover:not(:disabled) { - ${tw`border-neutral-500 text-neutral-100`}; - ${(props) => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`}; - ${(props) => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`}; - ${(props) => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`}; - } - `}; - - &:disabled { - opacity: 0.55; - cursor: default; - } -`; - -type ComponentProps = Omit & Props; - -const Button: React.FC = ({ children, isLoading, ...props }) => ( - - {isLoading && ( -
- -
- )} - {children} -
-); - -type LinkProps = Omit & Props; - -const LinkButton: React.FC = (props) => ; - -export { LinkButton, ButtonStyle }; -export default Button; diff --git a/resources/scripts/components/elements/Can.tsx b/resources/scripts/components/elements/Can.tsx deleted file mode 100644 index 824051936..000000000 --- a/resources/scripts/components/elements/Can.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { memo } from 'react'; -import { usePermissions } from '@/plugins/usePermissions'; -import isEqual from 'react-fast-compare'; - -interface Props { - action: string | string[]; - matchAny?: boolean; - renderOnError?: React.ReactNode | null; - children: React.ReactNode; -} - -const Can = ({ action, matchAny = false, renderOnError, children }: Props) => { - const can = usePermissions(action); - - return ( - <> - {(matchAny && can.filter((p) => p).length > 0) || (!matchAny && can.every((p) => p)) - ? children - : renderOnError} - - ); -}; - -export default memo(Can, isEqual); diff --git a/resources/scripts/components/elements/Checkbox.tsx b/resources/scripts/components/elements/Checkbox.tsx deleted file mode 100644 index 731fd24de..000000000 --- a/resources/scripts/components/elements/Checkbox.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Field, FieldProps } from 'formik'; -import Input from '@/components/elements/Input'; - -interface Props { - name: string; - value: string; - className?: string; -} - -type OmitFields = 'ref' | 'name' | 'value' | 'type' | 'checked' | 'onClick' | 'onChange'; - -type InputProps = Omit; - -const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => ( - - {({ field, form }: FieldProps) => { - if (!Array.isArray(field.value)) { - console.error('Attempting to mount a checkbox using a field value that is not an array.'); - - return null; - } - - return ( - form.setFieldTouched(field.name, true)} - onChange={(e) => { - const set = new Set(field.value); - set.has(value) ? set.delete(value) : set.add(value); - - field.onChange(e); - form.setFieldValue(field.name, Array.from(set)); - }} - /> - ); - }} - -); - -export default Checkbox; diff --git a/resources/scripts/components/elements/Code.tsx b/resources/scripts/components/elements/Code.tsx deleted file mode 100644 index 30eac0d86..000000000 --- a/resources/scripts/components/elements/Code.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; - -interface CodeProps { - dark?: boolean | undefined; - className?: string; - children: React.ReactChild | React.ReactFragment | React.ReactPortal; -} - -export default ({ dark, className, children }: CodeProps) => ( - - {children} - -); diff --git a/resources/scripts/components/elements/CodemirrorEditor.tsx b/resources/scripts/components/elements/CodemirrorEditor.tsx deleted file mode 100644 index 99a7b5e85..000000000 --- a/resources/scripts/components/elements/CodemirrorEditor.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import CodeMirror from 'codemirror'; -import styled from 'styled-components/macro'; -import tw from 'twin.macro'; -import modes from '@/modes'; - -require('codemirror/lib/codemirror.css'); -require('codemirror/theme/ayu-mirage.css'); -require('codemirror/addon/edit/closebrackets'); -require('codemirror/addon/edit/closetag'); -require('codemirror/addon/edit/matchbrackets'); -require('codemirror/addon/edit/matchtags'); -require('codemirror/addon/edit/trailingspace'); -require('codemirror/addon/fold/foldcode'); -require('codemirror/addon/fold/foldgutter.css'); -require('codemirror/addon/fold/foldgutter'); -require('codemirror/addon/fold/brace-fold'); -require('codemirror/addon/fold/comment-fold'); -require('codemirror/addon/fold/indent-fold'); -require('codemirror/addon/fold/markdown-fold'); -require('codemirror/addon/fold/xml-fold'); -require('codemirror/addon/hint/css-hint'); -require('codemirror/addon/hint/html-hint'); -require('codemirror/addon/hint/javascript-hint'); -require('codemirror/addon/hint/show-hint.css'); -require('codemirror/addon/hint/show-hint'); -require('codemirror/addon/hint/sql-hint'); -require('codemirror/addon/hint/xml-hint'); -require('codemirror/addon/mode/simple'); -require('codemirror/addon/dialog/dialog.css'); -require('codemirror/addon/dialog/dialog'); -require('codemirror/addon/scroll/annotatescrollbar'); -require('codemirror/addon/scroll/scrollpastend'); -require('codemirror/addon/scroll/simplescrollbars.css'); -require('codemirror/addon/scroll/simplescrollbars'); -require('codemirror/addon/search/jump-to-line'); -require('codemirror/addon/search/match-highlighter'); -require('codemirror/addon/search/matchesonscrollbar.css'); -require('codemirror/addon/search/matchesonscrollbar'); -require('codemirror/addon/search/search'); -require('codemirror/addon/search/searchcursor'); - -require('codemirror/mode/brainfuck/brainfuck'); -require('codemirror/mode/clike/clike'); -require('codemirror/mode/css/css'); -require('codemirror/mode/dart/dart'); -require('codemirror/mode/diff/diff'); -require('codemirror/mode/dockerfile/dockerfile'); -require('codemirror/mode/erlang/erlang'); -require('codemirror/mode/gfm/gfm'); -require('codemirror/mode/go/go'); -require('codemirror/mode/handlebars/handlebars'); -require('codemirror/mode/htmlembedded/htmlembedded'); -require('codemirror/mode/htmlmixed/htmlmixed'); -require('codemirror/mode/http/http'); -require('codemirror/mode/javascript/javascript'); -require('codemirror/mode/jsx/jsx'); -require('codemirror/mode/julia/julia'); -require('codemirror/mode/lua/lua'); -require('codemirror/mode/markdown/markdown'); -require('codemirror/mode/nginx/nginx'); -require('codemirror/mode/perl/perl'); -require('codemirror/mode/php/php'); -require('codemirror/mode/properties/properties'); -require('codemirror/mode/protobuf/protobuf'); -require('codemirror/mode/pug/pug'); -require('codemirror/mode/python/python'); -require('codemirror/mode/rpm/rpm'); -require('codemirror/mode/ruby/ruby'); -require('codemirror/mode/rust/rust'); -require('codemirror/mode/sass/sass'); -require('codemirror/mode/shell/shell'); -require('codemirror/mode/smarty/smarty'); -require('codemirror/mode/sql/sql'); -require('codemirror/mode/swift/swift'); -require('codemirror/mode/toml/toml'); -require('codemirror/mode/twig/twig'); -require('codemirror/mode/vue/vue'); -require('codemirror/mode/xml/xml'); -require('codemirror/mode/yaml/yaml'); - -const EditorContainer = styled.div` - min-height: 16rem; - height: calc(100vh - 20rem); - ${tw`relative`}; - - > div { - ${tw`rounded h-full`}; - } - - .CodeMirror { - font-size: 12px; - line-height: 1.375rem; - } - - .CodeMirror-linenumber { - padding: 1px 12px 0 12px !important; - } - - .CodeMirror-foldmarker { - color: #cbccc6; - text-shadow: none; - margin-left: 0.25rem; - margin-right: 0.25rem; - } -`; - -export interface Props { - style?: React.CSSProperties; - initialContent?: string; - mode: string; - filename?: string; - onModeChanged: (mode: string) => void; - fetchContent: (callback: () => Promise) => void; - onContentSaved: () => void; -} - -const findModeByFilename = (filename: string) => { - for (let i = 0; i < modes.length; i++) { - const info = modes[i]; - - if (info.file && info.file.test(filename)) { - return info; - } - } - - const dot = filename.lastIndexOf('.'); - const ext = dot > -1 && filename.substring(dot + 1, filename.length); - - if (ext) { - for (let i = 0; i < modes.length; i++) { - const info = modes[i]; - if (info.ext) { - for (let j = 0; j < info.ext.length; j++) { - if (info.ext[j] === ext) { - return info; - } - } - } - } - } - - return undefined; -}; - -export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { - const [editor, setEditor] = useState(); - - const ref = useCallback((node) => { - if (!node) return; - - const e = CodeMirror.fromTextArea(node, { - mode: 'text/plain', - theme: 'ayu-mirage', - indentUnit: 4, - smartIndent: true, - tabSize: 4, - indentWithTabs: false, - lineWrapping: true, - lineNumbers: true, - foldGutter: true, - fixedGutter: true, - scrollbarStyle: 'overlay', - coverGutterNextToScrollbar: false, - readOnly: false, - showCursorWhenSelecting: false, - autofocus: false, - spellcheck: true, - autocorrect: false, - autocapitalize: false, - lint: false, - // @ts-expect-error this property is actually used, the d.ts file for CodeMirror is incorrect. - autoCloseBrackets: true, - matchBrackets: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - }); - - setEditor(e); - }, []); - - useEffect(() => { - if (filename === undefined) { - return; - } - - onModeChanged(findModeByFilename(filename)?.mime || 'text/plain'); - }, [filename]); - - useEffect(() => { - editor && editor.setOption('mode', mode); - }, [editor, mode]); - - useEffect(() => { - editor && editor.setValue(initialContent || ''); - }, [editor, initialContent]); - - useEffect(() => { - if (!editor) { - fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); - return; - } - - editor.addKeyMap({ - 'Ctrl-S': () => onContentSaved(), - 'Cmd-S': () => onContentSaved(), - }); - - fetchContent(() => Promise.resolve(editor.getValue())); - }, [editor, fetchContent, onContentSaved]); - - return ( - -