Compare commits
4 commits
develop
...
feat/socke
Author | SHA1 | Date | |
---|---|---|---|
|
76f9637d77 | ||
|
25c39b92e5 | ||
|
52a2753522 | ||
|
7f98f65bd0 |
29 changed files with 711 additions and 263 deletions
|
@ -2,5 +2,5 @@ module.exports = {
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
semi: true,
|
semi: true,
|
||||||
trailingComma: 'all',
|
trailingComma: 'all',
|
||||||
printWidth: 200,
|
printWidth: 150,
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,9 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./packages/worker/Dockerfile.dev
|
dockerfile: ./packages/worker/Dockerfile.dev
|
||||||
container_name: tipi-worker
|
container_name: tipi-worker
|
||||||
|
user: root
|
||||||
|
ports:
|
||||||
|
- 3935:3001
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
|
@ -60,6 +60,8 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./packages/worker/Dockerfile
|
dockerfile: ./packages/worker/Dockerfile
|
||||||
container_name: tipi-worker
|
container_name: tipi-worker
|
||||||
|
ports:
|
||||||
|
- 3935:3001
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
"description": "A homeserver for everyone",
|
"description": "A homeserver for everyone",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"knip": "knip",
|
"knip": "knip",
|
||||||
"prepare": "mkdir -p state && echo \"{}\" > state/system-info.json && echo \"random-seed\" > state/seed",
|
|
||||||
"test": "dotenv -e .env.test -- jest --colors",
|
"test": "dotenv -e .env.test -- jest --colors",
|
||||||
"test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test",
|
"test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test",
|
||||||
"test:e2e:ui": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test --ui",
|
"test:e2e:ui": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test --ui",
|
||||||
"test:client": "jest --colors --selectProjects client --",
|
"test:client": "jest --colors --selectProjects client --",
|
||||||
"test:server": "jest --colors --selectProjects server --",
|
"test:server": "jest --colors --selectProjects server --",
|
||||||
"test:vite": "dotenv -e .env.test -- vitest run --coverage",
|
"test:vite": "dotenv -e .env.test -- vitest run --coverage",
|
||||||
"dev": "next dev",
|
"dev": "DEBUG=socket.io:client* next dev",
|
||||||
"dev:watcher": "pnpm -r --filter cli dev",
|
"dev:watcher": "pnpm -r --filter cli dev",
|
||||||
"db:migrate": "NODE_ENV=development dotenv -e .env.local -- tsx ./src/server/run-migrations-dev.ts",
|
"db:migrate": "NODE_ENV=development dotenv -e .env.local -- tsx ./src/server/run-migrations-dev.ts",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
@ -20,8 +19,8 @@
|
||||||
"start": "NODE_ENV=production node server.js",
|
"start": "NODE_ENV=production node server.js",
|
||||||
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
|
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
|
||||||
"start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build",
|
"start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build",
|
||||||
"start:dev": "npm run prepare && docker compose -f docker-compose.dev.yml up --build",
|
"start:dev": "docker compose -f docker-compose.dev.yml up --build",
|
||||||
"start:prod": "npm run prepare && docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
|
"start:prod": "docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
|
||||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
|
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
|
||||||
"version": "echo $npm_package_version",
|
"version": "echo $npm_package_version",
|
||||||
"release:rc": "./scripts/deploy/release-rc.sh",
|
"release:rc": "./scripts/deploy/release-rc.sh",
|
||||||
|
@ -76,6 +75,7 @@
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
|
"socket.io-client": "^4.7.2",
|
||||||
"swr": "^2.2.4",
|
"swr": "^2.2.4",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
|
|
@ -60,6 +60,8 @@ services:
|
||||||
container_name: tipi-worker
|
container_name: tipi-worker
|
||||||
image: ghcr.io/runtipi/worker:${TIPI_VERSION}
|
image: ghcr.io/runtipi/worker:${TIPI_VERSION}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 3935:3001
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
35
packages/shared/src/schemas/socket.ts
Normal file
35
packages/shared/src/schemas/socket.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const socketEventSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
type: z.literal('app'),
|
||||||
|
event: z.union([
|
||||||
|
z.literal('status_change'),
|
||||||
|
z.literal('install_success'),
|
||||||
|
z.literal('install_error'),
|
||||||
|
z.literal('uninstall_success'),
|
||||||
|
z.literal('uninstall_error'),
|
||||||
|
z.literal('update_success'),
|
||||||
|
z.literal('update_error'),
|
||||||
|
z.literal('start_success'),
|
||||||
|
z.literal('start_error'),
|
||||||
|
z.literal('stop_success'),
|
||||||
|
z.literal('stop_error'),
|
||||||
|
z.literal('generate_env_success'),
|
||||||
|
z.literal('generate_env_error'),
|
||||||
|
]),
|
||||||
|
data: z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('dummy'),
|
||||||
|
event: z.literal('dummy_event'),
|
||||||
|
data: z.object({
|
||||||
|
dummy: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SocketEvent = z.infer<typeof socketEventSchema>;
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"watch": ["src"],
|
"watch": ["src"],
|
||||||
"exec": "NODE_ENV=development tsx ./src/index.ts",
|
"exec": "NODE_ENV=development DEBUG=socket.io:client* tsx ./src/index.ts",
|
||||||
"ext": "js ts"
|
"ext": "js ts"
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"socket.io": "^4.7.2",
|
||||||
"systeminformation": "^5.21.15",
|
"systeminformation": "^5.21.15",
|
||||||
"web-push": "^3.6.6",
|
"web-push": "^3.6.6",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { SystemEvent } from '@runtipi/shared';
|
import { SystemEvent } from '@runtipi/shared';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
|
@ -9,6 +11,7 @@ import { runPostgresMigrations } from '@/lib/migrations';
|
||||||
import { startWorker } from './watcher/watcher';
|
import { startWorker } from './watcher/watcher';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { AppExecutors } from './services';
|
import { AppExecutors } from './services';
|
||||||
|
import { SocketManager } from './lib/socket/SocketManager';
|
||||||
|
|
||||||
const rootFolder = '/app';
|
const rootFolder = '/app';
|
||||||
const envFile = path.join(rootFolder, '.env');
|
const envFile = path.join(rootFolder, '.env');
|
||||||
|
@ -85,6 +88,16 @@ const main = async () => {
|
||||||
server.listen(3000, () => {
|
server.listen(3000, () => {
|
||||||
startWorker();
|
startWorker();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const io = new Server(3001, { cors: { origin: '*' } });
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
SocketManager.addSocket(socket);
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
SocketManager.removeSocket();
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
30
packages/worker/src/lib/socket/SocketManager.ts
Normal file
30
packages/worker/src/lib/socket/SocketManager.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { SocketEvent } from '@runtipi/shared/src/schemas/socket';
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
|
||||||
|
class SocketManager {
|
||||||
|
private socket: Socket | null = null;
|
||||||
|
|
||||||
|
addSocket(socket: Socket) {
|
||||||
|
if (!this.socket) {
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSocket() {
|
||||||
|
this.socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: SocketEvent) {
|
||||||
|
if (!this.socket) {
|
||||||
|
logger.warn('Socket is not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.emit(event.type, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new SocketManager();
|
||||||
|
|
||||||
|
export { instance as SocketManager };
|
|
@ -4,11 +4,13 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { execAsync, pathExists } from '@runtipi/shared';
|
import { execAsync, pathExists } from '@runtipi/shared';
|
||||||
|
import { SocketEvent } from '@runtipi/shared/src/schemas/socket';
|
||||||
import { copyDataDir, generateEnvFile } from './app.helpers';
|
import { copyDataDir, generateEnvFile } from './app.helpers';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { compose } from '@/lib/docker';
|
import { compose } from '@/lib/docker';
|
||||||
import { getEnv } from '@/lib/environment';
|
import { getEnv } from '@/lib/environment';
|
||||||
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
|
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
|
||||||
|
import { SocketManager } from '@/lib/socket/SocketManager';
|
||||||
|
|
||||||
const getDbClient = async () => {
|
const getDbClient = async () => {
|
||||||
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
|
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
|
||||||
|
@ -33,12 +35,14 @@ export class AppExecutors {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAppError = (err: unknown) => {
|
private handleAppError = (err: unknown, appId: string, event: Extract<SocketEvent, { type: 'app' }>['event']) => {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
SocketManager.emit({ type: 'app', event, data: { appId, error: err.message } });
|
||||||
this.logger.error(`An error occurred: ${err.message}`);
|
this.logger.error(`An error occurred: ${err.message}`);
|
||||||
return { success: false, message: err.message };
|
return { success: false, message: err.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event, data: { appId, error: String(err) } });
|
||||||
return { success: false, message: `An error occurred: ${err}` };
|
return { success: false, message: `An error occurred: ${err}` };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,6 +77,19 @@ export class AppExecutors {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public regenerateAppEnv = async (appId: string, config: Record<string, unknown>) => {
|
||||||
|
try {
|
||||||
|
this.logger.info(`Regenerating app.env file for app ${appId}`);
|
||||||
|
await this.ensureAppDir(appId);
|
||||||
|
await generateEnvFile(appId, config);
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'generate_env_success', data: { appId } });
|
||||||
|
return { success: true, message: `App ${appId} env file regenerated successfully` };
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleAppError(err, appId, 'generate_env_error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install an app from the repo
|
* Install an app from the repo
|
||||||
* @param {string} appId - The id of the app to install
|
* @param {string} appId - The id of the app to install
|
||||||
|
@ -80,6 +97,8 @@ export class AppExecutors {
|
||||||
*/
|
*/
|
||||||
public installApp = async (appId: string, config: Record<string, unknown>) => {
|
public installApp = async (appId: string, config: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
|
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
|
||||||
|
|
||||||
if (process.getuid && process.getgid) {
|
if (process.getuid && process.getgid) {
|
||||||
this.logger.info(`Installing app ${appId} as User ID: ${process.getuid()}, Group ID: ${process.getgid()}`);
|
this.logger.info(`Installing app ${appId} as User ID: ${process.getuid()}, Group ID: ${process.getgid()}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -134,9 +153,11 @@ export class AppExecutors {
|
||||||
|
|
||||||
this.logger.info(`Docker-compose up for app ${appId} finished`);
|
this.logger.info(`Docker-compose up for app ${appId} finished`);
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'install_success', data: { appId } });
|
||||||
|
|
||||||
return { success: true, message: `App ${appId} installed successfully` };
|
return { success: true, message: `App ${appId} installed successfully` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.handleAppError(err);
|
return this.handleAppError(err, appId, 'install_error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -147,6 +168,7 @@ export class AppExecutors {
|
||||||
*/
|
*/
|
||||||
public stopApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
|
public stopApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
|
||||||
try {
|
try {
|
||||||
|
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
|
||||||
this.logger.info(`Stopping app ${appId}`);
|
this.logger.info(`Stopping app ${appId}`);
|
||||||
|
|
||||||
await this.ensureAppDir(appId);
|
await this.ensureAppDir(appId);
|
||||||
|
@ -158,14 +180,19 @@ export class AppExecutors {
|
||||||
await compose(appId, 'rm --force --stop');
|
await compose(appId, 'rm --force --stop');
|
||||||
|
|
||||||
this.logger.info(`App ${appId} stopped`);
|
this.logger.info(`App ${appId} stopped`);
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'stop_success', data: { appId } });
|
||||||
|
|
||||||
return { success: true, message: `App ${appId} stopped successfully` };
|
return { success: true, message: `App ${appId} stopped successfully` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.handleAppError(err);
|
return this.handleAppError(err, appId, 'stop_error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public startApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
|
public startApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
|
||||||
try {
|
try {
|
||||||
|
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
|
||||||
|
|
||||||
const { appDataDirPath } = this.getAppPaths(appId);
|
const { appDataDirPath } = this.getAppPaths(appId);
|
||||||
|
|
||||||
this.logger.info(`Starting app ${appId}`);
|
this.logger.info(`Starting app ${appId}`);
|
||||||
|
@ -186,14 +213,18 @@ export class AppExecutors {
|
||||||
this.logger.error(`Error setting permissions for app ${appId}`);
|
this.logger.error(`Error setting permissions for app ${appId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'start_success', data: { appId } });
|
||||||
|
|
||||||
return { success: true, message: `App ${appId} started successfully` };
|
return { success: true, message: `App ${appId} started successfully` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.handleAppError(err);
|
return this.handleAppError(err, appId, 'start_error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public uninstallApp = async (appId: string, config: Record<string, unknown>) => {
|
public uninstallApp = async (appId: string, config: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
|
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
|
||||||
|
|
||||||
const { appDirPath, appDataDirPath } = this.getAppPaths(appId);
|
const { appDirPath, appDataDirPath } = this.getAppPaths(appId);
|
||||||
this.logger.info(`Uninstalling app ${appId}`);
|
this.logger.info(`Uninstalling app ${appId}`);
|
||||||
|
|
||||||
|
@ -221,14 +252,19 @@ export class AppExecutors {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`App ${appId} uninstalled`);
|
this.logger.info(`App ${appId} uninstalled`);
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'uninstall_success', data: { appId } });
|
||||||
|
|
||||||
return { success: true, message: `App ${appId} uninstalled successfully` };
|
return { success: true, message: `App ${appId} uninstalled successfully` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.handleAppError(err);
|
return this.handleAppError(err, appId, 'uninstall_error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public updateApp = async (appId: string, config: Record<string, unknown>) => {
|
public updateApp = async (appId: string, config: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
|
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
|
||||||
|
|
||||||
const { appDirPath, repoPath } = this.getAppPaths(appId);
|
const { appDirPath, repoPath } = this.getAppPaths(appId);
|
||||||
this.logger.info(`Updating app ${appId}`);
|
this.logger.info(`Updating app ${appId}`);
|
||||||
await this.ensureAppDir(appId);
|
await this.ensureAppDir(appId);
|
||||||
|
@ -245,20 +281,11 @@ export class AppExecutors {
|
||||||
|
|
||||||
await compose(appId, 'pull');
|
await compose(appId, 'pull');
|
||||||
|
|
||||||
|
SocketManager.emit({ type: 'app', event: 'update_success', data: { appId } });
|
||||||
|
|
||||||
return { success: true, message: `App ${appId} updated successfully` };
|
return { success: true, message: `App ${appId} updated successfully` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.handleAppError(err);
|
return this.handleAppError(err, appId, 'update_error');
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public regenerateAppEnv = async (appId: string, config: Record<string, unknown>) => {
|
|
||||||
try {
|
|
||||||
this.logger.info(`Regenerating app.env file for app ${appId}`);
|
|
||||||
await this.ensureAppDir(appId);
|
|
||||||
await generateEnvFile(appId, config);
|
|
||||||
return { success: true, message: `App ${appId} env file regenerated successfully` };
|
|
||||||
} catch (err) {
|
|
||||||
return this.handleAppError(err);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
150
pnpm-lock.yaml
150
pnpm-lock.yaml
|
@ -140,6 +140,9 @@ importers:
|
||||||
sharp:
|
sharp:
|
||||||
specifier: 0.32.6
|
specifier: 0.32.6
|
||||||
version: 0.32.6
|
version: 0.32.6
|
||||||
|
socket.io-client:
|
||||||
|
specifier: ^4.7.2
|
||||||
|
version: 4.7.2
|
||||||
swr:
|
swr:
|
||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.2.4(react@18.2.0)
|
version: 2.2.4(react@18.2.0)
|
||||||
|
@ -424,6 +427,9 @@ importers:
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.11.3
|
specifier: ^8.11.3
|
||||||
version: 8.11.3
|
version: 8.11.3
|
||||||
|
socket.io:
|
||||||
|
specifier: ^4.7.2
|
||||||
|
version: 4.7.2
|
||||||
systeminformation:
|
systeminformation:
|
||||||
specifier: ^5.21.15
|
specifier: ^5.21.15
|
||||||
version: 5.21.15
|
version: 5.21.15
|
||||||
|
@ -2957,6 +2963,10 @@ packages:
|
||||||
p-map: 4.0.0
|
p-map: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@socket.io/component-emitter@3.1.0:
|
||||||
|
resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@swc/helpers@0.5.2:
|
/@swc/helpers@0.5.2:
|
||||||
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
|
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3200,7 +3210,12 @@ packages:
|
||||||
|
|
||||||
/@types/cookie@0.4.1:
|
/@types/cookie@0.4.1:
|
||||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
||||||
dev: true
|
|
||||||
|
/@types/cors@2.8.17:
|
||||||
|
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.8.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/debug@4.1.7:
|
/@types/debug@4.1.7:
|
||||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||||
|
@ -3731,6 +3746,14 @@ packages:
|
||||||
/abbrev@1.1.1:
|
/abbrev@1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
|
|
||||||
|
/accepts@1.3.8:
|
||||||
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.35
|
||||||
|
negotiator: 0.6.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/acorn-globals@7.0.1:
|
/acorn-globals@7.0.1:
|
||||||
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
|
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4186,6 +4209,11 @@ packages:
|
||||||
/base64-js@1.5.1:
|
/base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
|
/base64id@2.0.0:
|
||||||
|
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||||
|
engines: {node: ^4.5.0 || >= 5.9}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/binary-extensions@2.2.0:
|
/binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -4650,6 +4678,14 @@ packages:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cors@2.8.5:
|
||||||
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cosmiconfig@7.1.0:
|
/cosmiconfig@7.1.0:
|
||||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -5097,6 +5133,45 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
|
/engine.io-client@6.5.3:
|
||||||
|
resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==}
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.0
|
||||||
|
debug: 4.3.4
|
||||||
|
engine.io-parser: 5.2.1
|
||||||
|
ws: 8.11.0
|
||||||
|
xmlhttprequest-ssl: 2.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/engine.io-parser@5.2.1:
|
||||||
|
resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/engine.io@6.5.4:
|
||||||
|
resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
dependencies:
|
||||||
|
'@types/cookie': 0.4.1
|
||||||
|
'@types/cors': 2.8.17
|
||||||
|
'@types/node': 20.8.10
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cookie: 0.4.2
|
||||||
|
cors: 2.8.5
|
||||||
|
debug: 4.3.4
|
||||||
|
engine.io-parser: 5.2.1
|
||||||
|
ws: 8.11.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
/enhanced-resolve@5.12.0:
|
/enhanced-resolve@5.12.0:
|
||||||
resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==}
|
resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
@ -10001,6 +10076,56 @@ packages:
|
||||||
is-fullwidth-code-point: 4.0.0
|
is-fullwidth-code-point: 4.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/socket.io-adapter@2.5.2:
|
||||||
|
resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==}
|
||||||
|
dependencies:
|
||||||
|
ws: 8.11.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/socket.io-client@4.7.2:
|
||||||
|
resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.0
|
||||||
|
debug: 4.3.4
|
||||||
|
engine.io-client: 6.5.3
|
||||||
|
socket.io-parser: 4.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/socket.io-parser@4.2.4:
|
||||||
|
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.0
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/socket.io@4.7.2:
|
||||||
|
resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cors: 2.8.5
|
||||||
|
debug: 4.3.4
|
||||||
|
engine.io: 6.5.4
|
||||||
|
socket.io-adapter: 2.5.2
|
||||||
|
socket.io-parser: 4.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
/source-map-js@1.0.2:
|
/source-map-js@1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -10949,6 +11074,11 @@ packages:
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/version-selector-type@3.0.0:
|
/version-selector-type@3.0.0:
|
||||||
resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==}
|
resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==}
|
||||||
engines: {node: '>=10.13'}
|
engines: {node: '>=10.13'}
|
||||||
|
@ -11350,6 +11480,19 @@ packages:
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ws@8.11.0:
|
||||||
|
resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ^5.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ws@8.12.1:
|
/ws@8.12.1:
|
||||||
resolution: {integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==}
|
resolution: {integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
@ -11372,6 +11515,11 @@ packages:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/xmlhttprequest-ssl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xtend@4.0.2:
|
/xtend@4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|
|
@ -14,10 +14,11 @@ export function LoginContainer() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const loginMutation = useAction(loginAction, {
|
const loginMutation = useAction(loginAction, {
|
||||||
|
onError: (e) => {
|
||||||
|
if (e.serverError) toast.error(e.serverError);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (!data.success) {
|
if (data.success && data.totpSessionId) {
|
||||||
toast.error(data.failure.reason);
|
|
||||||
} else if (data.success && data.totpSessionId) {
|
|
||||||
setTotpSessionId(data.totpSessionId);
|
setTotpSessionId(data.totpSessionId);
|
||||||
} else {
|
} else {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
@ -26,18 +27,27 @@ export function LoginContainer() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifyTotpMutation = useAction(verifyTotpAction, {
|
const verifyTotpMutation = useAction(verifyTotpAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (totpSessionId) {
|
if (totpSessionId) {
|
||||||
return <TotpForm loading={verifyTotpMutation.status === 'executing'} onSubmit={(totpCode) => verifyTotpMutation.execute({ totpCode, totpSessionId })} />;
|
return (
|
||||||
|
<TotpForm
|
||||||
|
loading={verifyTotpMutation.status === 'executing'}
|
||||||
|
onSubmit={(totpCode) => verifyTotpMutation.execute({ totpCode, totpSessionId })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LoginForm loading={loginMutation.status === 'executing'} onSubmit={({ email, password }) => loginMutation.execute({ username: email, password })} />;
|
return (
|
||||||
|
<LoginForm
|
||||||
|
loading={loginMutation.status === 'executing'}
|
||||||
|
onSubmit={({ email, password }) => loginMutation.execute({ username: email, password })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,18 @@ export const RegisterContainer: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const registerMutation = useAction(registerAction, {
|
const registerMutation = useAction(registerAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return <RegisterForm onSubmit={({ email, password }) => registerMutation.execute({ username: email, password })} loading={registerMutation.status === 'executing'} />;
|
return (
|
||||||
|
<RegisterForm
|
||||||
|
onSubmit={({ email, password }) => registerMutation.execute({ username: email, password })}
|
||||||
|
loading={registerMutation.status === 'executing'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,18 +15,14 @@ export const ResetPasswordContainer: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetPasswordMutation = useAction(resetPasswordAction, {
|
const resetPasswordMutation = useAction(resetPasswordAction, {
|
||||||
onSuccess: (data) => {
|
onError: (error) => {
|
||||||
if (!data.success) {
|
if (error.serverError) toast.error(error.serverError);
|
||||||
toast.error(data.failure.reason);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelRequestMutation = useAction(cancelResetPasswordAction, {
|
const cancelRequestMutation = useAction(cancelResetPasswordAction, {
|
||||||
onSuccess: (data) => {
|
onError: (error) => {
|
||||||
if (!data.success) {
|
if (error.serverError) toast.error(error.serverError);
|
||||||
toast.error(data.failure.reason);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
@ -13,10 +11,10 @@ import { updateAppAction } from '@/actions/app-actions/update-app-action';
|
||||||
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
|
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
|
||||||
import { AppLogo } from '@/components/AppLogo';
|
import { AppLogo } from '@/components/AppLogo';
|
||||||
import { AppStatus } from '@/components/AppStatus';
|
import { AppStatus } from '@/components/AppStatus';
|
||||||
import { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
|
||||||
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
||||||
import { AppService } from '@/server/services/apps/apps.service';
|
import { AppService } from '@/server/services/apps/apps.service';
|
||||||
import { resetAppAction } from '@/actions/app-actions/reset-app-action';
|
import { resetAppAction } from '@/actions/app-actions/reset-app-action';
|
||||||
|
import { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||||
import { InstallModal } from '../InstallModal';
|
import { InstallModal } from '../InstallModal';
|
||||||
import { StopModal } from '../StopModal';
|
import { StopModal } from '../StopModal';
|
||||||
import { UninstallModal } from '../UninstallModal';
|
import { UninstallModal } from '../UninstallModal';
|
||||||
|
@ -24,19 +22,20 @@ import { UpdateModal } from '../UpdateModal';
|
||||||
import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal';
|
import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal';
|
||||||
import { AppActions } from '../AppActions';
|
import { AppActions } from '../AppActions';
|
||||||
import { AppDetailsTabs } from '../AppDetailsTabs';
|
import { AppDetailsTabs } from '../AppDetailsTabs';
|
||||||
import { FormValues } from '../InstallForm';
|
|
||||||
import { ResetAppModal } from '../ResetAppModal';
|
import { ResetAppModal } from '../ResetAppModal';
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
app: Awaited<ReturnType<AppService['getApp']>>;
|
|
||||||
localDomain?: string;
|
|
||||||
}
|
|
||||||
type OpenType = 'local' | 'domain' | 'local_domain';
|
type OpenType = 'local' | 'domain' | 'local_domain';
|
||||||
|
|
||||||
export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
type AppDetailsContainerProps = {
|
||||||
const [customStatus, setCustomStatus] = React.useState<AppStatusEnum>(app.status);
|
app: Awaited<ReturnType<AppService['getApp']>>;
|
||||||
|
localDomain?: string;
|
||||||
|
optimisticStatus: AppStatusEnum;
|
||||||
|
setOptimisticStatus: (status: AppStatusEnum) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppDetailsContainer: React.FC<AppDetailsContainerProps> = ({ app, localDomain, optimisticStatus, setOptimisticStatus }) => {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const installDisclosure = useDisclosure();
|
const installDisclosure = useDisclosure();
|
||||||
const uninstallDisclosure = useDisclosure();
|
const uninstallDisclosure = useDisclosure();
|
||||||
const stopDisclosure = useDisclosure();
|
const stopDisclosure = useDisclosure();
|
||||||
|
@ -45,130 +44,78 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
||||||
const resetAppDisclosure = useDisclosure();
|
const resetAppDisclosure = useDisclosure();
|
||||||
|
|
||||||
const installMutation = useAction(installAppAction, {
|
const installMutation = useAction(installAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
setCustomStatus(app.status);
|
},
|
||||||
toast.error(data.failure.reason);
|
onExecute: () => {
|
||||||
} else {
|
setOptimisticStatus('installing');
|
||||||
setCustomStatus('running');
|
installDisclosure.close();
|
||||||
toast.success(t('apps.app-details.install-success'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const uninstallMutation = useAction(uninstallAppAction, {
|
const uninstallMutation = useAction(uninstallAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
setCustomStatus(app.status);
|
},
|
||||||
toast.error(data.failure.reason);
|
onExecute: () => {
|
||||||
} else {
|
uninstallDisclosure.close();
|
||||||
setCustomStatus('missing');
|
setOptimisticStatus('uninstalling');
|
||||||
toast.success(t('apps.app-details.uninstall-success'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const stopMutation = useAction(stopAppAction, {
|
const stopMutation = useAction(stopAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
setCustomStatus(app.status);
|
},
|
||||||
toast.error(data.failure.reason);
|
onExecute: () => {
|
||||||
} else {
|
stopDisclosure.close();
|
||||||
setCustomStatus('stopped');
|
setOptimisticStatus('stopping');
|
||||||
toast.success(t('apps.app-details.stop-success'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const startMutation = useAction(startAppAction, {
|
const startMutation = useAction(startAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
setCustomStatus(app.status);
|
},
|
||||||
toast.error(data.failure.reason);
|
onExecute: () => {
|
||||||
} else {
|
setOptimisticStatus('starting');
|
||||||
setCustomStatus('running');
|
|
||||||
toast.success(t('apps.app-details.start-success'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useAction(updateAppAction, {
|
const updateMutation = useAction(updateAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
setCustomStatus(app.status);
|
if (e.serverError) toast.error(e.serverError);
|
||||||
|
},
|
||||||
if (!data.success) {
|
onExecute: () => {
|
||||||
toast.error(data.failure.reason);
|
updateDisclosure.close();
|
||||||
} else {
|
setOptimisticStatus('updating');
|
||||||
toast.success(t('apps.app-details.update-success'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateConfigMutation = useAction(updateAppConfigAction, {
|
const updateConfigMutation = useAction(updateAppConfigAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onExecute: () => {
|
||||||
toast.success(t('apps.app-details.update-config-success'));
|
updateSettingsDisclosure.close();
|
||||||
}
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('apps.app-details.update-config-success'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetMutation = useAction(resetAppAction, {
|
const resetMutation = useAction(resetAppAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
resetAppDisclosure.close();
|
onExecute: () => {
|
||||||
} else {
|
resetAppDisclosure.open();
|
||||||
resetAppDisclosure.close();
|
setOptimisticStatus('stopping');
|
||||||
toast.success(t('apps.app-details.app-reset-success'));
|
|
||||||
setCustomStatus('running');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);
|
const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);
|
||||||
|
|
||||||
const handleInstallSubmit = async (values: FormValues) => {
|
|
||||||
setCustomStatus('installing');
|
|
||||||
installDisclosure.close();
|
|
||||||
installMutation.execute({ id: app.id, form: values });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnistallSubmit = () => {
|
|
||||||
setCustomStatus('uninstalling');
|
|
||||||
uninstallDisclosure.close();
|
|
||||||
uninstallMutation.execute({ id: app.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStopSubmit = () => {
|
|
||||||
setCustomStatus('stopping');
|
|
||||||
stopDisclosure.close();
|
|
||||||
stopMutation.execute({ id: app.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartSubmit = async () => {
|
|
||||||
setCustomStatus('starting');
|
|
||||||
startMutation.execute({ id: app.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateSettingsSubmit = async (values: FormValues) => {
|
|
||||||
updateSettingsDisclosure.close();
|
|
||||||
updateConfigMutation.execute({ id: app.id, form: values });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateSubmit = async () => {
|
|
||||||
setCustomStatus('updating');
|
|
||||||
updateDisclosure.close();
|
|
||||||
updateMutation.execute({ id: app.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetSubmit = () => {
|
|
||||||
setCustomStatus('stopping');
|
|
||||||
resetMutation.execute({ id: app.id });
|
|
||||||
resetAppDisclosure.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openResetAppModal = () => {
|
const openResetAppModal = () => {
|
||||||
updateSettingsDisclosure.close();
|
updateSettingsDisclosure.close();
|
||||||
resetAppDisclosure.open();
|
resetAppDisclosure.open();
|
||||||
|
@ -200,19 +147,46 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card" data-testid="app-details">
|
<div className="card" data-testid="app-details">
|
||||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} info={app.info} />
|
<InstallModal
|
||||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} info={app.info} />
|
onSubmit={(values) => installMutation.execute({ id: app.id, form: values })}
|
||||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} info={app.info} />
|
isOpen={installDisclosure.isOpen}
|
||||||
<UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} info={app.info} newVersion={newVersion} />
|
onClose={installDisclosure.close}
|
||||||
<ResetAppModal onConfirm={handleResetSubmit} isOpen={resetAppDisclosure.isOpen} onClose={resetAppDisclosure.close} info={app.info} isLoading={resetMutation.status === 'executing'} />
|
info={app.info}
|
||||||
|
/>
|
||||||
|
<StopModal
|
||||||
|
onConfirm={() => stopMutation.execute({ id: app.id })}
|
||||||
|
isOpen={stopDisclosure.isOpen}
|
||||||
|
onClose={stopDisclosure.close}
|
||||||
|
info={app.info}
|
||||||
|
/>
|
||||||
|
<UninstallModal
|
||||||
|
onConfirm={() => uninstallMutation.execute({ id: app.id })}
|
||||||
|
isOpen={uninstallDisclosure.isOpen}
|
||||||
|
onClose={uninstallDisclosure.close}
|
||||||
|
info={app.info}
|
||||||
|
/>
|
||||||
|
<UpdateModal
|
||||||
|
onConfirm={() => updateMutation.execute({ id: app.id })}
|
||||||
|
isOpen={updateDisclosure.isOpen}
|
||||||
|
onClose={updateDisclosure.close}
|
||||||
|
info={app.info}
|
||||||
|
newVersion={newVersion}
|
||||||
|
/>
|
||||||
|
<ResetAppModal
|
||||||
|
onConfirm={() => resetMutation.execute({ id: app.id })}
|
||||||
|
isOpen={resetAppDisclosure.isOpen}
|
||||||
|
onClose={resetAppDisclosure.close}
|
||||||
|
info={app.info}
|
||||||
|
isLoading={resetMutation.status === 'executing'}
|
||||||
|
/>
|
||||||
<UpdateSettingsModal
|
<UpdateSettingsModal
|
||||||
onSubmit={handleUpdateSettingsSubmit}
|
onSubmit={(values) => updateConfigMutation.execute({ id: app.id, form: values })}
|
||||||
isOpen={updateSettingsDisclosure.isOpen}
|
isOpen={updateSettingsDisclosure.isOpen}
|
||||||
onClose={updateSettingsDisclosure.close}
|
onClose={updateSettingsDisclosure.close}
|
||||||
info={app.info}
|
info={app.info}
|
||||||
config={castAppConfig(app?.config)}
|
config={castAppConfig(app?.config)}
|
||||||
onReset={openResetAppModal}
|
onReset={openResetAppModal}
|
||||||
status={customStatus}
|
status={optimisticStatus}
|
||||||
/>
|
/>
|
||||||
<div className="card-header d-flex flex-column flex-md-row">
|
<div className="card-header d-flex flex-column flex-md-row">
|
||||||
<AppLogo id={app.id} size={130} alt={app.info.name} />
|
<AppLogo id={app.id} size={130} alt={app.info.name} />
|
||||||
|
@ -222,7 +196,7 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
||||||
<span className="badge bg-muted mt-2 text-white">{app.info.version}</span>
|
<span className="badge bg-muted mt-2 text-white">{app.info.version}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="mt-1 text-muted text-center text-md-start mb-2">{app.info.short_desc}</span>
|
<span className="mt-1 text-muted text-center text-md-start mb-2">{app.info.short_desc}</span>
|
||||||
<div className="mb-1">{customStatus !== 'missing' && <AppStatus status={customStatus} />}</div>
|
<div className="mb-1">{optimisticStatus !== 'missing' && <AppStatus status={optimisticStatus} />}</div>
|
||||||
<AppActions
|
<AppActions
|
||||||
localDomain={localDomain}
|
localDomain={localDomain}
|
||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
|
@ -233,9 +207,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
||||||
onUninstall={uninstallDisclosure.open}
|
onUninstall={uninstallDisclosure.open}
|
||||||
onInstall={installDisclosure.open}
|
onInstall={installDisclosure.open}
|
||||||
onOpen={handleOpen}
|
onOpen={handleOpen}
|
||||||
onStart={handleStartSubmit}
|
onStart={() => startMutation.execute({ id: app.id })}
|
||||||
app={app}
|
app={app}
|
||||||
status={customStatus}
|
status={optimisticStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { startTransition, useOptimistic } from 'react';
|
||||||
|
import { useSocket } from '@/lib/socket/useSocket';
|
||||||
|
import { AppStatus } from '@/server/db/schema';
|
||||||
|
import { AppService } from '@/server/services/apps/apps.service';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useAction } from 'next-safe-action/hook';
|
||||||
|
import { revalidateAppAction } from '@/actions/app-actions/revalidate-app';
|
||||||
|
import { AppDetailsContainer } from './AppDetailsContainer';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
app: Awaited<ReturnType<AppService['getApp']>>;
|
||||||
|
localDomain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppDetailsWrapper = (props: IProps) => {
|
||||||
|
const { app } = props;
|
||||||
|
const t = useTranslations();
|
||||||
|
const [optimisticStatus, setOptimisticStatus] = useOptimistic<AppStatus>(app.status);
|
||||||
|
const revalidateAppMutation = useAction(revalidateAppAction);
|
||||||
|
|
||||||
|
const changeStatus = (status: AppStatus) => {
|
||||||
|
startTransition(() => {
|
||||||
|
setOptimisticStatus(status);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useSocket({
|
||||||
|
onEvent: (event, data) => {
|
||||||
|
if (data.error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case 'install_success':
|
||||||
|
toast.success(t('apps.app-details.install-success'));
|
||||||
|
changeStatus('running');
|
||||||
|
break;
|
||||||
|
case 'install_error':
|
||||||
|
toast.error(t('server-messages.errors.app-failed-to-install', { id: app.id }));
|
||||||
|
changeStatus('missing');
|
||||||
|
break;
|
||||||
|
case 'start_success':
|
||||||
|
toast.success(t('apps.app-details.start-success'));
|
||||||
|
changeStatus('running');
|
||||||
|
break;
|
||||||
|
case 'start_error':
|
||||||
|
toast.error(t('server-messages.errors.app-failed-to-start', { id: app.id }));
|
||||||
|
changeStatus('stopped');
|
||||||
|
break;
|
||||||
|
case 'stop_success':
|
||||||
|
toast.success(t('apps.app-details.stop-success'));
|
||||||
|
changeStatus('stopped');
|
||||||
|
break;
|
||||||
|
case 'stop_error':
|
||||||
|
toast.error(t('server-messages.errors.app-failed-to-stop', { id: app.id }));
|
||||||
|
changeStatus('running');
|
||||||
|
break;
|
||||||
|
case 'uninstall_success':
|
||||||
|
toast.success(t('apps.app-details.uninstall-success'));
|
||||||
|
changeStatus('missing');
|
||||||
|
break;
|
||||||
|
case 'uninstall_error':
|
||||||
|
toast.error(t('server-messages.errors.app-failed-to-uninstall', { id: app.id }));
|
||||||
|
changeStatus('stopped');
|
||||||
|
break;
|
||||||
|
case 'update_success':
|
||||||
|
toast.success(t('apps.app-details.update-success'));
|
||||||
|
changeStatus('running');
|
||||||
|
break;
|
||||||
|
case 'update_error':
|
||||||
|
toast.error(t('server-messages.errors.app-failed-to-update', { id: app.id }));
|
||||||
|
changeStatus('stopped');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateAppMutation.execute({ id: app.id });
|
||||||
|
},
|
||||||
|
selector: { type: 'app', data: { property: 'appId', value: app.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return <AppDetailsContainer app={app} optimisticStatus={optimisticStatus} setOptimisticStatus={setOptimisticStatus} />;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export { AppDetailsWrapper } from './AppDetailsWrapper';
|
|
@ -4,7 +4,7 @@ import { Metadata } from 'next';
|
||||||
import { db } from '@/server/db';
|
import { db } from '@/server/db';
|
||||||
import { getTranslatorFromCookie } from '@/lib/get-translator';
|
import { getTranslatorFromCookie } from '@/lib/get-translator';
|
||||||
import { getSettings } from '@/server/core/TipiConfig';
|
import { getSettings } from '@/server/core/TipiConfig';
|
||||||
import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer';
|
import { AppDetailsWrapper } from './components/AppDetailsContainer';
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const translator = await getTranslatorFromCookie();
|
const translator = await getTranslatorFromCookie();
|
||||||
|
@ -19,5 +19,5 @@ export default async function AppDetailsPage({ params }: { params: { id: string
|
||||||
const app = await appsService.getApp(params.id);
|
const app = await appsService.getApp(params.id);
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
return <AppDetailsContainer app={app} localDomain={settings.localDomain} />;
|
return <AppDetailsWrapper app={app} localDomain={settings.localDomain} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,13 +34,12 @@ export const ChangePasswordForm = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const changePasswordMutation = useAction(changePasswordAction, {
|
const changePasswordMutation = useAction(changePasswordAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
toast.success(t('password-change-success'));
|
toast.success(t('password-change-success'));
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,13 +27,12 @@ export const ChangeUsernameForm = ({ username }: Props) => {
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
const changeUsernameMutation = useAction(changeUsernameAction, {
|
const changeUsernameMutation = useAction(changeUsernameAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
toast.success(t('change-username.success'));
|
toast.success(t('change-username.success'));
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -29,28 +29,26 @@ export const OtpForm = (props: { totpEnabled: boolean }) => {
|
||||||
onExecute: () => {
|
onExecute: () => {
|
||||||
setupOtpDisclosure.close();
|
setupOtpDisclosure.close();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
setPassword('');
|
||||||
|
if (e.serverError) toast.error(e.serverError);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (!data.success) {
|
setKey(data.key);
|
||||||
setPassword('');
|
setUri(data.uri);
|
||||||
toast.error(data.failure.reason);
|
|
||||||
} else {
|
|
||||||
setKey(data.key);
|
|
||||||
setUri(data.uri);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupTotpMutation = useAction(setupTotpAction, {
|
const setupTotpMutation = useAction(setupTotpAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
setTotpCode('');
|
||||||
setTotpCode('');
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
setTotpCode('');
|
setTotpCode('');
|
||||||
setKey('');
|
setKey('');
|
||||||
setUri('');
|
setUri('');
|
||||||
toast.success(t('2fa-enable-success'));
|
toast.success(t('2fa-enable-success'));
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -58,13 +56,12 @@ export const OtpForm = (props: { totpEnabled: boolean }) => {
|
||||||
onExecute: () => {
|
onExecute: () => {
|
||||||
disableOtpDisclosure.close();
|
disableOtpDisclosure.close();
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
setPassword('');
|
||||||
setPassword('');
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
toast.success(t('2fa-disable-success'));
|
toast.success(t('2fa-disable-success'));
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,12 @@ export const SettingsContainer = ({ initialValues, currentLocale }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const updateSettingsMutation = useAction(updateSettingsAction, {
|
const updateSettingsMutation = useAction(updateSettingsAction, {
|
||||||
onSuccess: (data) => {
|
onError: (e) => {
|
||||||
if (!data.success) {
|
if (e.serverError) toast.error(e.serverError);
|
||||||
toast.error(data.failure.reason);
|
},
|
||||||
} else {
|
onSuccess: () => {
|
||||||
toast.success(t('settings.settings.settings-updated'));
|
toast.success(t('settings.settings.settings-updated'));
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,7 +35,12 @@ export const SettingsContainer = ({ initialValues, currentLocale }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<SettingsForm initalValues={initialValues} currentLocale={currentLocale} loading={updateSettingsMutation.status === 'executing'} onSubmit={onSubmit} />
|
<SettingsForm
|
||||||
|
initalValues={initialValues}
|
||||||
|
currentLocale={currentLocale}
|
||||||
|
loading={updateSettingsMutation.status === 'executing'}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
23
src/app/actions/app-actions/revalidate-app.ts
Normal file
23
src/app/actions/app-actions/revalidate-app.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { action } from '@/lib/safe-action';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import { handleActionError } from '../utils/handle-action-error';
|
||||||
|
|
||||||
|
const input = z.object({ id: z.string() });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an app id, revalidates the app and app store pages on demand.
|
||||||
|
*/
|
||||||
|
export const revalidateAppAction = action(input, async ({ id }) => {
|
||||||
|
try {
|
||||||
|
revalidatePath('/apps');
|
||||||
|
revalidatePath(`/app/${id}`);
|
||||||
|
revalidatePath(`/app-store/${id}`);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
return handleActionError(e);
|
||||||
|
}
|
||||||
|
});
|
|
@ -9,12 +9,7 @@ export const handleActionError = async (e: unknown) => {
|
||||||
const errorVariables = e instanceof TranslatedError ? e.variableValues : {};
|
const errorVariables = e instanceof TranslatedError ? e.variableValues : {};
|
||||||
|
|
||||||
const translator = await getTranslatorFromCookie();
|
const translator = await getTranslatorFromCookie();
|
||||||
const messageTranslated = translator(message as MessageKey, errorVariables);
|
const messageTranslated = e instanceof TranslatedError ? translator(message as MessageKey, errorVariables) : message;
|
||||||
|
|
||||||
return {
|
throw new Error(messageTranslated as string);
|
||||||
success: false as const,
|
|
||||||
failure: {
|
|
||||||
reason: messageTranslated,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
import { createSafeActionClient } from 'next-safe-action';
|
import { createSafeActionClient } from 'next-safe-action';
|
||||||
|
|
||||||
export const action = createSafeActionClient();
|
export const action = createSafeActionClient({
|
||||||
|
handleReturnedServerError: (e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Error from server', e);
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverError: e.message || 'An unexpected error occurred',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
71
src/lib/socket/useSocket.ts
Normal file
71
src/lib/socket/useSocket.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { SocketEvent, socketEventSchema } from '@runtipi/shared/src/schemas/socket';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import io from 'socket.io-client';
|
||||||
|
|
||||||
|
// Data selector is used to select a specific property/value from the data object if it exists
|
||||||
|
type DataSelector<T> = {
|
||||||
|
property: keyof Extract<SocketEvent, { type: T }>['data'];
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Selector<T, U> = {
|
||||||
|
type: T;
|
||||||
|
event?: U;
|
||||||
|
data?: DataSelector<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props<T, U> = {
|
||||||
|
onEvent: (event: Extract<Extract<SocketEvent, { type: T }>['event'], U>, data: Extract<SocketEvent, { type: T }>['data']) => void;
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
selector: Selector<T, U>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSocket = <T extends SocketEvent['type'], U extends SocketEvent['event']>(props: Props<T, U>) => {
|
||||||
|
const { onEvent, onError, selector } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = io('http://localhost:3935');
|
||||||
|
|
||||||
|
const handleEvent = (type: SocketEvent['type'], rawData: unknown) => {
|
||||||
|
const parsedEvent = socketEventSchema.safeParse(rawData);
|
||||||
|
|
||||||
|
if (!parsedEvent.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { event, data } = parsedEvent.data;
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
if (selector.type !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector.event && selector.event !== event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const property = selector.data?.property as keyof SocketEvent['data'];
|
||||||
|
if (selector.data && selector.data.value !== data[property]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - This is fine
|
||||||
|
onEvent(event, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on(selector.type as string, (data) => {
|
||||||
|
handleEvent(selector.type, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (error: string) => {
|
||||||
|
onError?.(String(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket?.off(selector.type as string);
|
||||||
|
socket.disconnect();
|
||||||
|
};
|
||||||
|
}, [onError, onEvent, selector, selector.type]);
|
||||||
|
};
|
|
@ -44,7 +44,7 @@ export class AppQueries {
|
||||||
* @param {NewApp} data - The data to create the app with
|
* @param {NewApp} data - The data to create the app with
|
||||||
*/
|
*/
|
||||||
public async createApp(data: NewApp) {
|
public async createApp(data: NewApp) {
|
||||||
const newApps = await this.db.insert(appTable).values(data).returning();
|
const newApps = await this.db.insert(appTable).values(data).returning().execute();
|
||||||
return newApps[0];
|
return newApps[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import validator from 'validator';
|
||||||
import { App } from '@/server/db/schema';
|
import { App } from '@/server/db/schema';
|
||||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||||
import { TranslatedError } from '@/server/utils/errors';
|
import { TranslatedError } from '@/server/utils/errors';
|
||||||
import { Database } from '@/server/db';
|
import { Database, db } from '@/server/db';
|
||||||
import { AppInfo } from '@runtipi/shared';
|
import { AppInfo } from '@runtipi/shared';
|
||||||
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
|
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
|
||||||
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
||||||
|
@ -32,7 +32,7 @@ const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(fi
|
||||||
export class AppServiceClass {
|
export class AppServiceClass {
|
||||||
private queries;
|
private queries;
|
||||||
|
|
||||||
constructor(p: Database) {
|
constructor(p: Database = db) {
|
||||||
this.queries = new AppQueries(p);
|
this.queries = new AppQueries(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,13 +55,15 @@ export class AppServiceClass {
|
||||||
try {
|
try {
|
||||||
await this.queries.updateApp(app.id, { status: 'starting' });
|
await this.queries.updateApp(app.id, { status: 'starting' });
|
||||||
|
|
||||||
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) }).then(({ success }) => {
|
eventDispatcher
|
||||||
if (success) {
|
.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) })
|
||||||
this.queries.updateApp(app.id, { status: 'running' });
|
.then(({ success }) => {
|
||||||
} else {
|
if (success) {
|
||||||
this.queries.updateApp(app.id, { status: 'stopped' });
|
this.queries.updateApp(app.id, { status: 'running' });
|
||||||
}
|
} else {
|
||||||
});
|
this.queries.updateApp(app.id, { status: 'stopped' });
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.queries.updateApp(app.id, { status: 'stopped' });
|
await this.queries.updateApp(app.id, { status: 'stopped' });
|
||||||
Logger.error(e);
|
Logger.error(e);
|
||||||
|
@ -87,7 +89,12 @@ export class AppServiceClass {
|
||||||
|
|
||||||
await this.queries.updateApp(appName, { status: 'starting' });
|
await this.queries.updateApp(appName, { status: 'starting' });
|
||||||
const eventDispatcher = new EventDispatcher('startApp');
|
const eventDispatcher = new EventDispatcher('startApp');
|
||||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: appName, form: castAppConfig(app.config) });
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync({
|
||||||
|
type: 'app',
|
||||||
|
command: 'start',
|
||||||
|
appid: appName,
|
||||||
|
form: castAppConfig(app.config),
|
||||||
|
});
|
||||||
await eventDispatcher.close();
|
await eventDispatcher.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -166,18 +173,17 @@ export class AppServiceClass {
|
||||||
|
|
||||||
// Run script
|
// Run script
|
||||||
const eventDispatcher = new EventDispatcher('installApp');
|
const eventDispatcher = new EventDispatcher('installApp');
|
||||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form });
|
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }).then(({ success, stdout }) => {
|
||||||
await eventDispatcher.close();
|
if (success) {
|
||||||
|
this.queries.updateApp(id, { status: 'running' });
|
||||||
|
} else {
|
||||||
|
this.queries.deleteApp(id);
|
||||||
|
Logger.error(`Failed to install app ${id}: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!success) {
|
eventDispatcher.close();
|
||||||
await this.queries.deleteApp(id);
|
});
|
||||||
Logger.error(`Failed to install app ${id}: ${stdout}`);
|
|
||||||
throw new TranslatedError('server-messages.errors.app-failed-to-install', { id });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedApp = await this.queries.updateApp(id, { status: 'running' });
|
|
||||||
return updatedApp;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,7 +246,12 @@ export class AppServiceClass {
|
||||||
await eventDispatcher.close();
|
await eventDispatcher.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form, isVisibleOnGuestDashboard: form.isVisibleOnGuestDashboard });
|
const updatedApp = await this.queries.updateApp(id, {
|
||||||
|
exposed: exposed || false,
|
||||||
|
domain: domain || null,
|
||||||
|
config: form,
|
||||||
|
isVisibleOnGuestDashboard: form.isVisibleOnGuestDashboard,
|
||||||
|
});
|
||||||
return updatedApp;
|
return updatedApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,16 +275,16 @@ export class AppServiceClass {
|
||||||
await this.queries.updateApp(id, { status: 'stopping' });
|
await this.queries.updateApp(id, { status: 'stopping' });
|
||||||
|
|
||||||
const eventDispatcher = new EventDispatcher('stopApp');
|
const eventDispatcher = new EventDispatcher('stopApp');
|
||||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) });
|
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) }).then(({ success, stdout }) => {
|
||||||
await eventDispatcher.close();
|
if (success) {
|
||||||
|
this.queries.updateApp(id, { status: 'stopped' });
|
||||||
|
} else {
|
||||||
|
Logger.error(`Failed to stop app ${id}: ${stdout}`);
|
||||||
|
this.queries.updateApp(id, { status: 'running' });
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
eventDispatcher.close();
|
||||||
await this.queries.updateApp(id, { status: 'stopped' });
|
});
|
||||||
} else {
|
|
||||||
await this.queries.updateApp(id, { status: 'running' });
|
|
||||||
Logger.error(`Failed to stop app ${id}: ${stdout}`);
|
|
||||||
throw new TranslatedError('server-messages.errors.app-failed-to-stop', { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedApp = await this.queries.getApp(id);
|
const updatedApp = await this.queries.getApp(id);
|
||||||
return updatedApp;
|
return updatedApp;
|
||||||
|
@ -298,16 +309,17 @@ export class AppServiceClass {
|
||||||
await this.queries.updateApp(id, { status: 'uninstalling' });
|
await this.queries.updateApp(id, { status: 'uninstalling' });
|
||||||
|
|
||||||
const eventDispatcher = new EventDispatcher('uninstallApp');
|
const eventDispatcher = new EventDispatcher('uninstallApp');
|
||||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) });
|
eventDispatcher
|
||||||
await eventDispatcher.close();
|
.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) })
|
||||||
|
.then(({ stdout, success }) => {
|
||||||
if (!success) {
|
if (success) {
|
||||||
await this.queries.updateApp(id, { status: 'stopped' });
|
this.queries.deleteApp(id);
|
||||||
Logger.error(`Failed to uninstall app ${id}: ${stdout}`);
|
} else {
|
||||||
throw new TranslatedError('server-messages.errors.app-failed-to-uninstall', { id });
|
this.queries.updateApp(id, { status: 'stopped' });
|
||||||
}
|
Logger.error(`Failed to uninstall app ${id}: ${stdout}`);
|
||||||
|
}
|
||||||
await this.queries.deleteApp(id);
|
eventDispatcher.close();
|
||||||
|
});
|
||||||
|
|
||||||
return { id, status: 'missing', config: {} };
|
return { id, status: 'missing', config: {} };
|
||||||
};
|
};
|
||||||
|
@ -350,7 +362,12 @@ export class AppServiceClass {
|
||||||
await this.queries.updateApp(id, { status: 'updating' });
|
await this.queries.updateApp(id, { status: 'updating' });
|
||||||
|
|
||||||
const eventDispatcher = new EventDispatcher('updateApp');
|
const eventDispatcher = new EventDispatcher('updateApp');
|
||||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) });
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync({
|
||||||
|
type: 'app',
|
||||||
|
command: 'update',
|
||||||
|
appid: id,
|
||||||
|
form: castAppConfig(app.config),
|
||||||
|
});
|
||||||
await eventDispatcher.close();
|
await eventDispatcher.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
Loading…
Reference in a new issue