mirror of
https://github.com/meienberger/runtipi.git
synced 2024-09-11 12:15:48 +03:00
refactor: isolate database client in its own package (#1581)
* refactor: isolate database client in its own package * refactor: bot suggestions
This commit is contained in:
parent
52a99490b1
commit
e988991bca
@ -8,3 +8,4 @@ jest.config.js
|
||||
/app-data
|
||||
/apps
|
||||
package.json
|
||||
packages/**
|
||||
|
@ -18,7 +18,7 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
'import/no-cycle': 2,
|
||||
'import/no-cycle': 1,
|
||||
'import/no-named-as-default-member': 0,
|
||||
'import/no-named-as-default': 0,
|
||||
'no-console': 1,
|
||||
|
22
Dockerfile
22
Dockerfile
@ -17,6 +17,11 @@ ENV LOCAL=${LOCAL}
|
||||
RUN npm install pnpm@9.4.0 -g
|
||||
RUN apk add --no-cache curl python3 make g++ git
|
||||
|
||||
WORKDIR /deps
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
|
||||
# ---- RUNNER BASE ----
|
||||
FROM node_base AS runner_base
|
||||
|
||||
@ -28,12 +33,10 @@ FROM builder_base AS dashboard_builder
|
||||
|
||||
WORKDIR /dashboard
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
|
||||
COPY ./pnpm-workspace.yaml ./
|
||||
COPY ./package*.json ./
|
||||
COPY ./packages/shared ./packages/shared
|
||||
COPY ./packages/db ./packages/db
|
||||
COPY ./scripts ./scripts
|
||||
COPY ./public ./public
|
||||
|
||||
@ -55,11 +58,10 @@ WORKDIR /worker
|
||||
|
||||
ARG TARGETARCH
|
||||
ENV TARGETARCH=${TARGETARCH}
|
||||
ARG DOCKER_COMPOSE_VERSION="v2.27.0"
|
||||
ARG DOCKER_COMPOSE_VERSION="v2.29.1"
|
||||
|
||||
RUN echo "Building for ${TARGETARCH}"
|
||||
|
||||
|
||||
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
|
||||
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-aarch64"; \
|
||||
elif [ "${TARGETARCH}" = "amd64" ]; then \
|
||||
@ -70,20 +72,15 @@ RUN if [ "${TARGETARCH}" = "arm64" ]; then \
|
||||
|
||||
RUN chmod +x docker-binary
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch --ignore-scripts
|
||||
|
||||
COPY ./pnpm-workspace.yaml ./
|
||||
COPY ./packages/worker/package.json ./packages/worker/package.json
|
||||
COPY ./packages/shared/package.json ./packages/shared/package.json
|
||||
COPY ./packages/db/package.json ./packages/db/package.json
|
||||
COPY ./packages/shared/package.json ./packages/shared/package.json
|
||||
|
||||
RUN pnpm install -r --prefer-offline
|
||||
|
||||
COPY ./packages ./packages
|
||||
COPY ./packages/worker/build.js ./packages/worker/build.js
|
||||
COPY ./packages/worker/src ./packages/worker/src
|
||||
COPY ./packages/worker/package.json ./packages/worker/package.json
|
||||
COPY ./packages/worker/assets ./packages/worker/assets
|
||||
|
||||
# Print TIPI_VERSION to the console
|
||||
RUN echo "TIPI_VERSION: ${SENTRY_RELEASE}"
|
||||
@ -99,6 +96,7 @@ WORKDIR /worker
|
||||
|
||||
COPY --from=worker_builder /worker/packages/worker/dist .
|
||||
COPY --from=worker_builder /worker/packages/worker/assets ./assets
|
||||
COPY --from=worker_builder /worker/packages/db/assets/migrations ./assets/migrations
|
||||
COPY --from=worker_builder /worker/docker-binary /usr/local/bin/docker-compose
|
||||
|
||||
WORKDIR /dashboard
|
||||
|
@ -10,7 +10,7 @@ RUN apk add --no-cache python3 make g++ curl git
|
||||
RUN npm install pnpm@9.4.0 pm2 -g
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG DOCKER_COMPOSE_VERSION="v2.23.3"
|
||||
ARG DOCKER_COMPOSE_VERSION="v2.29.1"
|
||||
ENV TARGETARCH=${TARGETARCH}
|
||||
ENV NODE_ENV="development"
|
||||
|
||||
@ -35,12 +35,16 @@ RUN pnpm fetch --ignore-scripts
|
||||
COPY ./package*.json ./
|
||||
COPY ./packages/worker/package.json ./packages/worker/package.json
|
||||
COPY ./packages/shared/package.json ./packages/shared/package.json
|
||||
COPY ./packages/db/package.json ./packages/db/package.json
|
||||
|
||||
COPY ./scripts ./scripts
|
||||
COPY ./public ./public
|
||||
|
||||
RUN pnpm install -r --prefer-offline
|
||||
|
||||
COPY ./packages ./packages
|
||||
|
||||
COPY ./packages/db/assets/migrations ./packages/worker/assets/migrations
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./next.config.mjs ./next.config.mjs
|
||||
|
||||
|
@ -44,6 +44,9 @@
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi']
|
||||
test: ["CMD-SHELL", "pg_isready -d tipi -U tipi"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
@ -48,7 +48,7 @@ services:
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
@ -71,6 +71,7 @@ services:
|
||||
- ./src:/app/src
|
||||
- ./packages/worker/src:/app/packages/worker/src
|
||||
- ./packages/shared/src:/app/packages/shared/src
|
||||
- ./packages/db/src:/app/packages/db/src
|
||||
# Data
|
||||
- ${RUNTIPI_MEDIA_PATH:-.}/media:/data/media
|
||||
- ${RUNTIPI_STATE_PATH:-.}/state:/data/state
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { appTable } from '@/server/db/schema';
|
||||
import { appTable } from '@runtipi/db';
|
||||
import { setSettings } from './helpers/settings';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase, db } from './helpers/db';
|
||||
@ -49,7 +49,7 @@ test('logged out users can see the apps on the guest dashboard', async ({ browse
|
||||
await page.goto('/');
|
||||
await expect(page.getByText(/Hello World web server/)).toBeVisible();
|
||||
const locator = page.locator('text=Actual Budget');
|
||||
expect(locator).not.toBeVisible();
|
||||
await expect(locator).not.toBeVisible();
|
||||
|
||||
const [newPage] = await Promise.all([context.waitForEvent('page'), await page.getByRole('link', { name: /Hello World/ }).click()]);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as argon2 from 'argon2';
|
||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||
import { userTable } from '@/server/db/schema';
|
||||
import { type BrowserContext, expect, type Page } from '@playwright/test';
|
||||
import { userTable } from '@runtipi/db';
|
||||
import { db } from '../helpers/db';
|
||||
import { testUser } from '../helpers/constants';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from '../../src/server/db/schema';
|
||||
import * as schema from '@runtipi/db';
|
||||
|
||||
const connectionString = `postgresql://tipi:${process.env.POSTGRES_PASSWORD}@${process.env.SERVER_IP}:5432/tipi?connect_timeout=300`;
|
||||
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@runtipi/db": "workspace:^",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@sentry/nextjs": "^8.22.0",
|
||||
|
19
packages/db/package.json
Normal file
19
packages/db/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@runtipi/db",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 0"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"drizzle-orm": "^0.32.1",
|
||||
"pg": "^8.12.0"
|
||||
}
|
||||
}
|
42
packages/db/src/client.ts
Normal file
42
packages/db/src/client.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import * as schema from './schema';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
|
||||
export type IDatabase = NodePgDatabase<typeof schema>;
|
||||
|
||||
type IConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
|
||||
export interface IDbClient {
|
||||
db: IDatabase;
|
||||
}
|
||||
|
||||
export class DbClient {
|
||||
public db: IDatabase;
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(config: IConfig, logger: ILogger) {
|
||||
this.logger = logger;
|
||||
const connectionString = `postgresql://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}?connect_timeout=300`;
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
});
|
||||
|
||||
pool.on('error', async (err) => {
|
||||
this.logger.error('Unexpected error on idle client:', err);
|
||||
});
|
||||
|
||||
pool.on('connect', () => {
|
||||
this.logger.info('Connected to the database successfully.');
|
||||
});
|
||||
|
||||
this.db = drizzle(pool, { schema });
|
||||
}
|
||||
}
|
3
packages/db/src/index.ts
Normal file
3
packages/db/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './schema';
|
||||
export { Migrator, type IMigrator } from './migrator';
|
||||
export { type IDbClient, type IDatabase as Database, DbClient } from './client';
|
57
packages/db/src/migrator.ts
Normal file
57
packages/db/src/migrator.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import path from 'node:path';
|
||||
import pg from 'pg';
|
||||
import { migrate } from '@runtipi/postgres-migrations';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
|
||||
type MigrationParams = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
migrationsFolder: string;
|
||||
};
|
||||
|
||||
export interface IMigrator {
|
||||
runPostgresMigrations(params: MigrationParams): Promise<void>;
|
||||
}
|
||||
|
||||
export class Migrator implements IMigrator {
|
||||
constructor(private logger: ILogger) {}
|
||||
|
||||
public runPostgresMigrations = async (params: MigrationParams) => {
|
||||
const { database, host, username, password, port, migrationsFolder } = params;
|
||||
|
||||
this.logger.info('Starting database migration');
|
||||
|
||||
this.logger.info(`Connecting to database ${database} on ${host} as ${username} on port ${port}`);
|
||||
|
||||
const client = new pg.Client({ user: username, host, database, password, port: Number(port) });
|
||||
await client.connect();
|
||||
|
||||
this.logger.info('Client connected');
|
||||
|
||||
try {
|
||||
const { rows } = await client.query('SELECT * FROM migrations');
|
||||
// if rows contains a migration with name 'Initial1657299198975' (legacy typeorm) delete table migrations. As all migrations are idempotent we can safely delete the table and start over.
|
||||
if (rows.find((row) => row.name === 'Initial1657299198975')) {
|
||||
this.logger.info('Found legacy migration. Deleting table migrations');
|
||||
await client.query('DROP TABLE migrations');
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.info('Migrations table not found, creating it', e);
|
||||
}
|
||||
|
||||
this.logger.info('Running migrations');
|
||||
try {
|
||||
await migrate({ client }, path.join(migrationsFolder, 'migrations'), { skipCreateMigrationTable: true });
|
||||
} catch (e) {
|
||||
this.logger.error('Error running migrations. Dropping table migrations and trying again', e);
|
||||
await client.query('DROP TABLE migrations');
|
||||
await migrate({ client }, path.join(migrationsFolder, 'migrations'), { skipCreateMigrationTable: true });
|
||||
}
|
||||
|
||||
this.logger.info('Migration complete');
|
||||
await client.end();
|
||||
};
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
||||
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
||||
import { pgTable, pgEnum, integer, varchar, timestamp, serial, boolean, text, jsonb } from 'drizzle-orm/pg-core';
|
||||
|
||||
const updateStatusEnum = pgEnum('update_status_enum', ['SUCCESS', 'FAILED']);
|
30
packages/db/tsconfig.json
Normal file
30
packages/db/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2022",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"module": "preserve",
|
||||
"noEmit": true,
|
||||
"lib": [
|
||||
"es2022"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mjs",
|
||||
"**/*.js",
|
||||
"**/*.jsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
@ -3,7 +3,11 @@
|
||||
"target": "es2017",
|
||||
"baseUrl": ".",
|
||||
"paths": {},
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@ -18,9 +22,19 @@
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.js", "**/*.jsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mjs",
|
||||
"**/*.js",
|
||||
"**/*.jsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript', 'prettier'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@ -46,7 +41,7 @@ module.exports = {
|
||||
'arrow-body-style': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
'no-console': 0,
|
||||
'import/no-cycle': 2,
|
||||
'import/no-cycle': 1,
|
||||
},
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
|
@ -29,7 +29,7 @@ async function bundle() {
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
},
|
||||
minify: true,
|
||||
minify: process.env.LOCAL !== 'true',
|
||||
plugins,
|
||||
};
|
||||
|
||||
|
@ -38,18 +38,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
"@runtipi/db": "workspace:^",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@sentry/integrations": "^7.114.0",
|
||||
"@sentry/node": "^8.22.0",
|
||||
"bullmq": "^5.12.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.32.1",
|
||||
"hono": "^4.5.3",
|
||||
"inversify": "^6.0.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"pg": "^8.12.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"socket.io": "^4.7.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"systeminformation": "^5.23.3",
|
||||
"tar-fs": "^3.0.6",
|
||||
"web-push": "^3.6.7",
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { jwt } from 'hono/jwt';
|
||||
import { prettyJSON } from 'hono/pretty-json';
|
||||
import { secureHeaders } from 'hono/secure-headers';
|
||||
import { Hono } from 'hono';
|
||||
import type { Hono } from 'hono';
|
||||
import { getEnv } from './lib/environment';
|
||||
import { AppExecutors, SystemExecutors } from './services';
|
||||
import { SystemExecutors } from './services';
|
||||
import { container } from './inversify.config';
|
||||
import type { IAppExecutors } from './services/app/app.executors';
|
||||
|
||||
const apps = new AppExecutors();
|
||||
const system = new SystemExecutors();
|
||||
|
||||
export const setupRoutes = (app: Hono) => {
|
||||
const apps = container.get<IAppExecutors>('IAppExecutors');
|
||||
app.get('/healthcheck', (c) => c.text('OK', 200));
|
||||
|
||||
app.use('*', prettyJSON());
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata';
|
||||
import 'source-map-support/register';
|
||||
|
||||
import { SystemEvent, cleanseErrorData } from '@runtipi/shared';
|
||||
import { type SystemEvent, cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
import path from 'node:path';
|
||||
import Redis from 'ioredis';
|
||||
@ -11,13 +12,16 @@ import { extraErrorDataIntegration } from '@sentry/integrations';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
|
||||
import { runPostgresMigrations } from '@/lib/migrations';
|
||||
import { startWorker } from './watcher/watcher';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { AppExecutors, RepoExecutors } from './services';
|
||||
import { RepoExecutors } from './services';
|
||||
import { setupRoutes } from './api';
|
||||
import { DATA_DIR } from './config';
|
||||
import { APP_DIR, DATA_DIR } from './config';
|
||||
import { socketManager } from './lib/socket';
|
||||
import { container } from './inversify.config';
|
||||
import type { IAppExecutors } from './services/app/app.executors';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
import type { IMigrator } from '@runtipi/db';
|
||||
|
||||
const envFile = path.join(DATA_DIR, '.env');
|
||||
|
||||
@ -42,6 +46,11 @@ const setupSentry = (release?: string) => {
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
// TODO: Convert to class with injected dependencies
|
||||
const logger = container.get<ILogger>('ILogger');
|
||||
const migrator = container.get<IMigrator>('IMigrator');
|
||||
const appExecutor = container.get<IAppExecutors>('IAppExecutors');
|
||||
|
||||
await logger.flush();
|
||||
|
||||
logger.info(`Running tipi-worker version: ${process.env.TIPI_VERSION}`);
|
||||
@ -51,11 +60,7 @@ const main = async () => {
|
||||
logger.info('Copying system files...');
|
||||
await copySystemFiles(envMap);
|
||||
|
||||
if (
|
||||
envMap.get('ALLOW_ERROR_MONITORING') === 'true' &&
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
process.env.LOCAL !== 'true'
|
||||
) {
|
||||
if (envMap.get('ALLOW_ERROR_MONITORING') === 'true' && process.env.NODE_ENV === 'production' && process.env.LOCAL !== 'true') {
|
||||
logger.info(
|
||||
`Anonymous error monitoring is enabled, to disable it add "allowErrorMonitoring": false to your settings.json file. Version: ${process.env.TIPI_VERSION}`,
|
||||
);
|
||||
@ -119,12 +124,14 @@ const main = async () => {
|
||||
await repeatQueue.close();
|
||||
|
||||
logger.info('Running database migrations...');
|
||||
await runPostgresMigrations({
|
||||
postgresHost: envMap.get('POSTGRES_HOST') as string,
|
||||
postgresDatabase: envMap.get('POSTGRES_DBNAME') as string,
|
||||
postgresUsername: envMap.get('POSTGRES_USERNAME') as string,
|
||||
postgresPassword: envMap.get('POSTGRES_PASSWORD') as string,
|
||||
postgresPort: envMap.get('POSTGRES_PORT') as string,
|
||||
|
||||
await migrator.runPostgresMigrations({
|
||||
host: envMap.get('POSTGRES_HOST') as string,
|
||||
database: envMap.get('POSTGRES_DBNAME') as string,
|
||||
username: envMap.get('POSTGRES_USERNAME') as string,
|
||||
password: envMap.get('POSTGRES_PASSWORD') as string,
|
||||
port: Number(envMap.get('POSTGRES_PORT')),
|
||||
migrationsFolder: path.join(APP_DIR, 'assets'),
|
||||
});
|
||||
|
||||
// Set status to running
|
||||
@ -138,7 +145,6 @@ const main = async () => {
|
||||
await cache.quit();
|
||||
|
||||
// Start all apps
|
||||
const appExecutor = new AppExecutors();
|
||||
logger.info('Starting all apps...');
|
||||
|
||||
// Fire and forget
|
||||
@ -153,7 +159,11 @@ const main = async () => {
|
||||
});
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(e);
|
||||
logger.error('Failed to start');
|
||||
|
||||
if (e instanceof Error) {
|
||||
logger.error(e.stack);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
process.exit(1);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { type ILogger, Logger } from '@runtipi/shared/node';
|
||||
import { type IDbClient, type IMigrator, DbClient, Migrator } from '@runtipi/db';
|
||||
import { Container } from 'inversify';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import { DATA_DIR } from './config';
|
||||
import { ISocketManager, SocketManager } from './lib/socket/SocketManager';
|
||||
import { type ISocketManager, SocketManager } from './lib/socket/SocketManager';
|
||||
import { getEnv } from './lib/environment';
|
||||
import { AppExecutors, type IAppExecutors } from './services/app/app.executors';
|
||||
|
||||
export function createContainer() {
|
||||
const { postgresHost, postgresPort, postgresDatabase, postgresPassword, postgresUsername } = getEnv();
|
||||
|
||||
const container = new Container();
|
||||
|
||||
container.bind<ILogger>('ILogger').toDynamicValue(() => {
|
||||
@ -14,6 +17,28 @@ export function createContainer() {
|
||||
});
|
||||
container.bind<ISocketManager>('ISocketManager').to(SocketManager).inSingletonScope();
|
||||
|
||||
container
|
||||
.bind<IDbClient>('IDbClient')
|
||||
.toDynamicValue((context) => {
|
||||
return new DbClient(
|
||||
{
|
||||
host: postgresHost,
|
||||
port: Number(postgresPort),
|
||||
database: postgresDatabase,
|
||||
password: postgresPassword,
|
||||
username: postgresUsername,
|
||||
},
|
||||
context.container.get<ILogger>('ILogger'),
|
||||
);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
container.bind<IMigrator>('IMigrator').toDynamicValue((context) => {
|
||||
return new Migrator(context.container.get<ILogger>('ILogger'));
|
||||
});
|
||||
|
||||
container.bind<IAppExecutors>('IAppExecutors').to(AppExecutors);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
import pg from 'pg';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { getEnv } from '../environment';
|
||||
import { logger } from '../logger';
|
||||
|
||||
class DbClientSingleton {
|
||||
private client: pg.Client | null;
|
||||
|
||||
constructor() {
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (!this.client) {
|
||||
try {
|
||||
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
|
||||
|
||||
this.client = new pg.Client({
|
||||
host: postgresHost,
|
||||
database: postgresDatabase,
|
||||
user: postgresUsername,
|
||||
password: postgresPassword,
|
||||
port: Number(postgresPort),
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
logger.info('Database connection successfully established.');
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to the database:', error);
|
||||
this.client = null; // Ensure client is null to retry connection on next call
|
||||
throw error; // Rethrow or handle error as needed
|
||||
}
|
||||
}
|
||||
|
||||
this.client.on('error', (error) => {
|
||||
Sentry.captureException(error);
|
||||
logger.error('Database connection error:', error);
|
||||
this.client = null;
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (!this.client) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
|
||||
const dbClientSingleton = new DbClientSingleton();
|
||||
|
||||
export const getDbClient = async () => {
|
||||
return dbClientSingleton.getClient();
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { getDbClient } from './db';
|
@ -1 +0,0 @@
|
||||
export { runPostgresMigrations } from './run-migration';
|
@ -1,57 +0,0 @@
|
||||
import path from 'path';
|
||||
import pg from 'pg';
|
||||
import { migrate } from '@runtipi/postgres-migrations';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { APP_DIR } from '@/config/constants';
|
||||
|
||||
type MigrationParams = {
|
||||
postgresHost: string;
|
||||
postgresDatabase: string;
|
||||
postgresUsername: string;
|
||||
postgresPassword: string;
|
||||
postgresPort: string;
|
||||
};
|
||||
|
||||
export const runPostgresMigrations = async (params: MigrationParams) => {
|
||||
const assetsFolder = path.join(APP_DIR, 'assets');
|
||||
|
||||
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = params;
|
||||
|
||||
logger.info('Starting database migration');
|
||||
|
||||
logger.info(`Connecting to database ${postgresDatabase} on ${postgresHost} as ${postgresUsername} on port ${postgresPort}`);
|
||||
|
||||
const client = new pg.Client({
|
||||
user: postgresUsername,
|
||||
host: postgresHost,
|
||||
database: postgresDatabase,
|
||||
password: postgresPassword,
|
||||
port: Number(postgresPort),
|
||||
});
|
||||
await client.connect();
|
||||
|
||||
logger.info('Client connected');
|
||||
|
||||
try {
|
||||
const { rows } = await client.query('SELECT * FROM migrations');
|
||||
// if rows contains a migration with name 'Initial1657299198975' (legacy typeorm) delete table migrations. As all migrations are idempotent we can safely delete the table and start over.
|
||||
if (rows.find((row) => row.name === 'Initial1657299198975')) {
|
||||
logger.info('Found legacy migration. Deleting table migrations');
|
||||
await client.query('DROP TABLE migrations');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info('Migrations table not found, creating it');
|
||||
}
|
||||
|
||||
logger.info('Running migrations');
|
||||
try {
|
||||
await migrate({ client }, path.join(assetsFolder, 'migrations'), { skipCreateMigrationTable: true });
|
||||
} catch (e) {
|
||||
logger.error('Error running migrations. Dropping table migrations and trying again');
|
||||
await client.query('DROP TABLE migrations');
|
||||
await migrate({ client }, path.join(assetsFolder, 'migrations'), { skipCreateMigrationTable: true });
|
||||
}
|
||||
|
||||
logger.info('Migration complete');
|
||||
await client.end();
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { SocketEvent } from '@runtipi/shared';
|
||||
import type { SocketEvent } from '@runtipi/shared';
|
||||
import type { ILogger } from '@runtipi/shared/src/node';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Server } from 'socket.io';
|
||||
import { handleViewAppLogsEvent, handleViewRuntipiLogsEvent } from '../docker';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { ILogger } from '@runtipi/shared/src/node';
|
||||
|
||||
export interface ISocketManager {
|
||||
init(): void;
|
||||
|
@ -1,27 +1,26 @@
|
||||
import fs from 'fs';
|
||||
import fs from 'node:fs';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import * as sharedNode from '@runtipi/shared/node';
|
||||
import { AppExecutors } from '../app.executors';
|
||||
import type { IAppExecutors } from '../app.executors';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import * as dockerHelpers from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const { pathExists } = sharedNode;
|
||||
|
||||
const { appsRepoId } = getEnv();
|
||||
|
||||
describe('test: app executors', () => {
|
||||
const appExecutors = new AppExecutors();
|
||||
const appExecutors = container.get<IAppExecutors>('IAppExecutors');
|
||||
|
||||
describe('test: installApp()', () => {
|
||||
it('should run correct compose script', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const config = createAppConfig({}, false);
|
||||
|
||||
// act
|
||||
@ -32,10 +31,7 @@ describe('test: app executors', () => {
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(message).toBe(`App ${config.id} installed successfully`);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
config.id,
|
||||
'up --detach --force-recreate --remove-orphans --pull always',
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(config.id, 'up --detach --force-recreate --remove-orphans --pull always');
|
||||
expect(envExists).toBe(true);
|
||||
spy.mockRestore();
|
||||
});
|
||||
@ -94,20 +90,14 @@ describe('test: app executors', () => {
|
||||
await fs.promises.mkdir(path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data'), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.promises.writeFile(
|
||||
path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data', filename),
|
||||
'test',
|
||||
);
|
||||
await fs.promises.writeFile(path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'test');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(APP_DATA_DIR, config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(
|
||||
path.join(APP_DATA_DIR, config.id, 'data', filename),
|
||||
'utf-8',
|
||||
);
|
||||
const data = await fs.promises.readFile(path.join(APP_DATA_DIR, config.id, 'data', filename), 'utf-8');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(data).toBe('test');
|
||||
@ -121,20 +111,14 @@ describe('test: app executors', () => {
|
||||
await fs.promises.mkdir(path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data'), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.promises.writeFile(
|
||||
path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data', filename),
|
||||
'yeah',
|
||||
);
|
||||
await fs.promises.writeFile(path.join(DATA_DIR, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'yeah');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(APP_DATA_DIR, config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(
|
||||
path.join(APP_DATA_DIR, config.id, 'data', filename),
|
||||
'utf-8',
|
||||
);
|
||||
const data = await fs.promises.readFile(path.join(APP_DATA_DIR, config.id, 'data', filename), 'utf-8');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(data).toBe('test');
|
||||
@ -142,9 +126,7 @@ describe('test: app executors', () => {
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const config = createAppConfig();
|
||||
|
||||
// act
|
||||
@ -169,9 +151,7 @@ describe('test: app executors', () => {
|
||||
describe('test: stopApp()', () => {
|
||||
it('should run correct compose script', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const config = createAppConfig();
|
||||
|
||||
// act
|
||||
@ -186,9 +166,7 @@ describe('test: app executors', () => {
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const config = createAppConfig();
|
||||
|
||||
// act
|
||||
@ -204,9 +182,7 @@ describe('test: app executors', () => {
|
||||
describe('test: restartApp()', () => {
|
||||
it('should start and stop the app', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: 'done', stderr: '' }));
|
||||
const config = createAppConfig();
|
||||
|
||||
// act
|
||||
@ -215,19 +191,14 @@ describe('test: app executors', () => {
|
||||
// assert
|
||||
expect(success).toBe(true);
|
||||
expect(message).toBe(`App ${config.id} restarted successfully`);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
config.id,
|
||||
'up --detach --force-recreate --remove-orphans --pull always',
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(config.id, 'up --detach --force-recreate --remove-orphans --pull always');
|
||||
expect(spy).toHaveBeenCalledWith(config.id, 'rm --force --stop');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// arrange
|
||||
const spy = vi
|
||||
.spyOn(dockerHelpers, 'compose')
|
||||
.mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.reject(new Error('test')));
|
||||
const config = createAppConfig();
|
||||
|
||||
// act
|
||||
@ -276,10 +247,7 @@ describe('test: app executors', () => {
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(oldFolder);
|
||||
const content = await fs.promises.readFile(
|
||||
path.join(oldFolder, 'docker-compose.yml'),
|
||||
'utf-8',
|
||||
);
|
||||
const content = await fs.promises.readFile(path.join(oldFolder, 'docker-compose.yml'), 'utf-8');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(content).not.toBe('test');
|
||||
|
@ -1,26 +1,42 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { AppEventForm, SocketEvent, appInfoSchema, sanitizePath } from '@runtipi/shared';
|
||||
import { execAsync, type ILogger, pathExists } from '@runtipi/shared/node';
|
||||
import { type AppEventForm, type SocketEvent, appInfoSchema, sanitizePath } from '@runtipi/shared';
|
||||
import { copyDataDir, generateEnvFile } from './app.helpers';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { compose } from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { getDbClient } from '@/lib/db';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { getDockerCompose } from '@/config/docker-templates';
|
||||
import { ArchiveManager } from '@/lib/archive/ArchiveManager';
|
||||
import { socketManager } from '@/lib/socket';
|
||||
import { type App, appTable, type IDbClient } from '@runtipi/db';
|
||||
import { and, eq, ne } from 'drizzle-orm';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import type { ISocketManager } from '@/lib/socket/SocketManager';
|
||||
|
||||
export class AppExecutors {
|
||||
private readonly logger;
|
||||
export interface IAppExecutors {
|
||||
regenerateAppEnv(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;
|
||||
installApp(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;
|
||||
stopApp(appId: string, form: AppEventForm, skipEnvGeneration?: boolean): Promise<{ success: boolean; message: string }>;
|
||||
restartApp(appId: string, form: AppEventForm, skipEnvGeneration?: boolean): Promise<{ success: boolean; message: string }>;
|
||||
startApp(appId: string, form: AppEventForm, skipEnvGeneration?: boolean): Promise<{ success: boolean; message: string }>;
|
||||
uninstallApp(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;
|
||||
resetApp(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;
|
||||
updateApp(appId: string, form: AppEventForm, performBackup: boolean): Promise<{ success: boolean; message: string }>;
|
||||
startAllApps(forceStartAll?: boolean): Promise<void>;
|
||||
backupApp(appId: string): Promise<{ success: boolean; message: string }>;
|
||||
restoreApp(appId: string, filename: string): Promise<{ success: boolean; message: string }>;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class AppExecutors implements IAppExecutors {
|
||||
private archiveManager: ArchiveManager;
|
||||
|
||||
constructor() {
|
||||
this.logger = logger;
|
||||
constructor(
|
||||
@inject('ILogger') private logger: ILogger,
|
||||
@inject('IDbClient') private dbClient: IDbClient,
|
||||
@inject('ISocketManager') private socketManager: ISocketManager,
|
||||
) {
|
||||
this.archiveManager = new ArchiveManager();
|
||||
}
|
||||
|
||||
@ -35,7 +51,7 @@ export class AppExecutors {
|
||||
});
|
||||
|
||||
if (err instanceof Error) {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event,
|
||||
data: { appId, error: err.message, appStatus: newStatus },
|
||||
@ -44,7 +60,7 @@ export class AppExecutors {
|
||||
return { success: false, message: err.message };
|
||||
}
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event,
|
||||
data: { appId, error: String(err), appStatus: newStatus },
|
||||
@ -86,19 +102,14 @@ export class AppExecutors {
|
||||
if (await pathExists(path.join(repoPath, 'docker-compose.json'))) {
|
||||
try {
|
||||
// Generate docker-compose.yml file
|
||||
const rawComposeConfig = await fs.promises.readFile(
|
||||
path.join(repoPath, 'docker-compose.json'),
|
||||
'utf-8',
|
||||
);
|
||||
const rawComposeConfig = await fs.promises.readFile(path.join(repoPath, 'docker-compose.json'), 'utf-8');
|
||||
const jsonComposeConfig = JSON.parse(rawComposeConfig);
|
||||
|
||||
const composeFile = getDockerCompose(jsonComposeConfig.services, form);
|
||||
|
||||
await fs.promises.writeFile(dockerFilePath, composeFile);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Error generating docker-compose.yml file for app ${appId}. Falling back to default docker-compose.yml`,
|
||||
);
|
||||
this.logger.error(`Error generating docker-compose.yml file for app ${appId}. Falling back to default docker-compose.yml`);
|
||||
this.logger.error(err);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
@ -117,7 +128,7 @@ export class AppExecutors {
|
||||
await this.ensureAppDir(appId, form);
|
||||
await generateEnvFile(appId, form);
|
||||
|
||||
await socketManager.emit({ type: 'app', event: 'generate_env_success', data: { appId } });
|
||||
await this.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');
|
||||
@ -131,16 +142,14 @@ export class AppExecutors {
|
||||
*/
|
||||
public installApp = async (appId: string, form: AppEventForm) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'installing' },
|
||||
});
|
||||
|
||||
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 {
|
||||
this.logger.info(`Installing app ${appId}. No User ID or Group ID found.`);
|
||||
}
|
||||
@ -150,9 +159,7 @@ export class AppExecutors {
|
||||
const { appDirPath, repoPath, appDataDirPath } = this.getAppPaths(appId);
|
||||
|
||||
// Check if app exists in repo
|
||||
const apps = await fs.promises.readdir(
|
||||
path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps'),
|
||||
);
|
||||
const apps = await fs.promises.readdir(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps'));
|
||||
|
||||
if (!apps.includes(appId)) {
|
||||
this.logger.error(`App ${appId} not found in repo ${appsRepoId}`);
|
||||
@ -193,7 +200,7 @@ export class AppExecutors {
|
||||
|
||||
this.logger.info(`Docker-compose up for app ${appId} finished`);
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'install_success',
|
||||
data: { appId, appStatus: 'running' },
|
||||
@ -220,7 +227,7 @@ export class AppExecutors {
|
||||
return { success: true, message: `App ${appId} is not an app. Skipping...` };
|
||||
}
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'stopping' },
|
||||
@ -237,14 +244,14 @@ export class AppExecutors {
|
||||
|
||||
this.logger.info(`App ${appId} stopped`);
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'stop_success',
|
||||
data: { appId, appStatus: 'stopped' },
|
||||
});
|
||||
|
||||
const client = await getDbClient();
|
||||
await client?.query('UPDATE app SET status = $1 WHERE id = $2', ['stopped', appId]);
|
||||
await this.dbClient.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, appId));
|
||||
|
||||
return { success: true, message: `App ${appId} stopped successfully` };
|
||||
} catch (err) {
|
||||
return this.handleAppError(err, appId, 'stop_error', 'running');
|
||||
@ -261,7 +268,7 @@ export class AppExecutors {
|
||||
return { success: true, message: `App ${appId} is not an app. Skipping...` };
|
||||
}
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'restarting' },
|
||||
@ -293,7 +300,7 @@ export class AppExecutors {
|
||||
|
||||
this.logger.info(`App ${appId} restarted`);
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'restart_success',
|
||||
data: { appId, appStatus: 'running' },
|
||||
@ -307,7 +314,7 @@ export class AppExecutors {
|
||||
|
||||
public startApp = async (appId: string, form: AppEventForm, skipEnvGeneration = false) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'starting' },
|
||||
@ -326,14 +333,13 @@ export class AppExecutors {
|
||||
|
||||
this.logger.info(`App ${appId} started`);
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'start_success',
|
||||
data: { appId, appStatus: 'running' },
|
||||
});
|
||||
|
||||
const client = await getDbClient();
|
||||
await client?.query('UPDATE app SET status = $1 WHERE id = $2', ['running', appId]);
|
||||
await this.dbClient.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, appId));
|
||||
return { success: true, message: `App ${appId} started successfully` };
|
||||
} catch (err) {
|
||||
return this.handleAppError(err, appId, 'start_error', 'stopped');
|
||||
@ -342,7 +348,7 @@ export class AppExecutors {
|
||||
|
||||
public uninstallApp = async (appId: string, form: AppEventForm) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'uninstalling' },
|
||||
@ -378,14 +384,14 @@ export class AppExecutors {
|
||||
|
||||
this.logger.info(`App ${appId} uninstalled`);
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'uninstall_success',
|
||||
data: { appId, appStatus: 'missing' },
|
||||
});
|
||||
|
||||
const client = await getDbClient();
|
||||
await client?.query(`DELETE FROM app WHERE id = $1`, [appId]);
|
||||
await this.dbClient.db.delete(appTable).where(eq(appTable.id, appId));
|
||||
|
||||
return { success: true, message: `App ${appId} uninstalled successfully` };
|
||||
} catch (err) {
|
||||
return this.handleAppError(err, appId, 'uninstall_error', 'stopped');
|
||||
@ -394,7 +400,7 @@ export class AppExecutors {
|
||||
|
||||
public resetApp = async (appId: string, form: AppEventForm) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'resetting' },
|
||||
@ -410,9 +416,7 @@ export class AppExecutors {
|
||||
await compose(appId, 'down --remove-orphans --volumes');
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('conflict')) {
|
||||
this.logger.warn(
|
||||
`Could not reset app ${appId}. Most likely there have been made changes to the compose file.`,
|
||||
);
|
||||
this.logger.warn(`Could not reset app ${appId}. Most likely there have been made changes to the compose file.`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
@ -440,14 +444,14 @@ export class AppExecutors {
|
||||
this.logger.info(`Running docker-compose up for app ${appId}`);
|
||||
await compose(appId, 'up -d');
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'reset_success',
|
||||
data: { appId, appStatus: 'running' },
|
||||
});
|
||||
|
||||
const client = await getDbClient();
|
||||
await client?.query(`UPDATE app SET status = $1 WHERE id = $2`, ['running', appId]);
|
||||
await this.dbClient.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, appId));
|
||||
|
||||
return { success: true, message: `App ${appId} reset successfully` };
|
||||
} catch (err) {
|
||||
return this.handleAppError(err, appId, 'reset_error', 'stopped');
|
||||
@ -461,7 +465,7 @@ export class AppExecutors {
|
||||
await this.backupApp(appId);
|
||||
}
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'updating' },
|
||||
@ -476,9 +480,7 @@ export class AppExecutors {
|
||||
await compose(appId, 'up --detach --force-recreate --remove-orphans');
|
||||
await compose(appId, 'down --rmi all --remove-orphans');
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`App ${appId} has likely a broken docker-compose.yml file. Continuing with update...`,
|
||||
);
|
||||
this.logger.warn(`App ${appId} has likely a broken docker-compose.yml file. Continuing with update...`);
|
||||
}
|
||||
|
||||
this.logger.info(`Deleting folder ${appDirPath}`);
|
||||
@ -491,7 +493,7 @@ export class AppExecutors {
|
||||
|
||||
await compose(appId, 'pull');
|
||||
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'update_success',
|
||||
data: { appId, appStatus: 'stopped' },
|
||||
@ -508,35 +510,32 @@ export class AppExecutors {
|
||||
*/
|
||||
public startAllApps = async (forceStartAll = false) => {
|
||||
try {
|
||||
const client = await getDbClient();
|
||||
let rows: { id: string; config: Record<string, unknown> }[] = [];
|
||||
let apps: App[] = [];
|
||||
|
||||
if (!forceStartAll) {
|
||||
// Get all apps with status running
|
||||
const result = await client?.query(`SELECT * FROM app WHERE status = 'running'`);
|
||||
rows = result?.rows || [];
|
||||
apps = await this.dbClient.db.select().from(appTable).where(eq(appTable.status, 'running'));
|
||||
} else {
|
||||
// Get all apps
|
||||
const result = await client?.query(`SELECT * FROM app`);
|
||||
rows = result?.rows || [];
|
||||
apps = await this.dbClient.db.select().from(appTable);
|
||||
}
|
||||
|
||||
// Update all apps with status different than running or stopped to stopped
|
||||
await client?.query(
|
||||
`UPDATE app SET status = 'stopped' WHERE status != 'stopped' AND status != 'running' AND status != 'missing'`,
|
||||
);
|
||||
await this.dbClient.db
|
||||
.update(appTable)
|
||||
.set({ status: 'stopped' })
|
||||
.where(and(ne(appTable.status, 'running'), ne(appTable.status, 'stopped'), ne(appTable.status, 'missing')));
|
||||
|
||||
// Start all apps
|
||||
for (const row of rows) {
|
||||
for (const row of apps) {
|
||||
const { id, config } = row;
|
||||
|
||||
const { success } = await this.startApp(id, config as AppEventForm);
|
||||
|
||||
if (!success) {
|
||||
this.logger.error(`Error starting app ${id}`);
|
||||
await client?.query(`UPDATE app SET status = $1 WHERE id = $2`, ['stopped', id]);
|
||||
await this.dbClient.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
||||
} else {
|
||||
await client?.query(`UPDATE app SET status = $1 WHERE id = $2`, ['running', id]);
|
||||
await this.dbClient.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, id));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -546,7 +545,7 @@ export class AppExecutors {
|
||||
|
||||
public backupApp = async (appId: string) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'backing_up' },
|
||||
@ -578,11 +577,7 @@ export class AppExecutors {
|
||||
|
||||
// Check if the user config folder exists and if it does copy it too
|
||||
if (await pathExists(path.join(DATA_DIR, 'user-config', appId))) {
|
||||
await fs.promises.cp(
|
||||
path.join(DATA_DIR, 'user-config', appId),
|
||||
path.join(tempDir, 'user-config'),
|
||||
{ recursive: true },
|
||||
);
|
||||
await fs.promises.cp(path.join(DATA_DIR, 'user-config', appId), path.join(tempDir, 'user-config'), { recursive: true });
|
||||
}
|
||||
|
||||
this.logger.info('Creating archive...');
|
||||
@ -594,10 +589,7 @@ export class AppExecutors {
|
||||
|
||||
// Move the archive to the backup directory
|
||||
await fs.promises.mkdir(backupDir, { recursive: true });
|
||||
await fs.promises.cp(
|
||||
path.join(tempDir, `${backupName}.tar.gz`),
|
||||
path.join(backupDir, `${backupName}.tar.gz`),
|
||||
);
|
||||
await fs.promises.cp(path.join(tempDir, `${backupName}.tar.gz`), path.join(backupDir, `${backupName}.tar.gz`));
|
||||
|
||||
// Remove the temp backup folder
|
||||
await fs.promises.rm(tempDir, { force: true, recursive: true });
|
||||
@ -605,7 +597,7 @@ export class AppExecutors {
|
||||
this.logger.info('Backup completed!');
|
||||
|
||||
// Done
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'backup_success',
|
||||
data: { appId, appStatus: 'stopped' },
|
||||
@ -618,7 +610,7 @@ export class AppExecutors {
|
||||
|
||||
public restoreApp = async (appId: string, filename: string) => {
|
||||
try {
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'status_change',
|
||||
data: { appId, appStatus: 'restoring' },
|
||||
@ -627,7 +619,6 @@ export class AppExecutors {
|
||||
const { appDataDirPath, appDirPath } = this.getAppPaths(appId);
|
||||
const restoreDir = path.join('/tmp', appId);
|
||||
const archive = path.join(DATA_DIR, 'backups', appId, filename);
|
||||
const client = await getDbClient();
|
||||
|
||||
this.logger.info('Restoring app from backup...');
|
||||
|
||||
@ -666,11 +657,7 @@ export class AppExecutors {
|
||||
|
||||
// Copy user config foler if it exists
|
||||
if (await pathExists(path.join(restoreDir, 'user-config'))) {
|
||||
await fs.promises.cp(
|
||||
path.join(restoreDir, 'user-config'),
|
||||
path.join(DATA_DIR, 'user-config', appId),
|
||||
{ recursive: true },
|
||||
);
|
||||
await fs.promises.cp(path.join(restoreDir, 'user-config'), path.join(DATA_DIR, 'user-config', appId), { recursive: true });
|
||||
}
|
||||
|
||||
// Delete restore folder
|
||||
@ -681,15 +668,13 @@ export class AppExecutors {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const configParsed = appInfoSchema.safeParse(JSON.parse(configFileRaw));
|
||||
await client?.query(`UPDATE app SET version = $1 WHERE id = $2`, [
|
||||
configParsed.data?.tipi_version,
|
||||
appId,
|
||||
]);
|
||||
|
||||
await this.dbClient.db.update(appTable).set({ version: configParsed.data?.tipi_version }).where(eq(appTable.id, appId));
|
||||
|
||||
this.logger.info(`App ${appId} restored!`);
|
||||
|
||||
// Done
|
||||
await socketManager.emit({
|
||||
await this.socketManager.emit({
|
||||
type: 'app',
|
||||
event: 'restore_success',
|
||||
data: { appId, appStatus: 'stopped' },
|
||||
|
@ -1,21 +1,13 @@
|
||||
import { eventSchema } from '@runtipi/shared';
|
||||
import { Worker } from 'bullmq';
|
||||
import { AppExecutors, RepoExecutors } from '@/services';
|
||||
import { RepoExecutors } from '@/services';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAppExecutors } from 'src/services/app/app.executors';
|
||||
|
||||
const {
|
||||
installApp,
|
||||
resetApp,
|
||||
startApp,
|
||||
stopApp,
|
||||
restartApp,
|
||||
uninstallApp,
|
||||
updateApp,
|
||||
regenerateAppEnv,
|
||||
backupApp,
|
||||
restoreApp,
|
||||
} = new AppExecutors();
|
||||
const { installApp, resetApp, startApp, stopApp, restartApp, uninstallApp, updateApp, regenerateAppEnv, backupApp, restoreApp } =
|
||||
container.get<IAppExecutors>('IAppExecutors');
|
||||
const { cloneRepo, pullRepo } = new RepoExecutors();
|
||||
|
||||
const runCommand = async (jobData: unknown) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'reflect-metadata';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
|
@ -44,6 +44,9 @@ importers:
|
||||
'@radix-ui/react-tabs':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@runtipi/db':
|
||||
specifier: workspace:^
|
||||
version: link:packages/db
|
||||
'@runtipi/postgres-migrations':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@ -340,6 +343,21 @@ importers:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
|
||||
packages/db:
|
||||
dependencies:
|
||||
'@runtipi/postgres-migrations':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@runtipi/shared':
|
||||
specifier: workspace:^
|
||||
version: link:../shared
|
||||
drizzle-orm:
|
||||
specifier: ^0.32.1
|
||||
version: 0.32.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@types/react@18.3.3)(pg@8.12.0)(react@18.3.1)
|
||||
pg:
|
||||
specifier: ^8.12.0
|
||||
version: 8.12.0
|
||||
|
||||
packages/shared:
|
||||
dependencies:
|
||||
fs-extra:
|
||||
@ -390,6 +408,9 @@ importers:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.0
|
||||
'@runtipi/db':
|
||||
specifier: workspace:^
|
||||
version: link:../db
|
||||
'@runtipi/postgres-migrations':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@ -408,6 +429,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
drizzle-orm:
|
||||
specifier: ^0.32.1
|
||||
version: 0.32.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@types/react@18.3.3)(pg@8.12.0)(react@18.3.1)
|
||||
hono:
|
||||
specifier: ^4.5.3
|
||||
version: 4.5.3
|
||||
@ -426,6 +450,9 @@ importers:
|
||||
socket.io:
|
||||
specifier: ^4.7.5
|
||||
version: 4.7.5
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
systeminformation:
|
||||
specifier: ^5.23.3
|
||||
version: 5.23.3
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import { AuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { db } from '@/server/db';
|
||||
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { LoginContainer } from './components/LoginContainer';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
export default async function LoginPage() {
|
||||
const authQueries = new AuthQueries(db);
|
||||
const authQueries = container.get<IAuthQueries>('IAuthQueries');
|
||||
const isConfigured = await authQueries.getFirstOperator();
|
||||
|
||||
if (!isConfigured) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import { AuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { db } from '@/server/db';
|
||||
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { RegisterContainer } from './components/RegisterContainer';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
export default async function LoginPage() {
|
||||
const user = await getUserFromCookie();
|
||||
@ -11,7 +11,7 @@ export default async function LoginPage() {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
const authQueries = new AuthQueries(db);
|
||||
const authQueries = container.get<IAuthQueries>('IAuthQueries');
|
||||
const isConfigured = await authQueries.getFirstOperator();
|
||||
|
||||
if (isConfigured) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { getTranslator } from '@/lib/get-translator';
|
||||
import { ResetPasswordContainer } from './components/ResetPasswordContainer';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
|
||||
export default async function ResetPasswordPage() {
|
||||
const authService = new AuthServiceClass();
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
const isRequested = await authService.checkPasswordChangeRequest();
|
||||
|
||||
if (isRequested) {
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
IconX,
|
||||
IconRotateClockwise,
|
||||
} from '@tabler/icons-react';
|
||||
import React, { Fragment } from 'react';
|
||||
import type React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {
|
||||
@ -24,7 +25,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/DropdownMenu';
|
||||
import { Button, type ButtonProps } from '@/components/ui/Button';
|
||||
import { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import { InstallModal } from '../InstallModal';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface IProps {
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { type FormField, type AppInfo } from '@runtipi/shared';
|
||||
import type { FormField, AppInfo } from '@runtipi/shared';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { validateAppConfig } from '../../utils/validators';
|
||||
import { InstallFormField } from './InstallFormField';
|
||||
import { FormValues } from './InstallForm.types';
|
||||
import type { FormValues } from './InstallForm.types';
|
||||
import { useClientSettings } from '@/hooks/useClientSettings';
|
||||
|
||||
interface IProps {
|
||||
@ -50,9 +51,9 @@ export const InstallForm: React.FC<IProps> = ({ formFields, info, onSubmit, init
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues && !isDirty) {
|
||||
Object.entries(initialValues).forEach(([key, value]) => {
|
||||
for (const [key, value] of Object.entries(initialValues)) {
|
||||
setValue(key, value as string);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [initialValues, isDirty, setValue]);
|
||||
|
||||
@ -164,11 +165,11 @@ export const InstallForm: React.FC<IProps> = ({ formFields, info, onSubmit, init
|
||||
const validate = (values: FormValues) => {
|
||||
const validationErrors = validateAppConfig(values, formFields);
|
||||
|
||||
Object.entries(validationErrors).forEach(([key, value]) => {
|
||||
for (const [key, value] of Object.entries(validationErrors)) {
|
||||
if (value) {
|
||||
setError(key, { message: value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(validationErrors).length === 0) {
|
||||
onSubmit(values);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import type React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { ScrollArea } from '@/components/ui/ScrollArea';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { InstallForm, type FormValues } from '../InstallForm';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { CustomLinksServiceClass } from '@/server/services/custom-links/custom-links.service';
|
||||
import { db } from '@/server/db';
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import { getTranslator } from '@/lib/get-translator';
|
||||
import { AppTile } from '@/components/AppTile';
|
||||
import Link from 'next/link';
|
||||
import { Link as CustomLink } from '@/server/db/schema';
|
||||
import { Link as CustomLink } from '@runtipi/db';
|
||||
import clsx from 'clsx';
|
||||
import { LinkTile } from '@/components/LinkTile/LinkTile';
|
||||
import { EmptyPage } from '../../components/EmptyPage';
|
||||
import styles from './page.module.css';
|
||||
import { AddLinkButton } from '../components/AddLink/AddLinkButton';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { ICustomLinksService } from '@/server/services/custom-links/custom-links.service';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const translator = await getTranslator();
|
||||
@ -26,7 +26,7 @@ export default async function Page() {
|
||||
const installedApps = await appCatalog.executeCommand('getInstalledApps');
|
||||
|
||||
const user = await getUserFromCookie();
|
||||
const linksService = new CustomLinksServiceClass(db);
|
||||
const linksService = container.get<ICustomLinksService>('ICustomLinksService');
|
||||
const customLinks = await linksService.getLinks(user?.id);
|
||||
|
||||
const renderApp = (app: (typeof installedApps)[number]) => {
|
||||
|
@ -1,14 +1,16 @@
|
||||
'use server';
|
||||
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { publicActionClient } from '@/lib/safe-action';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
/**
|
||||
* Given that a password change request has been made, cancels the password change request.
|
||||
*/
|
||||
export const cancelResetPasswordAction = publicActionClient.action(async () => {
|
||||
await AuthServiceClass.cancelPasswordChangeRequest();
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
await authService.cancelPasswordChangeRequest();
|
||||
|
||||
revalidatePath('/reset-password');
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import { CustomLinksServiceClass } from '@/server/services/custom-links/custom-links.service';
|
||||
import { linkSchema } from '@runtipi/shared';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { ICustomLinksService } from '@/server/services/custom-links/custom-links.service';
|
||||
|
||||
export const addLinkAction = authActionClient.schema(linkSchema).action(async ({ parsedInput: link, ctx }) => {
|
||||
const linksService = new CustomLinksServiceClass();
|
||||
const linksService = container.get<ICustomLinksService>('ICustomLinksService');
|
||||
|
||||
const linkResponse = await linksService.add(link, ctx.user.id);
|
||||
return { success: true, link: linkResponse };
|
||||
|
@ -1,11 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import type { ICustomLinksService } from '@/server/services/custom-links/custom-links.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { z } from 'zod';
|
||||
import { CustomLinksServiceClass } from '@/server/services/custom-links/custom-links.service';
|
||||
|
||||
export const deleteLinkAction = authActionClient.schema(z.number()).action(async ({ parsedInput: linkId, ctx: { user } }) => {
|
||||
const linksService = new CustomLinksServiceClass();
|
||||
const linksService = container.get<ICustomLinksService>('ICustomLinksService');
|
||||
|
||||
await linksService.delete(linkId, user.id);
|
||||
return { success: true };
|
||||
|
@ -1,11 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import { linkSchema } from '@runtipi/shared';
|
||||
import { CustomLinksServiceClass } from '@/server/services/custom-links/custom-links.service';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { ICustomLinksService } from '@/server/services/custom-links/custom-links.service';
|
||||
|
||||
export const editLinkAction = authActionClient.schema(linkSchema).action(async ({ parsedInput: link, ctx: { user } }) => {
|
||||
const linksService = new CustomLinksServiceClass();
|
||||
const linksService = container.get<ICustomLinksService>('ICustomLinksService');
|
||||
|
||||
const linkResponse = await linksService.edit(link, user.id);
|
||||
return { success: true, link: linkResponse };
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/server/db';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { publicActionClient } from '@/lib/safe-action';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({
|
||||
username: z.string(),
|
||||
@ -16,7 +16,7 @@ const input = z.object({
|
||||
* if that user has 2FA enabled.
|
||||
*/
|
||||
export const loginAction = publicActionClient.schema(input).action(async ({ parsedInput: { username, password } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
|
||||
const { totpSessionId } = await authService.login({ username, password });
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/server/db';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Logs out the current user making the request.
|
||||
@ -19,7 +19,7 @@ export const logoutAction = authActionClient.action(async () => {
|
||||
};
|
||||
}
|
||||
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
await authService.logout(sessionCookie.value);
|
||||
|
||||
revalidatePath('/');
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/server/db';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { publicActionClient } from '@/lib/safe-action';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
|
||||
const input = z.object({
|
||||
username: z.string(),
|
||||
@ -15,7 +15,7 @@ const input = z.object({
|
||||
* Given a username and password, registers the user and logs them in.
|
||||
*/
|
||||
export const registerAction = publicActionClient.schema(input).action(async ({ parsedInput: { username, password } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
|
||||
const result = await authService.register({ username, password });
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/server/db';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { publicActionClient } from '@/lib/safe-action';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({
|
||||
newPassword: z.string(),
|
||||
@ -13,7 +13,7 @@ const input = z.object({
|
||||
* Given that a password change request has been made, changes the password of the first operator.
|
||||
*/
|
||||
export const resetPasswordAction = publicActionClient.schema(input).action(async ({ parsedInput: { newPassword } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
const { email } = await authService.changeOperatorPassword({ newPassword });
|
||||
|
||||
return { success: true, email };
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { db } from '@/server/db';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({ currentPassword: z.string(), newPassword: z.string() });
|
||||
|
||||
@ -13,7 +13,7 @@ const input = z.object({ currentPassword: z.string(), newPassword: z.string() })
|
||||
export const changePasswordAction = authActionClient
|
||||
.schema(input)
|
||||
.action(async ({ parsedInput: { currentPassword, newPassword }, ctx: { user } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
|
||||
await authService.changePassword({ userId: user.id, currentPassword, newPassword });
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { db } from '@/server/db';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
|
||||
const input = z.object({ newUsername: z.string().email(), password: z.string() });
|
||||
|
||||
@ -11,7 +11,7 @@ const input = z.object({ newUsername: z.string().email(), password: z.string() }
|
||||
* Given the current password and a new username, change the username of the current user.
|
||||
*/
|
||||
export const changeUsernameAction = authActionClient.schema(input).action(async ({ parsedInput: { newUsername, password }, ctx: { user } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
|
||||
await authService.changeUsername({ userId: user.id, newUsername, password });
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { db } from '@/server/db';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({ password: z.string() });
|
||||
|
||||
@ -12,7 +12,7 @@ const input = z.object({ password: z.string() });
|
||||
* Given a valid user password, disable TOTP for the user
|
||||
*/
|
||||
export const disableTotpAction = authActionClient.schema(input).action(async ({ parsedInput: { password }, ctx: { user } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
await authService.disableTotp({ userId: user.id, password });
|
||||
|
||||
revalidatePath('/settings');
|
||||
|
@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { db } from '@/server/db';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({ password: z.string() });
|
||||
|
||||
@ -11,7 +11,7 @@ const input = z.object({ password: z.string() });
|
||||
* Given user's password, return the TOTP URI and key
|
||||
*/
|
||||
export const getTotpUriAction = authActionClient.schema(input).action(async ({ parsedInput: { password }, ctx: { user } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
const { key, uri } = await authService.getTotpUri({ userId: user.id, password });
|
||||
|
||||
return { success: true, key, uri };
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { db } from '@/server/db';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { authActionClient } from '@/lib/safe-action';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const input = z.object({ totpCode: z.string() });
|
||||
|
||||
@ -12,7 +12,7 @@ const input = z.object({ totpCode: z.string() });
|
||||
* Given a valid user's TOTP code, activate TOTP for the user
|
||||
*/
|
||||
export const setupTotpAction = authActionClient.schema(input).action(async ({ parsedInput: { totpCode }, ctx: { user } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
await authService.setupTotp({ userId: user.id, totpCode });
|
||||
|
||||
revalidatePath('/settings');
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/server/db';
|
||||
import { AuthServiceClass } from '@/server/services/auth/auth.service';
|
||||
import { publicActionClient } from '@/lib/safe-action';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { IAuthService } from '@/server/services/auth/auth.service';
|
||||
|
||||
const input = z.object({
|
||||
totpCode: z.string(),
|
||||
@ -12,7 +12,7 @@ const input = z.object({
|
||||
});
|
||||
|
||||
export const verifyTotpAction = publicActionClient.schema(input).action(async ({ parsedInput: { totpSessionId, totpCode } }) => {
|
||||
const authService = new AuthServiceClass(db);
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
await authService.verifyTotp({ totpSessionId, totpCode });
|
||||
|
||||
revalidatePath('/login');
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { type ReactNode, createContext, useRef } from 'react';
|
||||
|
||||
import { createAppStatusStore } from './app-status-store';
|
||||
import type { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
|
||||
export type AppStatusStoreApi = ReturnType<typeof createAppStatusStore>;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
|
||||
export type AppStatusState = {
|
||||
|
@ -1,15 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl';
|
||||
import 'reflect-metadata';
|
||||
import type React from 'react';
|
||||
import { type AbstractIntlMessages, NextIntlClientProvider } from 'next-intl';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from './ThemeProvider';
|
||||
import { SocketProvider } from './SocketProvider/SocketProvider';
|
||||
import type { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { AppStatusStoreProvider } from './AppStatusProvider/app-status-provider';
|
||||
import { ClientSettingsStoreProvider } from './ClientSettingsProvider/ClientSettingsProvider';
|
||||
import { settingsSchema } from '@runtipi/shared';
|
||||
import { z } from 'zod';
|
||||
import type { settingsSchema } from '@runtipi/shared';
|
||||
import type { z } from 'zod';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import 'reflect-metadata';
|
||||
import type React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import { getLocale, getMessages } from 'next-intl/server';
|
||||
|
||||
@ -13,7 +14,7 @@ import { Toaster } from 'react-hot-toast';
|
||||
import { ClientProviders } from './components/ClientProviders';
|
||||
import { CookiesProvider } from 'next-client-cookies/server';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tipi',
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { db } from '@/server/db';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { AuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { UnauthenticatedPage } from '@/components/UnauthenticatedPage';
|
||||
import { headers } from 'next/headers';
|
||||
import { GuestDashboardApps } from './components/GuestDashboardApps';
|
||||
import { EmptyPage } from './components/EmptyPage';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
export default async function RootPage() {
|
||||
const { guestDashboard } = TipiConfig.getConfig();
|
||||
@ -33,7 +33,7 @@ export default async function RootPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const authQueries = new AuthQueries(db);
|
||||
const authQueries = container.get<IAuthQueries>('IAuthQueries');
|
||||
|
||||
const isConfigured = await authQueries.getFirstOperator();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import type React from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import styles from './AppStatus.module.scss';
|
||||
|
@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import type React from 'react';
|
||||
import { IconTrash, IconEdit } from '@tabler/icons-react';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { ContextMenu, ContextMenuItem, ContextMenuContent, ContextMenuTrigger } from '@/client/components/ui/ContextMenu/ContextMenu';
|
||||
import { AddLinkModal } from 'src/app/(dashboard)/components/AddLink/AddLinkModal';
|
||||
import { DeleteLinkModal } from 'src/app/(dashboard)/components/AddLink/DeleteLinkModal';
|
||||
import { LinkInfo } from '@runtipi/shared';
|
||||
import { Link } from '@/server/db/schema';
|
||||
import type { LinkInfo } from '@runtipi/shared';
|
||||
import type { Link } from '@runtipi/db';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppLogo } from '../AppLogo';
|
||||
|
||||
|
@ -4,15 +4,49 @@ import { type ILogger, Logger } from '@runtipi/shared/node';
|
||||
import { Container } from 'inversify';
|
||||
import path from 'node:path';
|
||||
import { DATA_DIR } from './config';
|
||||
import { DbClient, type IDbClient } from '@runtipi/db';
|
||||
import { TipiConfig } from './server/core/TipiConfig';
|
||||
import { AppQueries, type IAppQueries } from './server/queries/apps/apps.queries';
|
||||
import { AuthQueries, type IAuthQueries } from './server/queries/auth/auth.queries';
|
||||
import { LinkQueries, type ILinkQueries } from './server/queries/links/links.queries';
|
||||
import { CustomLinksService, type ICustomLinksService } from './server/services/custom-links/custom-links.service';
|
||||
import { AuthService, type IAuthService } from './server/services/auth/auth.service';
|
||||
|
||||
export function createContainer() {
|
||||
const container = new Container();
|
||||
|
||||
const { postgresHost, postgresPort, postgresDatabase, postgresPassword, postgresUsername } = TipiConfig.getConfig();
|
||||
|
||||
container.bind<ILogger>('ILogger').toDynamicValue(() => {
|
||||
return new Logger('dashboard', path.join(DATA_DIR, 'logs'));
|
||||
});
|
||||
container.bind<ITipiCache>('ITipiCache').to(TipiCache).inSingletonScope();
|
||||
|
||||
container
|
||||
.bind<IDbClient>('IDbClient')
|
||||
.toDynamicValue((context) => {
|
||||
return new DbClient(
|
||||
{
|
||||
host: postgresHost,
|
||||
port: Number(postgresPort),
|
||||
database: postgresDatabase,
|
||||
password: postgresPassword,
|
||||
username: postgresUsername,
|
||||
},
|
||||
context.container.get<ILogger>('ILogger'),
|
||||
);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Repositories
|
||||
container.bind<IAppQueries>('IAppQueries').to(AppQueries);
|
||||
container.bind<IAuthQueries>('IAuthQueries').to(AuthQueries);
|
||||
container.bind<ILinkQueries>('ILinkQueries').to(LinkQueries);
|
||||
|
||||
// Services
|
||||
container.bind<ICustomLinksService>('ICustomLinksService').to(CustomLinksService);
|
||||
container.bind<IAuthService>('IAuthService').to(AuthService);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { cookies } from 'next/headers';
|
||||
import { tipiCache } from '../core/TipiCache';
|
||||
import { db } from '../db';
|
||||
import { AuthQueries } from '../queries/auth/auth.queries';
|
||||
import type { IAuthQueries } from '../queries/auth/auth.queries';
|
||||
import { container } from 'src/inversify.config';
|
||||
import type { ITipiCache } from '../core/TipiCache/TipiCache';
|
||||
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24; // 1 day
|
||||
const COOKIE_NAME = 'tipi.sid';
|
||||
@ -12,6 +12,7 @@ export const generateSessionId = (prefix: string) => {
|
||||
};
|
||||
|
||||
export const setSession = async (sessionId: string, userId: string) => {
|
||||
const tipiCache = container.get<ITipiCache>('ITipiCache');
|
||||
const cookieStore = cookies();
|
||||
cookieStore.set(COOKIE_NAME, sessionId, { maxAge: COOKIE_MAX_AGE, httpOnly: true, secure: false, sameSite: false });
|
||||
|
||||
@ -22,7 +23,8 @@ export const setSession = async (sessionId: string, userId: string) => {
|
||||
};
|
||||
|
||||
export const getUserFromCookie = async () => {
|
||||
const authQueries = new AuthQueries(db);
|
||||
const tipiCache = container.get<ITipiCache>('ITipiCache');
|
||||
const authQueries = container.get<IAuthQueries>('IAuthQueries');
|
||||
|
||||
const cookieStore = cookies();
|
||||
const sessionId = cookieStore.get('tipi.sid');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Queue, QueueEvents } from 'bullmq';
|
||||
import { eventResultSchema, eventSchema, SystemEvent } from '@runtipi/shared';
|
||||
import { eventResultSchema, eventSchema, type SystemEvent } from '@runtipi/shared';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { Logger } from '../Logger';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import { createClient, type RedisClientType } from 'redis';
|
||||
import { TipiConfig } from '../TipiConfig';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { ILogger } from '@runtipi/shared/node';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
|
||||
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
|
||||
@ -12,10 +12,11 @@ export interface ITipiCache {
|
||||
getByPrefix(prefix: string): Promise<Array<{ key: string; val: string | null }>>;
|
||||
close(): Promise<string>;
|
||||
ttl(key: string): Promise<number>;
|
||||
clear(): Promise<number[]>;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TipiCache {
|
||||
export class TipiCache implements ITipiCache {
|
||||
private client: RedisClientType;
|
||||
|
||||
constructor(@inject('ILogger') private logger: ILogger) {
|
||||
@ -80,4 +81,15 @@ export class TipiCache {
|
||||
const client = await this.getClient();
|
||||
return client.ttl(key);
|
||||
}
|
||||
|
||||
public async clear() {
|
||||
const client = await this.getClient();
|
||||
try {
|
||||
const keys = await client.keys('*');
|
||||
return Promise.all(keys.map((key) => client.del(key)));
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to clear cache', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
import { container } from 'src/inversify.config';
|
||||
import { ITipiCache } from './TipiCache';
|
||||
|
||||
export const tipiCache = container.get<ITipiCache>('ITipiCache');
|
@ -1,15 +0,0 @@
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import { TipiConfig } from '../core/TipiConfig/TipiConfig';
|
||||
import * as schema from './schema';
|
||||
|
||||
const connectionString = `postgresql://${TipiConfig.getConfig().postgresUsername}:${TipiConfig.getConfig().postgresPassword}@${
|
||||
TipiConfig.getConfig().postgresHost
|
||||
}:${TipiConfig.getConfig().postgresPort}/${TipiConfig.getConfig().postgresDatabase}?connect_timeout=300`;
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema });
|
||||
export type Database = typeof db;
|
@ -1,13 +1,23 @@
|
||||
import { and, asc, eq, ne, notInArray } from 'drizzle-orm';
|
||||
import { Database, db } from '@/server/db';
|
||||
import { appTable, NewApp, AppStatus } from '../../db/schema';
|
||||
import { appTable, App, NewApp, AppStatus, IDbClient } from '@runtipi/db';
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
export class AppQueries {
|
||||
private db;
|
||||
export interface IAppQueries {
|
||||
getApp(appId: string): Promise<App | undefined>;
|
||||
updateApp(appId: string, data: Partial<NewApp>): Promise<App | undefined>;
|
||||
deleteApp(appId: string): Promise<void>;
|
||||
createApp(data: NewApp): Promise<App | undefined>;
|
||||
getAppsByStatus(status: AppStatus): Promise<App[]>;
|
||||
getApps(): Promise<App[]>;
|
||||
getGuestDashboardApps(): Promise<App[]>;
|
||||
getAppsByDomain(domain: string, id: string): Promise<App[]>;
|
||||
updateAppsByStatusNotIn(statuses: AppStatus[], data: Partial<NewApp>): Promise<App[]>;
|
||||
}
|
||||
|
||||
constructor(p: Database = db) {
|
||||
this.db = p;
|
||||
}
|
||||
@injectable()
|
||||
export class AppQueries implements IAppQueries {
|
||||
|
||||
constructor(@inject('IDbClient') private dbClient: IDbClient) { }
|
||||
|
||||
/**
|
||||
* Given an app id, return the app
|
||||
@ -15,7 +25,7 @@ export class AppQueries {
|
||||
* @param {string} appId - The id of the app to return
|
||||
*/
|
||||
public async getApp(appId: string) {
|
||||
return this.db.query.appTable.findFirst({ where: eq(appTable.id, appId) });
|
||||
return this.dbClient.db.query.appTable.findFirst({ where: eq(appTable.id, appId) });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -25,7 +35,7 @@ export class AppQueries {
|
||||
* @param {Partial<NewApp>} data - The data to update the app with
|
||||
*/
|
||||
public async updateApp(appId: string, data: Partial<NewApp>) {
|
||||
const updatedApps = await this.db.update(appTable).set(data).where(eq(appTable.id, appId)).returning().execute();
|
||||
const updatedApps = await this.dbClient.db.update(appTable).set(data).where(eq(appTable.id, appId)).returning().execute();
|
||||
return updatedApps[0];
|
||||
}
|
||||
|
||||
@ -35,7 +45,7 @@ export class AppQueries {
|
||||
* @param {string} appId - The id of the app to delete
|
||||
*/
|
||||
public async deleteApp(appId: string) {
|
||||
await this.db.delete(appTable).where(eq(appTable.id, appId)).execute();
|
||||
await this.dbClient.db.delete(appTable).where(eq(appTable.id, appId)).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,7 +54,7 @@ export class AppQueries {
|
||||
* @param {NewApp} data - The data to create the app with
|
||||
*/
|
||||
public async createApp(data: NewApp) {
|
||||
const newApps = await this.db.insert(appTable).values(data).returning().execute();
|
||||
const newApps = await this.dbClient.db.insert(appTable).values(data).returning().execute();
|
||||
return newApps[0];
|
||||
}
|
||||
|
||||
@ -54,21 +64,21 @@ export class AppQueries {
|
||||
* @param {AppStatus} status - The status of the apps to return
|
||||
*/
|
||||
public async getAppsByStatus(status: AppStatus) {
|
||||
return this.db.query.appTable.findMany({ where: eq(appTable.status, status), orderBy: asc(appTable.id) });
|
||||
return this.dbClient.db.query.appTable.findMany({ where: eq(appTable.status, status), orderBy: asc(appTable.id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all apps installed sorted by id ascending
|
||||
*/
|
||||
public async getApps() {
|
||||
return this.db.query.appTable.findMany({ orderBy: asc(appTable.id) });
|
||||
return this.dbClient.db.query.appTable.findMany({ orderBy: asc(appTable.id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all apps that are running and visible on guest dashboard sorted by id ascending
|
||||
*/
|
||||
public async getGuestDashboardApps() {
|
||||
return this.db.query.appTable.findMany({
|
||||
return this.dbClient.db.query.appTable.findMany({
|
||||
where: and(eq(appTable.status, 'running'), eq(appTable.isVisibleOnGuestDashboard, true)),
|
||||
orderBy: asc(appTable.id),
|
||||
});
|
||||
@ -81,7 +91,7 @@ export class AppQueries {
|
||||
* @param {string} id - The id of the app to exclude
|
||||
*/
|
||||
public async getAppsByDomain(domain: string, id: string) {
|
||||
return this.db.query.appTable.findMany({ where: and(eq(appTable.domain, domain), eq(appTable.exposed, true), ne(appTable.id, id)) });
|
||||
return this.dbClient.db.query.appTable.findMany({ where: and(eq(appTable.domain, domain), eq(appTable.exposed, true), ne(appTable.id, id)) });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,6 +101,6 @@ export class AppQueries {
|
||||
* @param {Partial<NewApp>} data - The data to update the apps with
|
||||
*/
|
||||
public async updateAppsByStatusNotIn(statuses: AppStatus[], data: Partial<NewApp>) {
|
||||
return this.db.update(appTable).set(data).where(notInArray(appTable.status, statuses)).returning().execute();
|
||||
return this.dbClient.db.update(appTable).set(data).where(notInArray(appTable.status, statuses)).returning().execute();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,20 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Database } from '@/server/db';
|
||||
import { userTable, NewUser } from '../../db/schema';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { type IDbClient, type NewUser, type User, userTable } from '@runtipi/db';
|
||||
|
||||
export class AuthQueries {
|
||||
private db;
|
||||
export interface IAuthQueries {
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
getUserById(id: number): Promise<User | undefined>;
|
||||
getUserDtoById(id: number): Promise<Pick<User, 'id' | 'username' | 'totpEnabled' | 'locale' | 'operator'> | undefined>;
|
||||
updateUser(id: number, data: Partial<NewUser>): Promise<User | undefined>;
|
||||
getOperators(): Promise<User[]>;
|
||||
getFirstOperator(): Promise<User | undefined>;
|
||||
createUser(data: NewUser): Promise<User | undefined>;
|
||||
}
|
||||
|
||||
constructor(p: Database) {
|
||||
this.db = p;
|
||||
}
|
||||
@injectable()
|
||||
export class AuthQueries implements IAuthQueries {
|
||||
constructor(@inject('IDbClient') private dbClient: IDbClient) {}
|
||||
|
||||
/**
|
||||
* Given a username, return the user associated to it
|
||||
@ -15,7 +22,7 @@ export class AuthQueries {
|
||||
* @param {string} username - The username of the user to return
|
||||
*/
|
||||
public async getUserByUsername(username: string) {
|
||||
return this.db.query.userTable.findFirst({ where: eq(userTable.username, username.trim().toLowerCase()) });
|
||||
return this.dbClient.db.query.userTable.findFirst({ where: eq(userTable.username, username.trim().toLowerCase()) });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -24,7 +31,7 @@ export class AuthQueries {
|
||||
* @param {number} id - The id of the user to return
|
||||
*/
|
||||
public async getUserById(id: number) {
|
||||
return this.db.query.userTable.findFirst({ where: eq(userTable.id, Number(id)) });
|
||||
return this.dbClient.db.query.userTable.findFirst({ where: eq(userTable.id, Number(id)) });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,7 +40,10 @@ export class AuthQueries {
|
||||
* @param {number} id - The id of the user to return
|
||||
*/
|
||||
public async getUserDtoById(id: number) {
|
||||
return this.db.query.userTable.findFirst({ where: eq(userTable.id, Number(id)), columns: { id: true, username: true, totpEnabled: true, locale: true, operator: true } });
|
||||
return this.dbClient.db.query.userTable.findFirst({
|
||||
where: eq(userTable.id, Number(id)),
|
||||
columns: { id: true, username: true, totpEnabled: true, locale: true, operator: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +53,7 @@ export class AuthQueries {
|
||||
* @param {Partial<NewUser>} data - The data to update the user with
|
||||
*/
|
||||
public async updateUser(id: number, data: Partial<NewUser>) {
|
||||
const updatedUsers = await this.db
|
||||
const updatedUsers = await this.dbClient.db
|
||||
.update(userTable)
|
||||
.set(data)
|
||||
.where(eq(userTable.id, Number(id)))
|
||||
@ -56,14 +66,14 @@ export class AuthQueries {
|
||||
* Returns all operators registered in the system
|
||||
*/
|
||||
public async getOperators() {
|
||||
return this.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
return this.dbClient.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first operator found in the system
|
||||
*/
|
||||
public async getFirstOperator() {
|
||||
return this.db.query.userTable.findFirst({ where: eq(userTable.operator, true) });
|
||||
return this.dbClient.db.query.userTable.findFirst({ where: eq(userTable.operator, true) });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,7 +82,7 @@ export class AuthQueries {
|
||||
* @param {NewUser} data - The data to create the user with
|
||||
*/
|
||||
public async createUser(data: NewUser) {
|
||||
const newUsers = await this.db.insert(userTable).values(data).returning();
|
||||
const newUsers = await this.dbClient.db.insert(userTable).values(data).returning();
|
||||
return newUsers[0];
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { Database } from '@/server/db';
|
||||
import { linkTable } from '@/server/db/schema';
|
||||
import { LinkInfoInput } from '@runtipi/shared';
|
||||
import { type IDbClient, type Link, linkTable } from '@runtipi/db';
|
||||
import type { LinkInfoInput } from '@runtipi/shared';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
export class LinkQueries {
|
||||
private db;
|
||||
export interface ILinkQueries {
|
||||
addLink(link: LinkInfoInput, userId: number): Promise<Link | undefined>;
|
||||
editLink(link: LinkInfoInput, userId: number): Promise<Link | undefined>;
|
||||
deleteLink(linkId: number, userId: number): Promise<void>;
|
||||
getLinks(userId: number): Promise<Link[]>;
|
||||
}
|
||||
|
||||
constructor(p: Database) {
|
||||
this.db = p;
|
||||
}
|
||||
@injectable()
|
||||
export class LinkQueries implements ILinkQueries {
|
||||
constructor(@inject('IDbClient') private dbClient: IDbClient) {}
|
||||
|
||||
/**
|
||||
* Adds a new link to the database.
|
||||
@ -17,7 +21,7 @@ export class LinkQueries {
|
||||
*/
|
||||
public async addLink(link: LinkInfoInput, userId: number) {
|
||||
const { title, description, url, iconUrl } = link;
|
||||
const newLinks = await this.db.insert(linkTable).values({ title, description, url, iconUrl, userId }).returning().execute();
|
||||
const newLinks = await this.dbClient.db.insert(linkTable).values({ title, description, url, iconUrl, userId }).returning();
|
||||
return newLinks[0];
|
||||
}
|
||||
|
||||
@ -32,12 +36,11 @@ export class LinkQueries {
|
||||
|
||||
if (!id) throw new Error('No id provided');
|
||||
|
||||
const updatedLinks = await this.db
|
||||
const updatedLinks = await this.dbClient.db
|
||||
.update(linkTable)
|
||||
.set({ title, description, url, iconUrl, updatedAt: new Date() })
|
||||
.where(and(eq(linkTable.id, id), eq(linkTable.userId, userId)))
|
||||
.returning()
|
||||
.execute();
|
||||
.returning();
|
||||
|
||||
return updatedLinks[0];
|
||||
}
|
||||
@ -47,11 +50,7 @@ export class LinkQueries {
|
||||
* @param {number} linkId - The id of the link to be deleted.
|
||||
*/
|
||||
public async deleteLink(linkId: number, userId: number) {
|
||||
await this.db
|
||||
.delete(linkTable)
|
||||
.where(and(eq(linkTable.id, linkId), eq(linkTable.userId, userId)))
|
||||
.returning()
|
||||
.execute();
|
||||
await this.dbClient.db.delete(linkTable).where(and(eq(linkTable.id, linkId), eq(linkTable.userId, userId)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +59,6 @@ export class LinkQueries {
|
||||
* @returns An array of links belonging to the user.
|
||||
*/
|
||||
public async getLinks(userId: number) {
|
||||
const links = await this.db.select().from(linkTable).where(eq(linkTable.userId, userId)).orderBy(linkTable.id);
|
||||
return links;
|
||||
return this.dbClient.db.select().from(linkTable).where(eq(linkTable.userId, userId)).orderBy(linkTable.id);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import pg from 'pg';
|
||||
import { migrate } from '@runtipi/postgres-migrations';
|
||||
import { createClient } from 'redis';
|
||||
@ -36,11 +36,11 @@ export const runPostgresMigrations = async (dbName?: string) => {
|
||||
|
||||
Logger.info('Running migrations');
|
||||
try {
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/worker/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/db/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
} catch (e) {
|
||||
Logger.error('Error running migrations. Dropping table migrations and trying again');
|
||||
await client.query('DROP TABLE migrations');
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/worker/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/db/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
}
|
||||
|
||||
Logger.info('Migration complete');
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
|
||||
import { IAppBackupCommand } from './commands/types';
|
||||
import type { IAppBackupCommand } from './commands/types';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { CreateAppBackupCommand, DeleteAppBackupCommand, GetAppBackupsCommand, RestoreAppBackupCommand } from './commands';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
export const availableCommands = {
|
||||
createAppBackup: CreateAppBackupCommand,
|
||||
@ -28,7 +29,7 @@ export class AppBackupClass {
|
||||
private commandInvoker: CommandInvoker;
|
||||
|
||||
constructor(
|
||||
private queries: AppQueries,
|
||||
private queries: IAppQueries,
|
||||
private eventDispatcher: EventDispatcher,
|
||||
private appDataService: AppDataService,
|
||||
) {
|
||||
@ -57,7 +58,7 @@ export class AppBackupClass {
|
||||
|
||||
export type AppBackup = InstanceType<typeof AppBackupClass>;
|
||||
|
||||
const queries = new AppQueries();
|
||||
const queries = container.get<IAppQueries>('IAppQueries');
|
||||
const eventDispatcher = new EventDispatcher();
|
||||
const appDataService = new AppDataService({ dataDir: DATA_DIR, appDataDir: APP_DATA_DIR, appsRepoId: TipiConfig.getConfig().appsRepoId });
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { AppBackupCommandParams, IAppBackupCommand } from './types';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { AppBackupCommandParams, IAppBackupCommand } from './types';
|
||||
import type { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import { Logger } from '@/server/core/Logger';
|
||||
import { TranslatedError } from '@/server/utils/errors';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { appLifecycle } from '../../app-lifecycle/app-lifecycle.service';
|
||||
|
||||
export class CreateAppBackupCommand implements IAppBackupCommand {
|
||||
private queries: AppQueries;
|
||||
private queries: IAppQueries;
|
||||
private eventDispatcher: EventDispatcher;
|
||||
|
||||
constructor(params: AppBackupCommandParams) {
|
||||
@ -15,7 +15,7 @@ export class CreateAppBackupCommand implements IAppBackupCommand {
|
||||
this.eventDispatcher = params.eventDispatcher;
|
||||
}
|
||||
|
||||
private async sendEvent(appId: string, appStatusBeforeUpdate: AppStatus): Promise<void> {
|
||||
private async sendEvent(appId: string, appStatusBeforeUpdate?: AppStatus): Promise<void> {
|
||||
const { success, stdout } = await this.eventDispatcher.dispatchEventAsync(
|
||||
{ type: 'app', command: 'backup', appid: appId, form: {} },
|
||||
1000 * 60 * 15, // 15 minutes
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { AppBackupCommandParams, IAppBackupCommand } from './types';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import type { AppBackupCommandParams, IAppBackupCommand } from './types';
|
||||
import type { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import { Logger } from '@/server/core/Logger';
|
||||
import { TranslatedError } from '@/server/utils/errors';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { appLifecycle } from '../../app-lifecycle/app-lifecycle.service';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
|
||||
export class RestoreAppBackupCommand implements IAppBackupCommand {
|
||||
private queries: AppQueries;
|
||||
private queries: IAppQueries;
|
||||
private eventDispatcher: EventDispatcher;
|
||||
|
||||
constructor(params: AppBackupCommandParams) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import type { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { AppDataService } from '@runtipi/shared/node';
|
||||
|
||||
export interface IAppBackupCommand<T = unknown> {
|
||||
execute(...args: unknown[]): Promise<T>;
|
||||
}
|
||||
|
||||
export type AppBackupCommandParams = {
|
||||
queries: AppQueries;
|
||||
queries: IAppQueries;
|
||||
eventDispatcher: EventDispatcher;
|
||||
appDataService: AppDataService;
|
||||
executeOtherCommand: IAppBackupCommand['execute'];
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'fs-extra';
|
||||
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { type TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { beforeEach, beforeAll, afterAll, describe, it, expect } from 'vitest';
|
||||
import { AppCatalogClass } from './app-catalog.service';
|
||||
@ -21,7 +21,7 @@ beforeAll(async () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const appDataService = new AppDataService({ dataDir: DATA_DIR, appDataDir: APP_DATA_DIR, appsRepoId: TipiConfig.getConfig().appsRepoId });
|
||||
appCatalog = new AppCatalogClass(new AppQueries(db.db), new AppCatalogCache(appDataService), appDataService);
|
||||
appCatalog = new AppCatalogClass(new AppQueries(db.dbClient), new AppCatalogCache(appDataService), appDataService);
|
||||
await clearDatabase(db);
|
||||
await TipiConfig.setConfig('version', 'test');
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { GetInstalledAppsCommand, GetGuestDashboardApps, GetAppCommand } from './commands';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
@ -6,7 +6,8 @@ import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { SearchAppsCommand } from './commands/search-apps-command';
|
||||
import { ListAppsCommand } from './commands/list-apps-command';
|
||||
import { AppCatalogCache } from './app-catalog-cache';
|
||||
import { IAppCatalogCommand } from './commands/types';
|
||||
import type { IAppCatalogCommand } from './commands/types';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
const availableCommands = {
|
||||
getInstalledApps: GetInstalledAppsCommand,
|
||||
@ -31,7 +32,7 @@ export class AppCatalogClass {
|
||||
private commandInvoker: CommandInvoker;
|
||||
|
||||
constructor(
|
||||
private queries: AppQueries,
|
||||
private queries: IAppQueries,
|
||||
private appCatalogCache: AppCatalogCache,
|
||||
private appDataService: AppDataService,
|
||||
) {
|
||||
@ -59,7 +60,7 @@ export class AppCatalogClass {
|
||||
|
||||
export type AppCatalog = InstanceType<typeof AppCatalogClass>;
|
||||
|
||||
const queries = new AppQueries();
|
||||
const queries = container.get<IAppQueries>('IAppQueries');
|
||||
const appDataService = new AppDataService({ dataDir: DATA_DIR, appDataDir: APP_DATA_DIR, appsRepoId: TipiConfig.getConfig().appsRepoId });
|
||||
const appCacheManager = new AppCatalogCache(appDataService);
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { type App } from '@/server/db/schema';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { App } from '@runtipi/db';
|
||||
import { TranslatedError } from '@/server/utils/errors';
|
||||
import { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import type { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import type { AppDataService } from '@runtipi/shared/node';
|
||||
|
||||
type ReturnValue = Awaited<ReturnType<InstanceType<typeof GetAppCommand>['execute']>>;
|
||||
|
||||
export class GetAppCommand implements IAppCatalogCommand<ReturnValue> {
|
||||
private queries: AppQueries;
|
||||
private queries: IAppQueries;
|
||||
private appDataService: AppDataService;
|
||||
|
||||
constructor(params: AppCatalogCommandParams) {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { notEmpty } from '@/server/common/typescript.helpers';
|
||||
import { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import type { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import type { AppDataService } from '@runtipi/shared/node';
|
||||
|
||||
type ReturnValue = Awaited<ReturnType<InstanceType<typeof GetGuestDashboardApps>['execute']>>;
|
||||
|
||||
export class GetGuestDashboardApps implements IAppCatalogCommand<ReturnValue> {
|
||||
private queries: AppQueries;
|
||||
private queries: IAppQueries;
|
||||
private appDataService: AppDataService;
|
||||
|
||||
constructor(params: AppCatalogCommandParams) {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { notEmpty } from '@/server/common/typescript.helpers';
|
||||
import { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import type { AppCatalogCommandParams, IAppCatalogCommand } from './types';
|
||||
import type { AppDataService } from '@runtipi/shared/node';
|
||||
|
||||
type ReturnValue = Awaited<ReturnType<InstanceType<typeof GetInstalledAppsCommand>['execute']>>;
|
||||
|
||||
export class GetInstalledAppsCommand implements IAppCatalogCommand<ReturnValue> {
|
||||
private queries: AppQueries;
|
||||
private queries: IAppQueries;
|
||||
private appDataService: AppDataService;
|
||||
|
||||
constructor(params: AppCatalogCommandParams) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import { AppCatalogCache } from '../app-catalog-cache';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { AppDataService } from '@runtipi/shared/node';
|
||||
import type { AppCatalogCache } from '../app-catalog-cache';
|
||||
|
||||
export interface IAppCatalogCommand<T = unknown> {
|
||||
execute(...args: unknown[]): Promise<T>;
|
||||
}
|
||||
|
||||
export type AppCatalogCommandParams = {
|
||||
queries: AppQueries;
|
||||
queries: IAppQueries;
|
||||
appDataService: AppDataService;
|
||||
appCatalogCache: AppCatalogCache;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { type TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { vi, beforeEach, beforeAll, afterAll, describe, it, expect } from 'vitest';
|
||||
import { AppLifecycleClass } from './app-lifecycle.service';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
@ -17,7 +17,7 @@ const appDataService = new AppDataService({ dataDir: DATA_DIR, appDataDir: APP_D
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await createDatabase(TEST_SUITE);
|
||||
appLifecycle = new AppLifecycleClass(new AppQueries(db.db), dispatcher, appDataService);
|
||||
appLifecycle = new AppLifecycleClass(new AppQueries(db.dbClient), dispatcher, appDataService);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import type { IAppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
|
||||
import { IAppLifecycleCommand } from './commands/types';
|
||||
import type { IAppLifecycleCommand } from './commands/types';
|
||||
import { AppDataService } from '@runtipi/shared/node';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
@ -14,6 +14,7 @@ import {
|
||||
UpdateAppCommand,
|
||||
UpdateAppConfigCommand,
|
||||
} from './commands';
|
||||
import { container } from 'src/inversify.config';
|
||||
|
||||
export const availableCommands = {
|
||||
startApp: StartAppCommand,
|
||||
@ -41,7 +42,7 @@ export class AppLifecycleClass {
|
||||
private commandInvoker: CommandInvoker;
|
||||
|
||||
constructor(
|
||||
private queries: AppQueries,
|
||||
private queries: IAppQueries,
|
||||
private eventDispatcher: EventDispatcher,
|
||||
private appDataService: AppDataService,
|
||||
) {
|
||||
@ -70,7 +71,7 @@ export class AppLifecycleClass {
|
||||
|
||||
export type AppLifecycle = InstanceType<typeof AppLifecycleClass>;
|
||||
|
||||
const queries = new AppQueries();
|
||||
const queries = container.get<IAppQueries>('IAppQueries');
|
||||
const eventDispatcher = new EventDispatcher();
|
||||
const appDataService = new AppDataService({ dataDir: DATA_DIR, appDataDir: APP_DATA_DIR, appsRepoId: TipiConfig.getConfig().appsRepoId });
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user