diff --git a/ui/package.json b/ui/package.json index 97821988..b20acb24 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,6 @@ "@uiw/codemirror-theme-material": "^4.24.2", "@uiw/react-codemirror": "^4.24.2", "@vitejs/plugin-react": "^5.0.0", - "axios": "^1.11.0", "detect-browser": "^5.3.0", "fractional-indexing": "^3.2.0", "mobx": "^6.13.7", @@ -60,8 +59,7 @@ "rimraf": "^6.0.1", "tree-kill": "^1.2.0", "typescript": "^5.9.2", - "typescript-eslint": "^8.38.0", - "wait-on": "^9.0.0" + "typescript-eslint": "^8.38.0" }, "browserslist": { "production": [ @@ -75,4 +73,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/ui/src/CurrentUser.ts b/ui/src/CurrentUser.ts index 2510de91..95173c72 100644 --- a/ui/src/CurrentUser.ts +++ b/ui/src/CurrentUser.ts @@ -1,9 +1,9 @@ -import axios, {AxiosError, AxiosResponse} from 'axios'; import * as config from './config'; import {detect} from 'detect-browser'; import {SnackReporter} from './snack/SnackManager'; import {observable, runInAction, action} from 'mobx'; import {IClient, IUser} from './types'; +import {identityTransform, jsonBody, jsonTransform, ResponseTransformer} from './fetchUtils'; const tokenKey = 'gotify-login-key'; @@ -33,32 +33,75 @@ export class CurrentUser { return ''; }; + public authenticatedFetch = async ( + url: string, + init: RequestInit, + xform: ResponseTransformer + ): Promise => { + const headers = new Headers(init?.headers); + if (this.loggedIn && !headers.has('X-Gotify-Key')) + headers.set('X-Gotify-Key', this.token()); + let response; + try { + response = await fetch(url, {...init, headers}); + } catch (error) { + this.snack('Gotify server is not reachable, try refreshing the page.'); + throw error; + } + if (response.ok) { + try { + return xform(response); + } catch (error) { + this.snack('Response transformation failed: ' + error); + throw error; + } + } + if (response.status === 401) { + this.tryAuthenticate().then(() => this.snack('Could not complete request.')); + } + + let error = 'Unexpected status code: ' + response.status; + if (response.status === 400 || response.status === 403 || response.status === 500) { + if (response.headers.get('content-type')?.includes('application/json')) { + const data = await response.json(); + error = data.error + ': ' + data.errorDescription; + } else { + const text = await response.text(); + error = 'Unexpected response: ' + text; + } + } + this.snack(error); + throw new Error(error); + }; + private readonly setToken = (token: string) => { this.tokenCache = token; window.localStorage.setItem(tokenKey, token); }; - public register = async (name: string, pass: string): Promise => - axios - .create() - .post(config.get('url') + 'user', {name, pass}) + public register = async (name: string, pass: string): Promise => { + runInAction(() => { + this.loggedIn = false; + }); + return this.authenticatedFetch( + config.get('url') + 'user', + jsonBody({name, pass}), + identityTransform + ) .then(() => { this.snack('User Created. Logging in...'); this.login(name, pass); return true; }) - .catch((error: AxiosError<{error?: string; errorDescription?: string}>) => { - if (!error || !error.response) { + .catch((error) => { + if (error instanceof TypeError) { this.snack('No network connection or server unavailable.'); return false; } - const {data} = error.response; - - this.snack( - `Register failed: ${data?.error ?? 'unknown'}: ${data?.errorDescription ?? ''}` - ); + this.snack(`Register failed: ${error?.message ?? error}`); return false; }); + }; public login = async (username: string, password: string) => { runInAction(() => { @@ -67,17 +110,17 @@ export class CurrentUser { }); const browser = detect(); const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser'; - axios - .create() - .request({ - url: config.get('url') + 'client', - method: 'POST', - data: {name}, - headers: {Authorization: 'Basic ' + btoa(username + ':' + password)}, - }) - .then((resp: AxiosResponse) => { + const fetchInit = jsonBody({name}); + fetchInit.headers = new Headers(fetchInit.headers); + fetchInit.headers.set('Authorization', 'Basic ' + btoa(username + ':' + password)); + return this.authenticatedFetch( + config.get('url') + 'client', + fetchInit, + jsonTransform + ) + .then((resp) => { this.snack(`A client named '${name}' was created for your session.`); - this.setToken(resp.data.token); + this.setToken(resp.token); this.tryAuthenticate().catch(() => { console.log( 'create client succeeded, but authenticated with given token failed' @@ -92,7 +135,7 @@ export class CurrentUser { ); }; - public tryAuthenticate = async (): Promise> => { + public tryAuthenticate = async (): Promise => { if (this.token() === '') { runInAction(() => { this.authenticating = false; @@ -100,51 +143,50 @@ export class CurrentUser { return Promise.reject(); } - return axios - .create() - .get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}}) - .then( - action((passThrough) => { - this.user = passThrough.data; - this.loggedIn = true; - this.authenticating = false; - this.connectionErrorMessage = null; - this.reconnectTime = 7500; - return passThrough; - }) - ) + return fetch(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}}) + .then(async (response) => { + if (response.ok) { + const user = await response.json(); + runInAction(() => { + this.user = user; + this.loggedIn = true; + this.authenticating = false; + this.connectionErrorMessage = null; + this.reconnectTime = 7500; + }); + return user; + } + if (response.status >= 500) { + this.connectionError(`${response.statusText} (code: ${response.status}).`); + return Promise.reject(new Error('Server error')); + } + + this.connectionErrorMessage = null; + + if (response.status >= 400 && response.status < 500) { + this.logout(); + } + throw new Error('Unexpected status code: ' + response.status); + }) .catch( - action((error: AxiosError) => { + action((error) => { this.authenticating = false; - if (!error || !error.response) { - this.connectionError('No network connection or server unavailable.'); - return Promise.reject(error); - } - - if (error.response.status >= 500) { - this.connectionError( - `${error.response.statusText} (code: ${error.response.status}).` - ); - return Promise.reject(error); - } - - this.connectionErrorMessage = null; - - if (error.response.status >= 400 && error.response.status < 500) { - this.logout(); - } + this.connectionError('No network connection or server unavailable.'); return Promise.reject(error); }) ); }; public logout = async () => { - await axios - .get(config.get('url') + 'client') - .then((resp: AxiosResponse) => { - resp.data - .filter((client) => client.token === this.tokenCache) - .forEach((client) => axios.delete(config.get('url') + 'client/' + client.id)); + await this.authenticatedFetch(config.get('url') + 'client', {}, jsonTransform) + .then((resp) => { + resp.filter((client) => client.token === this.tokenCache).forEach((client) => + this.authenticatedFetch( + config.get('url') + 'client/' + client.id, + {}, + jsonTransform + ) + ); }) .catch(() => Promise.resolve()); window.localStorage.removeItem(tokenKey); @@ -155,9 +197,15 @@ export class CurrentUser { }; public changePassword = (pass: string) => { - axios - .post(config.get('url') + 'current/user/password', {pass}) - .then(() => this.snack('Password changed')); + this.authenticatedFetch( + config.get('url') + 'current/user/password', + jsonBody({pass}), + identityTransform + ) + .then(() => this.snack('Password changed')) + .catch((error) => { + this.snack(`Change password failed: ${error?.message ?? error}`); + }); }; public tryReconnect = (quiet = false) => { diff --git a/ui/src/apiAuth.ts b/ui/src/apiAuth.ts deleted file mode 100644 index 183c8d66..00000000 --- a/ui/src/apiAuth.ts +++ /dev/null @@ -1,31 +0,0 @@ -import axios from 'axios'; -import {CurrentUser} from './CurrentUser'; -import {SnackReporter} from './snack/SnackManager'; - -export const initAxios = (currentUser: CurrentUser, snack: SnackReporter) => { - axios.interceptors.request.use((config) => { - if (!config.headers.has('x-gotify-key')) { - config.headers['x-gotify-key'] = currentUser.token(); - } - return config; - }); - - axios.interceptors.response.use(undefined, (error) => { - if (!error.response) { - snack('Gotify server is not reachable, try refreshing the page.'); - return Promise.reject(error); - } - - const status = error.response.status; - - if (status === 401) { - currentUser.tryAuthenticate().then(() => snack('Could not complete request.')); - } - - if (status === 400 || status === 403 || status === 500) { - snack(error.response.data.error + ': ' + error.response.data.errorDescription); - } - - return Promise.reject(error); - }); -}; diff --git a/ui/src/application/AppStore.ts b/ui/src/application/AppStore.ts index f14eb6cc..72efd8c7 100644 --- a/ui/src/application/AppStore.ts +++ b/ui/src/application/AppStore.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import {generateKeyBetween} from 'fractional-indexing'; import {action, runInAction} from 'mobx'; import {BaseStore} from '../common/BaseStore'; @@ -6,39 +5,62 @@ import * as config from '../config'; import {SnackReporter} from '../snack/SnackManager'; import {IApplication} from '../types'; import {arrayMove} from '@dnd-kit/sortable'; +import {CurrentUser} from '../CurrentUser'; +import {identityTransform, jsonBody, jsonTransform, multipartBody} from '../fetchUtils'; export class AppStore extends BaseStore { public onDelete: () => void = () => {}; - public constructor(private readonly snack: SnackReporter) { + public constructor( + private readonly currentUser: CurrentUser, + private readonly snack: SnackReporter + ) { super(); } protected requestItems = (): Promise => - axios - .get(`${config.get('url')}application`) - .then((response) => response.data); + this.currentUser.authenticatedFetch( + config.get('url') + 'application', + {}, + jsonTransform + ); protected requestDelete = (id: number): Promise => - axios.delete(`${config.get('url')}application/${id}`).then(() => { - this.onDelete(); - return this.snack('Application deleted'); - }); + this.currentUser + .authenticatedFetch( + config.get('url') + 'application/' + id, + { + method: 'DELETE', + }, + identityTransform + ) + .then(() => { + this.onDelete(); + return this.snack('Application deleted'); + }); @action public uploadImage = async (id: number, file: Blob): Promise => { const formData = new FormData(); formData.append('file', file); - await axios.post(`${config.get('url')}application/${id}/image`, formData, { - headers: {'content-type': 'multipart/form-data'}, - }); + await this.currentUser.authenticatedFetch( + config.get('url') + 'application/' + id + '/image', + multipartBody(formData), + jsonTransform + ); await this.refresh(); this.snack('Application image updated'); }; public async deleteImage(id: number): Promise { try { - await axios.delete(`${config.get('url')}application/${id}/image`); + await this.currentUser.authenticatedFetch( + config.get('url') + 'application/' + id + '/image', + { + method: 'DELETE', + }, + identityTransform + ); await this.refresh(); this.snack('Application image deleted'); } catch (error) { @@ -78,7 +100,11 @@ export class AppStore extends BaseStore { IApplication, 'id' | 'name' | 'description' | 'defaultPriority' | 'sortKey' >): Promise => { - await axios.put(`${config.get('url')}application/${id}`, app); + await this.currentUser.authenticatedFetch( + config.get('url') + 'application/' + id, + {...jsonBody(app), method: 'PUT'}, + jsonTransform + ); await this.refresh(); this.snack('Application updated'); }; @@ -89,11 +115,11 @@ export class AppStore extends BaseStore { description: string, defaultPriority: number ): Promise => { - await axios.post(`${config.get('url')}application`, { - name, - description, - defaultPriority, - }); + await this.currentUser.authenticatedFetch( + config.get('url') + 'application', + jsonBody({name, description, defaultPriority}), + jsonTransform + ); await this.refresh(); this.snack('Application created'); }; diff --git a/ui/src/client/ClientStore.ts b/ui/src/client/ClientStore.ts index cc63b6e4..c84c07da 100644 --- a/ui/src/client/ClientStore.ts +++ b/ui/src/client/ClientStore.ts @@ -1,36 +1,60 @@ import {BaseStore} from '../common/BaseStore'; -import axios from 'axios'; import * as config from '../config'; import {action} from 'mobx'; import {SnackReporter} from '../snack/SnackManager'; import {IClient} from '../types'; +import {CurrentUser} from '../CurrentUser'; +import {identityTransform, jsonBody, jsonTransform} from '../fetchUtils'; export class ClientStore extends BaseStore { - public constructor(private readonly snack: SnackReporter) { + public constructor( + private readonly currentUser: CurrentUser, + private readonly snack: SnackReporter + ) { super(); } protected requestItems = (): Promise => - axios.get(`${config.get('url')}client`).then((response) => response.data); + this.currentUser.authenticatedFetch( + config.get('url') + 'client', + {}, + jsonTransform + ); protected requestDelete(id: number): Promise { - return axios - .delete(`${config.get('url')}client/${id}`) + return this.currentUser + .authenticatedFetch( + config.get('url') + 'client/' + id, + { + method: 'DELETE', + }, + identityTransform + ) .then(() => this.snack('Client deleted')); } @action public update = async (id: number, name: string): Promise => { - await axios.put(`${config.get('url')}client/${id}`, {name}); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'client/' + id, + {...jsonBody({name}), method: 'PUT'}, + jsonTransform + ) + .then(() => this.snack('Client updated')); await this.refresh(); this.snack('Client updated'); }; @action public createNoNotifcation = async (name: string): Promise => { - const client = await axios.post(`${config.get('url')}client`, {name}); + const client = await this.currentUser.authenticatedFetch( + config.get('url') + 'client', + jsonBody({name}), + jsonTransform + ); await this.refresh(); - return client.data; + return client; }; @action diff --git a/ui/src/fetchUtils.ts b/ui/src/fetchUtils.ts new file mode 100644 index 00000000..9a165fd7 --- /dev/null +++ b/ui/src/fetchUtils.ts @@ -0,0 +1,31 @@ +export type ResponseTransformer = (response: Response) => Promise; + +export const identityTransform: ResponseTransformer = (response: Response) => + Promise.resolve(response); + +export const jsonTransform = (response: Response): Promise => response.json(); + +export const textTransform = (response: Response): Promise => response.text(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const jsonBody: (body: any) => RequestInit = (body: any) => ({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), +}); + +export const yamlBody: (text: string) => RequestInit = (text: string) => ({ + method: 'POST', + headers: { + 'Content-Type': 'application/x-yaml', + }, + body: text, +}); + +export const multipartBody: (body: FormData) => RequestInit = (body: FormData) => ({ + method: 'POST', + headers: {'content-type': 'multipart/form-data'}, + body, +}); diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 175d6cf6..e05822e9 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import {createRoot} from 'react-dom/client'; import 'typeface-roboto'; -import {initAxios} from './apiAuth'; import * as config from './config'; import Layout from './layout/Layout'; import {unregister} from './registerServiceWorker'; @@ -26,13 +25,13 @@ const prodUrl = urlWithSlash; const initStores = (): StoreMapping => { const snackManager = new SnackManager(); - const appStore = new AppStore(snackManager.snack); - const userStore = new UserStore(snackManager.snack); - const messagesStore = new MessagesStore(appStore, snackManager.snack); const currentUser = new CurrentUser(snackManager.snack); - const clientStore = new ClientStore(snackManager.snack); + const appStore = new AppStore(currentUser, snackManager.snack); + const userStore = new UserStore(currentUser, snackManager.snack); + const messagesStore = new MessagesStore(currentUser, appStore, snackManager.snack); + const clientStore = new ClientStore(currentUser, snackManager.snack); const wsStore = new WebSocketStore(snackManager.snack, currentUser); - const pluginStore = new PluginStore(snackManager.snack); + const pluginStore = new PluginStore(currentUser, snackManager.snack); appStore.onDelete = () => messagesStore.clearAll(); return { @@ -50,8 +49,6 @@ const initStores = (): StoreMapping => { (function clientJS() { config.set('url', prodUrl); const stores = initStores(); - initAxios(stores.currentUser, stores.snackManager.snack); - registerReactions(stores); stores.currentUser.tryAuthenticate().catch(() => {}); diff --git a/ui/src/message/MessagesStore.ts b/ui/src/message/MessagesStore.ts index e0be4070..4f1cb777 100644 --- a/ui/src/message/MessagesStore.ts +++ b/ui/src/message/MessagesStore.ts @@ -1,11 +1,12 @@ import {BaseStore} from '../common/BaseStore'; import {action, IObservableArray, observable, reaction, runInAction} from 'mobx'; -import axios, {AxiosResponse} from 'axios'; import * as config from '../config'; import {createTransformer} from 'mobx-utils'; import {SnackReporter} from '../snack/SnackManager'; import {IApplication, IMessage, IPagedMessages} from '../types'; import {closeSnackbar, SnackbarKey} from 'notistack'; +import {identityTransform, jsonBody, jsonTransform} from '../fetchUtils'; +import {CurrentUser} from '../CurrentUser'; const AllMessages = -1; @@ -28,6 +29,7 @@ export class MessagesStore { private loading = false; public constructor( + private readonly currentUser: CurrentUser, private readonly appStore: BaseStore, private readonly snack: SnackReporter ) { @@ -54,9 +56,7 @@ export class MessagesStore { this.loading = true; try { - const pagedResult = await this.fetchMessages(appId, state.nextSince).then( - (resp) => resp.data - ); + const pagedResult = await this.fetchMessages(appId, state.nextSince); runInAction(() => { state.messages.replace([...state.messages, ...pagedResult.messages]); state.nextSince = pagedResult.paging.since ?? 0; @@ -83,12 +83,28 @@ export class MessagesStore { @action public removeByApp = async (appId: number) => { if (appId === AllMessages) { - await axios.delete(config.get('url') + 'message'); - this.snack('Deleted all messages'); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'message', + { + method: 'DELETE', + }, + identityTransform + ) + .then(() => this.snack('Deleted all messages')); this.clearAll(); } else { - await axios.delete(config.get('url') + 'application/' + appId + '/message'); - this.snack(`Deleted all messages from ${this.appStore.getByID(appId).name}`); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'application/' + appId + '/message', + { + method: 'DELETE', + }, + identityTransform + ) + .then(() => + this.snack(`Deleted all messages from ${this.appStore.getByID(appId).name}`) + ); this.clear(AllMessages); this.clear(appId); } @@ -121,10 +137,16 @@ export class MessagesStore { return; } - await axios.delete(config.get('url') + 'message/' + message.id, { - adapter: 'fetch', - fetchOptions: {keepalive: true}, - }); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'message/' + message.id, + { + method: 'DELETE', + keepalive: true, + }, + identityTransform + ) + .then(() => this.snack(`Deleted message ${message.id}`)); if (this.exists(AllMessages)) { this.removeFromList(this.state[AllMessages].messages, message); } @@ -147,10 +169,12 @@ export class MessagesStore { title, }; - await axios.post(`${config.get('url')}message`, payload, { - headers: {'X-Gotify-Key': app.token}, - }); - this.snack(`Message sent to ${app.name}`); + const fetchInit = jsonBody(payload); + fetchInit.headers = new Headers(fetchInit.headers); + fetchInit.headers.set('X-Gotify-Key', app.token); + await this.currentUser + .authenticatedFetch(config.get('url') + 'message', fetchInit, jsonTransform) + .then(() => this.snack(`Message sent to ${app.name}`)); }; @action @@ -182,15 +206,18 @@ export class MessagesStore { @action private clear = (appId: number) => (this.state[appId] = this.emptyState()); - private fetchMessages = ( - appId: number, - since: number - ): Promise> => { + private fetchMessages = (appId: number, since: number): Promise => { if (appId === AllMessages) { - return axios.get(config.get('url') + 'message?since=' + since); + return this.currentUser.authenticatedFetch( + config.get('url') + 'message?since=' + since, + {}, + jsonTransform + ); } else { - return axios.get( - config.get('url') + 'application/' + appId + '/message?since=' + since + return this.currentUser.authenticatedFetch( + config.get('url') + 'application/' + appId + '/message?since=' + since, + {}, + jsonTransform ); } }; diff --git a/ui/src/message/WebSocketStore.ts b/ui/src/message/WebSocketStore.ts index 14a158fd..338f2f9d 100644 --- a/ui/src/message/WebSocketStore.ts +++ b/ui/src/message/WebSocketStore.ts @@ -1,7 +1,6 @@ import {SnackReporter} from '../snack/SnackManager'; import {CurrentUser} from '../CurrentUser'; import * as config from '../config'; -import {AxiosError} from 'axios'; import {IMessage} from '../types'; export class WebSocketStore { @@ -37,10 +36,8 @@ export class WebSocketStore { this.snack('WebSocket connection closed, trying again in 30 seconds.'); setTimeout(() => this.listen(callback), 30000); }) - .catch((error: AxiosError) => { - if (error?.response?.status === 401) { - this.snack('Could not authenticate with client token, logging out.'); - } + .catch(() => { + this.snack('Could not authenticate with client token.'); }); }; diff --git a/ui/src/plugin/PluginStore.ts b/ui/src/plugin/PluginStore.ts index 363af47e..8572961c 100644 --- a/ui/src/plugin/PluginStore.ts +++ b/ui/src/plugin/PluginStore.ts @@ -1,25 +1,41 @@ -import axios from 'axios'; import {action} from 'mobx'; import {BaseStore} from '../common/BaseStore'; import * as config from '../config'; import {SnackReporter} from '../snack/SnackManager'; import {IPlugin} from '../types'; +import {CurrentUser} from '../CurrentUser'; +import {identityTransform, yamlBody, jsonTransform, textTransform} from '../fetchUtils'; export class PluginStore extends BaseStore { public onDelete: () => void = () => {}; - public constructor(private readonly snack: SnackReporter) { + public constructor( + private readonly currentUser: CurrentUser, + private readonly snack: SnackReporter + ) { super(); } public requestConfig = (id: number): Promise => - axios.get(`${config.get('url')}plugin/${id}/config`).then((response) => response.data); + this.currentUser.authenticatedFetch( + config.get('url') + 'plugin/' + id + '/config', + {}, + textTransform + ); public requestDisplay = (id: number): Promise => - axios.get(`${config.get('url')}plugin/${id}/display`).then((response) => response.data); + this.currentUser.authenticatedFetch( + config.get('url') + 'plugin/' + id + '/display', + {}, + jsonTransform + ); protected requestItems = (): Promise => - axios.get(`${config.get('url')}plugin`).then((response) => response.data); + this.currentUser.authenticatedFetch( + config.get('url') + 'plugin', + {}, + jsonTransform + ); protected requestDelete = (): Promise => { this.snack('Cannot delete plugin'); @@ -33,17 +49,25 @@ export class PluginStore extends BaseStore { @action public changeConfig = async (id: number, newConfig: string): Promise => { - await axios.post(`${config.get('url')}plugin/${id}/config`, newConfig, { - headers: {'content-type': 'application/x-yaml'}, - }); - this.snack(`Plugin config updated`); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'plugin/' + id + '/config', + yamlBody(newConfig), + identityTransform + ) + .then(() => this.snack('Plugin config updated')); await this.refresh(); }; @action public changeEnabledState = async (id: number, enabled: boolean): Promise => { - await axios.post(`${config.get('url')}plugin/${id}/${enabled ? 'enable' : 'disable'}`); - this.snack(`Plugin ${enabled ? 'enabled' : 'disabled'}`); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'plugin/' + id + '/' + (enabled ? 'enable' : 'disable'), + {method: 'POST'}, + identityTransform + ) + .then(() => this.snack('Plugin ' + (enabled ? 'enabled' : 'disabled'))); await this.refresh(); }; } diff --git a/ui/src/tests/message.test.ts b/ui/src/tests/message.test.ts index d9b41727..ac764b58 100644 --- a/ui/src/tests/message.test.ts +++ b/ui/src/tests/message.test.ts @@ -13,8 +13,7 @@ import { import {afterAll, beforeAll, describe, expect, it} from 'vitest'; import * as auth from './authentication'; import * as selector from './selector'; -import axios from 'axios'; -import {IApplication, IMessage, IMessageExtras} from '../types'; +import {IMessage, IMessageExtras} from '../types'; let page: Page; let gotify: GotifyTest; @@ -25,8 +24,6 @@ beforeAll(async () => { afterAll(async () => await gotify.close()); -const axiosAuth = {auth: {username: 'admin', password: 'admin'}}; - let windowsServerToken: string; let linuxServerToken: string; let backupServerToken: string; @@ -52,9 +49,20 @@ describe('Messages', () => { expect(page.url()).toContain('/'); }); const createApp = (name: string) => - axios - .post(`${gotify.url}/application`, {name}, axiosAuth) - .then((resp) => resp.data.token); + fetch(`${gotify.url}/application`, { + method: 'POST', + headers: { + Authorization: 'Basic ' + btoa('admin:admin'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({name}), + }) + .then((resp) => { + expect(resp.ok).toBe(true); + return resp.json(); + }) + .then((data) => data.token); + it('shows navigation', async () => { await page.waitForSelector(naviId); }); @@ -148,8 +156,18 @@ describe('Messages', () => { const backup3 = m('Backup done', 'Gotify Backup finished (0.1MB).'); const createMessage = (msg: Partial, token: string) => - axios.post(`${gotify.url}/message`, msg, { - headers: {'X-Gotify-Key': token}, + fetch(`${gotify.url}/message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Gotify-Key': token, + }, + body: JSON.stringify(msg), + }).then((resp) => { + if (!resp.ok) { + throw new Error('Failed to create message'); + } + return resp.json(); }); const expectMessages = async (toCheck: { diff --git a/ui/src/tests/plugin.test.ts b/ui/src/tests/plugin.test.ts index b551cf66..380b9ebe 100644 --- a/ui/src/tests/plugin.test.ts +++ b/ui/src/tests/plugin.test.ts @@ -1,6 +1,5 @@ import * as os from 'os'; import {Page} from 'puppeteer'; -import axios from 'axios'; import {afterAll, beforeAll, describe, expect, it} from 'vitest'; import * as auth from './authentication'; import * as selector from './selector'; @@ -174,7 +173,7 @@ describe('plugin', () => { if (!hook) { throw 'href not found'; } - await axios.get(hook); + expect((await fetch(hook)).ok).toBe(true); }); }); it('has received message', async () => { diff --git a/ui/src/tests/setup.ts b/ui/src/tests/setup.ts index 84b757af..9698f816 100644 --- a/ui/src/tests/setup.ts +++ b/ui/src/tests/setup.ts @@ -1,11 +1,9 @@ import getPort from 'get-port'; -import {spawn, exec, ChildProcess} from 'child_process'; +import {spawn, ChildProcess} from 'child_process'; import {rimrafSync} from 'rimraf'; import path from 'path'; import puppeteer, {Browser, Page} from 'puppeteer'; import fs from 'fs'; -// @ts-expect-error no types -import wait from 'wait-on'; import kill from 'tree-kill'; export interface GotifyTest { @@ -37,7 +35,7 @@ export const newTest = async (pluginsDir = ''): Promise => { const gotifyInstance = startGotify(gotifyFile, port, pluginsDir); const gotifyURL = 'http://localhost:' + port; - await waitForGotify('http-get://localhost:' + port); + await waitForGotify(gotifyURL); const browser = await puppeteer.launch({ headless: process.env.CI === 'true', args: [`--window-size=1920,1080`, '--no-sandbox'], @@ -84,23 +82,51 @@ const testFilePath = (): string => { return path.join(testBuildPath, filename); }; -const waitForGotify = (url: string): Promise => - new Promise((resolve, err) => { - wait({resources: [url], timeout: 40000}, (error: string) => { - if (error) { - console.log(error); - err(error); +const waitForGotify = async (url: string): Promise => { + const deadline = Date.now() + 30000; + let status = new Error('timeout'); + while (Date.now() < deadline) { + const abc = new AbortController(); + const timeout = setTimeout(() => { + abc.abort(); + }, 1000); + try { + const res = await fetch(url, { + signal: abc.signal, + }); + if (res.status === 200) { + return; + } + status = new Error(`${res.status} ${res.statusText}`); + } catch (error) { + if (error instanceof Error) { + status = error; } else { - resolve(); + status = new Error(String(error)); } - }); - }); + await new Promise((resolve) => setTimeout(resolve, 250)); + } finally { + clearTimeout(timeout); + } + } + throw status; +}; const buildGoPlugin = (filename: string, pluginPath: string): Promise => { process.stdout.write(`### Building Plugin ${pluginPath}\n`); - return new Promise((resolve) => - exec(`go build -o ${filename} -buildmode=plugin ${pluginPath}`, () => resolve()) - ); + return new Promise((resolve, err) => { + const build = spawn('go', ['build', '-o', filename, '-buildmode=plugin', pluginPath], { + stdio: 'inherit', + }); + + build.on('close', (code) => { + if (code) { + err('exit code: ' + err); + } else { + resolve(); + } + }); + }); }; const buildGoExecutable = (filename: string): Promise => { @@ -114,11 +140,22 @@ const buildGoExecutable = (filename: string): Promise => { return Promise.resolve(); } else { process.stdout.write(`### Building Gotify ${filename}\n`); - return new Promise((resolve) => - exec(`go build -ldflags="-X main.Mode=prod" -o ${filename} ${appDotGo}`, () => - resolve() - ) - ); + return new Promise((resolve, err) => { + const build = spawn( + 'go', + ['build', '-ldflags=-X main.Mode=prod', '-o', filename, appDotGo], + { + stdio: 'inherit', + } + ); + build.on('close', (code) => { + if (code) { + err('exit code: ' + err); + } else { + resolve(); + } + }); + }); } }; diff --git a/ui/src/user/UserStore.ts b/ui/src/user/UserStore.ts index dc42f373..a023c676 100644 --- a/ui/src/user/UserStore.ts +++ b/ui/src/user/UserStore.ts @@ -1,34 +1,56 @@ import {BaseStore} from '../common/BaseStore'; -import axios from 'axios'; import * as config from '../config'; import {action} from 'mobx'; import {SnackReporter} from '../snack/SnackManager'; import {IUser} from '../types'; +import {CurrentUser} from '../CurrentUser'; +import {identityTransform, jsonBody, jsonTransform} from '../fetchUtils'; export class UserStore extends BaseStore { - constructor(private readonly snack: SnackReporter) { + constructor( + private readonly currentUser: CurrentUser, + private readonly snack: SnackReporter + ) { super(); } protected requestItems = (): Promise => - axios.get(`${config.get('url')}user`).then((response) => response.data); + this.currentUser.authenticatedFetch(config.get('url') + 'user', {}, jsonTransform); protected requestDelete(id: number): Promise { - return axios - .delete(`${config.get('url')}user/${id}`) + return this.currentUser + .authenticatedFetch( + config.get('url') + 'user/' + id, + { + method: 'DELETE', + }, + identityTransform + ) .then(() => this.snack('User deleted')); } @action public create = async (name: string, pass: string, admin: boolean) => { - await axios.post(`${config.get('url')}user`, {name, pass, admin}); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'user', + jsonBody({name, pass, admin}), + identityTransform + ) + .then(() => this.snack('User created')); await this.refresh(); this.snack('User created'); }; @action public update = async (id: number, name: string, pass: string | null, admin: boolean) => { - await axios.post(config.get('url') + 'user/' + id, {name, pass, admin}); + await this.currentUser + .authenticatedFetch( + config.get('url') + 'user/' + id, + jsonBody({name, pass, admin}), + identityTransform + ) + .then(() => this.snack('User updated')); await this.refresh(); this.snack('User updated'); }; diff --git a/ui/yarn.lock b/ui/yarn.lock index b96ca780..583d10d5 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -655,40 +655,6 @@ "@eslint/core" "^0.17.0" levn "^0.4.1" -"@hapi/address@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-5.1.1.tgz#e9925fc1b65f5cc3fbea821f2b980e4652e84cb6" - integrity sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA== - dependencies: - "@hapi/hoek" "^11.0.2" - -"@hapi/formula@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-3.0.2.tgz#81b538060ee079481c906f599906d163c4badeaf" - integrity sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw== - -"@hapi/hoek@^11.0.2", "@hapi/hoek@^11.0.7": - version "11.0.7" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.7.tgz#56a920793e0a42d10e530da9a64cc0d3919c4002" - integrity sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ== - -"@hapi/pinpoint@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.1.tgz#32077e715655fc00ab8df74b6b416114287d6513" - integrity sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q== - -"@hapi/tlds@^1.1.1": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.4.tgz#df4a7b59082b54ba4f3b7b38f781e2ac3cbc359a" - integrity sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA== - -"@hapi/topo@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-6.0.2.tgz#f219c1c60da8430228af4c1f2e40c32a0d84bbb4" - integrity sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg== - dependencies: - "@hapi/hoek" "^11.0.2" - "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1462,20 +1428,6 @@ ast-types@^0.13.4: dependencies: tslib "^2.0.1" -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.11.0, axios@^1.13.2: - version "1.13.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.4.tgz#15d109a4817fb82f73aea910d41a2c85606076bc" - integrity sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" - proxy-from-env "^1.1.0" - b4a@^1.6.4: version "1.7.3" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.7.3.tgz#24cf7ccda28f5465b66aec2bac69e32809bf112f" @@ -1583,14 +1535,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1691,13 +1635,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -1796,11 +1733,6 @@ degenerator@^5.0.0: escodegen "^2.1.0" esprima "^4.0.1" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -1831,15 +1763,6 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - electron-to-chromium@^1.5.263: version "1.5.286" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" @@ -1869,38 +1792,11 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - es-module-lexer@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - esbuild@^0.27.0: version "0.27.3" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" @@ -2160,22 +2056,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== -follow-redirects@^1.15.6: - version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" - integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== - -form-data@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - fractional-indexing@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/fractional-indexing/-/fractional-indexing-3.2.0.tgz#1193e63d54ff4e0cbe0c79a9ed6cfbab25d91628" @@ -2201,35 +2081,11 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - get-port@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-7.1.0.tgz#d5a500ebfc7aa705294ec2b83cc38c5d0e364fec" integrity sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw== -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -2272,28 +2128,11 @@ goober@^2.0.33: resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95" integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g== -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -2452,19 +2291,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -joi@^18.0.1: - version "18.0.2" - resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.2.tgz#30ced6aed00a7848cc11f92859515258301dc3a4" - integrity sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA== - dependencies: - "@hapi/address" "^5.1.1" - "@hapi/formula" "^3.0.2" - "@hapi/hoek" "^11.0.7" - "@hapi/pinpoint" "^2.0.1" - "@hapi/tlds" "^1.1.1" - "@hapi/topo" "^6.0.2" - "@standard-schema/spec" "^1.0.0" - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2539,11 +2365,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -2585,11 +2406,6 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - mdast-util-find-and-replace@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" @@ -3043,18 +2859,6 @@ micromark@^4.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - minimatch@^10.1.1: version "10.1.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" @@ -3076,11 +2880,6 @@ minimatch@^9.0.5: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" @@ -3576,13 +3375,6 @@ rollup@^4.43.0: "@rollup/rollup-win32-x64-msvc" "4.57.1" fsevents "~2.3.2" -rxjs@^7.8.2: - version "7.8.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" - integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== - dependencies: - tslib "^2.1.0" - scheduler@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" @@ -3816,7 +3608,7 @@ ts-api-utils@^2.4.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0: +tslib@^2.0.0, tslib@^2.0.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -3999,17 +3791,6 @@ w3c-keyname@^2.2.4: resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== -wait-on@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-9.0.3.tgz#3ea858db0b854039e6aff5f323885aaef25e32bf" - integrity sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA== - dependencies: - axios "^1.13.2" - joi "^18.0.1" - lodash "^4.17.21" - minimist "^1.2.8" - rxjs "^7.8.2" - webdriver-bidi-protocol@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz#3057477209cc5beebba19d50b31304c4cec1006d"