mirror of
https://github.com/meienberger/runtipi.git
synced 2024-09-11 12:15:48 +03:00
Sync 3.5.2 in develop (#1589)
* chore: bump version to 3.5.2 * fix(container): use env variables instead of parsing before ready * chore: run biome safe autofix * fix(db): use env for username and db instead of hardcoded value
This commit is contained in:
parent
ad1bbc5fbd
commit
84ea5dcb7f
@ -6,16 +6,9 @@
|
||||
"moby": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"waderyan.gitblame"
|
||||
],
|
||||
"extensions": ["ms-azuretools.vscode-docker", "ms-vscode.vscode-typescript-next", "waderyan.gitblame"],
|
||||
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
|
||||
"forwardPorts": [
|
||||
80,
|
||||
3000
|
||||
],
|
||||
"forwardPorts": [80, 3000],
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Runtipi"
|
||||
|
11
biome.json
11
biome.json
@ -1,13 +1,7 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"files": {
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.json"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.json"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
@ -21,7 +15,8 @@
|
||||
"repos/**",
|
||||
"state/**",
|
||||
"traefik/**",
|
||||
"user-config/**"
|
||||
"user-config/**",
|
||||
"playwright-report/**"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { testUser } from './helpers/constants';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginUser, createTestUser } from './fixtures/fixtures';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { createTestUser, loginUser } from './fixtures/fixtures';
|
||||
import { testUser } from './helpers/constants';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
import { setPassowrdChangeRequest, unsetPasswordChangeRequest } from './helpers/settings';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
import { testUser } from './helpers/constants';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
import { setSettings } from './helpers/settings';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { appTable } from '@runtipi/db';
|
||||
import { setSettings } from './helpers/settings';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase, db } from './helpers/db';
|
||||
import { setSettings } from './helpers/settings';
|
||||
|
||||
test.beforeEach(async () => {
|
||||
test.fixme(true, 'This test is flaky due to incorrect revalidation of the guest dashboard');
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as argon2 from 'argon2';
|
||||
import { type BrowserContext, expect, type Page } from '@playwright/test';
|
||||
import { type BrowserContext, type Page, expect } from '@playwright/test';
|
||||
import { userTable } from '@runtipi/db';
|
||||
import { db } from '../helpers/db';
|
||||
import * as argon2 from 'argon2';
|
||||
import { testUser } from '../helpers/constants';
|
||||
import { db } from '../helpers/db';
|
||||
|
||||
export const createTestUser = async () => {
|
||||
// Create user in database
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from '@runtipi/db';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const connectionString = `postgresql://tipi:${process.env.POSTGRES_PASSWORD}@${process.env.SERVER_IP}:5432/tipi?connect_timeout=300`;
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { promises } from 'fs';
|
||||
import { z } from 'zod';
|
||||
import { settingsSchema } from '@runtipi/shared';
|
||||
import { pathExists } from '@runtipi/shared/node';
|
||||
import path from 'path';
|
||||
import type { settingsSchema } from '@runtipi/shared';
|
||||
import { pathExists } from '@runtipi/shared/node';
|
||||
import type { z } from 'zod';
|
||||
import { BASE_PATH } from './constants';
|
||||
|
||||
export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtipi",
|
||||
"version": "3.5.1",
|
||||
"version": "3.5.2",
|
||||
"description": "A homeserver for everyone",
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"scripts": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
import { type NodePgDatabase, drizzle } 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>;
|
||||
|
||||
@ -34,7 +34,11 @@ export class DbClient {
|
||||
});
|
||||
|
||||
pool.on('connect', () => {
|
||||
this.logger.info('Connected to the database successfully.');
|
||||
this.logger.debug('Connected to the database successfully.');
|
||||
});
|
||||
|
||||
pool.on('remove', () => {
|
||||
this.logger.debug('Client removed from the pool.');
|
||||
});
|
||||
|
||||
this.db = drizzle(pool, { schema });
|
||||
|
@ -1,7 +1,7 @@
|
||||
import path from 'node:path';
|
||||
import pg from 'pg';
|
||||
import { migrate } from '@runtipi/postgres-migrations';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
import pg from 'pg';
|
||||
|
||||
type MigrationParams = {
|
||||
host: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
||||
import { pgTable, pgEnum, integer, varchar, timestamp, serial, boolean, text, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { boolean, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
|
||||
|
||||
const updateStatusEnum = pgEnum('update_status_enum', ['SUCCESS', 'FAILED']);
|
||||
const appStatusEnum = pgEnum('app_status_enum', [
|
||||
|
@ -13,18 +13,8 @@
|
||||
"noImplicitOverride": true,
|
||||
"module": "preserve",
|
||||
"noEmit": true,
|
||||
"lib": [
|
||||
"es2022"
|
||||
]
|
||||
"lib": ["es2022"]
|
||||
},
|
||||
"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',
|
||||
|
@ -18,11 +18,7 @@
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"node",
|
||||
"dist"
|
||||
],
|
||||
"files": ["src", "node", "dist"],
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts src",
|
||||
"tsc": "tsc --noEmit",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { newLogger as createLogger } from './Logger';
|
||||
import { ILogger } from './Logger.interface';
|
||||
import type { ILogger } from './Logger.interface';
|
||||
|
||||
export class Logger implements ILogger {
|
||||
private winstonLogger;
|
||||
@ -16,10 +16,7 @@ export class Logger implements ILogger {
|
||||
streamLogToHistory(logFile: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const appLogReadStream = fs.createReadStream(path.join(this.logsFolder, logFile), 'utf-8');
|
||||
const appLogHistoryWriteStream = fs.createWriteStream(
|
||||
path.join(this.logsFolder, `${logFile}.history`),
|
||||
{ flags: 'a' },
|
||||
);
|
||||
const appLogHistoryWriteStream = fs.createWriteStream(path.join(this.logsFolder, `${logFile}.history`), { flags: 'a' });
|
||||
|
||||
appLogReadStream
|
||||
.pipe(appLogHistoryWriteStream)
|
||||
|
@ -29,9 +29,7 @@ export const newLogger = (id: string, logsFolder: string) => {
|
||||
);
|
||||
exceptionHandlers = [new transports.File({ filename: path.join(logsFolder, 'error.log') })];
|
||||
|
||||
tr.push(
|
||||
new transports.Console({ level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' }),
|
||||
);
|
||||
tr.push(new transports.Console({ level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' }));
|
||||
} catch (error) {}
|
||||
|
||||
return createLogger({
|
||||
|
@ -1,9 +1,9 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { Logger } from '../../../logger/FileLogger';
|
||||
import fs from 'fs-extra';
|
||||
import { sanitizePath } from '../../../../helpers/sanitizers';
|
||||
import { pathExists } from '../../../helpers/fs-helpers';
|
||||
import { appInfoSchema } from '../../../../schemas/app-schemas';
|
||||
import { pathExists } from '../../../helpers/fs-helpers';
|
||||
import { Logger } from '../../../logger/FileLogger';
|
||||
|
||||
// Lower level data access class for apps
|
||||
export class DataAccessApp {
|
||||
@ -34,17 +34,11 @@ export class DataAccessApp {
|
||||
const repoAppFolder = path.join(this.getAppsRepoFolder(), sanitizePath(id));
|
||||
|
||||
if (await pathExists(path.join(repoAppFolder, 'config.json'))) {
|
||||
const configFile = await fs.promises.readFile(
|
||||
path.join(repoAppFolder, 'config.json'),
|
||||
'utf8',
|
||||
);
|
||||
const configFile = await fs.promises.readFile(path.join(repoAppFolder, 'config.json'), 'utf8');
|
||||
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile));
|
||||
|
||||
if (parsedConfig.success && parsedConfig.data.available) {
|
||||
const description = await fs.promises.readFile(
|
||||
path.join(repoAppFolder, 'metadata', 'description.md'),
|
||||
'utf8',
|
||||
);
|
||||
const description = await fs.promises.readFile(path.join(repoAppFolder, 'metadata', 'description.md'), 'utf8');
|
||||
return { ...parsedConfig.data, description };
|
||||
}
|
||||
}
|
||||
@ -62,17 +56,11 @@ export class DataAccessApp {
|
||||
const installedAppFolder = path.join(this.getInstalledAppsFolder(), sanitizePath(id));
|
||||
|
||||
if (await pathExists(path.join(installedAppFolder, 'config.json'))) {
|
||||
const configFile = await fs.promises.readFile(
|
||||
path.join(installedAppFolder, 'config.json'),
|
||||
'utf8',
|
||||
);
|
||||
const configFile = await fs.promises.readFile(path.join(installedAppFolder, 'config.json'), 'utf8');
|
||||
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile));
|
||||
|
||||
if (parsedConfig.success && parsedConfig.data.available) {
|
||||
const description = await fs.promises.readFile(
|
||||
path.join(installedAppFolder, 'metadata', 'description.md'),
|
||||
'utf8',
|
||||
);
|
||||
const description = await fs.promises.readFile(path.join(installedAppFolder, 'metadata', 'description.md'), 'utf8');
|
||||
return { ...parsedConfig.data, description };
|
||||
}
|
||||
}
|
||||
@ -107,9 +95,7 @@ export class DataAccessApp {
|
||||
const appsRepoFolder = this.getAppsRepoFolder();
|
||||
|
||||
if (!(await pathExists(appsRepoFolder))) {
|
||||
this.logger.error(
|
||||
`Apps repo ${this.appsRepoId} not found. Make sure your repo is configured correctly.`,
|
||||
);
|
||||
this.logger.error(`Apps repo ${this.appsRepoId} not found. Make sure your repo is configured correctly.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -47,16 +47,14 @@ export class AppDataService {
|
||||
}),
|
||||
);
|
||||
|
||||
return apps
|
||||
.filter(notEmpty)
|
||||
.map(({ id, categories, name, short_desc, deprecated, supported_architectures }) => ({
|
||||
id,
|
||||
categories,
|
||||
name,
|
||||
short_desc,
|
||||
deprecated,
|
||||
supported_architectures,
|
||||
}));
|
||||
return apps.filter(notEmpty).map(({ id, categories, name, short_desc, deprecated, supported_architectures }) => ({
|
||||
id,
|
||||
categories,
|
||||
name,
|
||||
short_desc,
|
||||
deprecated,
|
||||
supported_architectures,
|
||||
}));
|
||||
}
|
||||
|
||||
public async getAppBackups(params: { appId: string; pageSize: number; page: number }) {
|
||||
|
@ -65,11 +65,7 @@ const systemCommandSchema = z.object({
|
||||
command: z.literal('system_info'),
|
||||
});
|
||||
|
||||
export const eventSchema = appEventSchema
|
||||
.or(restoreAppCommandSchema)
|
||||
.or(repoCommandSchema)
|
||||
.or(updateAppCommandSchema)
|
||||
.or(systemCommandSchema);
|
||||
export const eventSchema = appEventSchema.or(restoreAppCommandSchema).or(repoCommandSchema).or(updateAppCommandSchema).or(systemCommandSchema);
|
||||
|
||||
export const eventResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
|
@ -3,11 +3,7 @@
|
||||
"target": "es2017",
|
||||
"baseUrl": ".",
|
||||
"paths": {},
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@ -22,19 +18,9 @@
|
||||
"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,10 +1,10 @@
|
||||
import type { Hono } from 'hono';
|
||||
import { jwt } from 'hono/jwt';
|
||||
import { prettyJSON } from 'hono/pretty-json';
|
||||
import { secureHeaders } from 'hono/secure-headers';
|
||||
import type { Hono } from 'hono';
|
||||
import { container } from './inversify.config';
|
||||
import { getEnv } from './lib/environment';
|
||||
import { SystemExecutors } from './services';
|
||||
import { container } from './inversify.config';
|
||||
import type { IAppExecutors } from './services/app/app.executors';
|
||||
|
||||
const system = new SystemExecutors();
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { ServiceInput, getDockerCompose } from '../docker-templates';
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type ServiceInput, getDockerCompose } from '../docker-templates';
|
||||
|
||||
describe('getDockerCompose', async () => {
|
||||
it('should return correct docker-compose config', async () => {
|
||||
|
@ -2,8 +2,8 @@ import { DockerComposeBuilder } from '@/lib/docker/builders/docker-compose-build
|
||||
import { serviceSchema } from '@/lib/docker/builders/schemas';
|
||||
import { ServiceBuilder } from '@/lib/docker/builders/service-builder';
|
||||
import { TraefikLabelsBuilder } from '@/lib/docker/builders/traefik-labels-builder';
|
||||
import { AppEventForm } from '@runtipi/shared';
|
||||
import { z } from 'zod';
|
||||
import type { AppEventForm } from '@runtipi/shared';
|
||||
import type { z } from 'zod';
|
||||
|
||||
export type ServiceInput = z.input<typeof serviceSchema>;
|
||||
export type Service = z.output<typeof serviceSchema>;
|
||||
|
@ -4,24 +4,24 @@ import 'source-map-support/register';
|
||||
import { type SystemEvent, cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
import path from 'node:path';
|
||||
import Redis from 'ioredis';
|
||||
import dotenv from 'dotenv';
|
||||
import { Queue } from 'bullmq';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { extraErrorDataIntegration } from '@sentry/integrations';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
|
||||
import { startWorker } from './watcher/watcher';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { RepoExecutors } from './services';
|
||||
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
|
||||
import { serve } from '@hono/node-server';
|
||||
import type { IMigrator } from '@runtipi/db';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
import { extraErrorDataIntegration } from '@sentry/integrations';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Queue } from 'bullmq';
|
||||
import dotenv from 'dotenv';
|
||||
import { Hono } from 'hono';
|
||||
import Redis from 'ioredis';
|
||||
import { setupRoutes } from './api';
|
||||
import { APP_DIR, DATA_DIR } from './config';
|
||||
import { socketManager } from './lib/socket';
|
||||
import { container } from './inversify.config';
|
||||
import { socketManager } from './lib/socket';
|
||||
import { RepoExecutors } from './services';
|
||||
import type { IAppExecutors } from './services/app/app.executors';
|
||||
import type { ILogger } from '@runtipi/shared/node';
|
||||
import type { IMigrator } from '@runtipi/db';
|
||||
import { startWorker } from './watcher/watcher';
|
||||
|
||||
const envFile = path.join(DATA_DIR, '.env');
|
||||
|
||||
|
@ -1,45 +1,47 @@
|
||||
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 'node:path';
|
||||
import { DbClient, type IDbClient, type IMigrator, Migrator } from '@runtipi/db';
|
||||
import { type ILogger, Logger } from '@runtipi/shared/node';
|
||||
import { Container } from 'inversify';
|
||||
import { DATA_DIR } from './config';
|
||||
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();
|
||||
try {
|
||||
const container = new Container();
|
||||
|
||||
const container = new Container();
|
||||
container.bind<ILogger>('ILogger').toDynamicValue(() => {
|
||||
return new Logger('worker', path.join(DATA_DIR, 'logs'));
|
||||
});
|
||||
container.bind<ISocketManager>('ISocketManager').to(SocketManager).inSingletonScope();
|
||||
|
||||
container.bind<ILogger>('ILogger').toDynamicValue(() => {
|
||||
return new Logger('worker', path.join(DATA_DIR, 'logs'));
|
||||
});
|
||||
container.bind<ISocketManager>('ISocketManager').to(SocketManager).inSingletonScope();
|
||||
container
|
||||
.bind<IDbClient>('IDbClient')
|
||||
.toDynamicValue((context) => {
|
||||
return new DbClient(
|
||||
{
|
||||
host: String(process.env.POSTGRES_HOST),
|
||||
port: Number(process.env.POSTGRES_PORT),
|
||||
password: String(process.env.POSTGRES_PASSWORD),
|
||||
database: String(process.env.POSTGRES_DBNAME),
|
||||
username: String(process.env.POSTGRES_USERNAME),
|
||||
},
|
||||
context.container.get<ILogger>('ILogger'),
|
||||
);
|
||||
})
|
||||
.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<IMigrator>('IMigrator').toDynamicValue((context) => {
|
||||
return new Migrator(context.container.get<ILogger>('ILogger'));
|
||||
});
|
||||
container.bind<IAppExecutors>('IAppExecutors').to(AppExecutors);
|
||||
|
||||
container.bind<IAppExecutors>('IAppExecutors').to(AppExecutors);
|
||||
|
||||
return container;
|
||||
return container;
|
||||
} catch (error) {
|
||||
console.error('Error creating container', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const container = createContainer();
|
||||
|
@ -1,25 +1,17 @@
|
||||
import fs from 'fs';
|
||||
import zlib from 'zlib';
|
||||
import { pack, extract } from 'tar-fs';
|
||||
import { extract, pack } from 'tar-fs';
|
||||
|
||||
export class ArchiveManager {
|
||||
createTarGz = async (sourceDir: string, destinationFile: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pack(sourceDir)
|
||||
.pipe(zlib.createGzip())
|
||||
.pipe(fs.createWriteStream(destinationFile))
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
pack(sourceDir).pipe(zlib.createGzip()).pipe(fs.createWriteStream(destinationFile)).on('finish', resolve).on('error', reject);
|
||||
});
|
||||
};
|
||||
|
||||
extractTarGz = async (sourceFile: string, destinationDir: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.createReadStream(sourceFile)
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(extract(destinationDir))
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
fs.createReadStream(sourceFile).pipe(zlib.createGunzip()).pipe(extract(destinationDir)).on('finish', resolve).on('error', reject);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as yaml from 'yaml';
|
||||
import { BuiltService } from './service-builder';
|
||||
import type { BuiltService } from './service-builder';
|
||||
|
||||
interface Network {
|
||||
key?: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DependsOn } from './schemas';
|
||||
import type { DependsOn } from './schemas';
|
||||
|
||||
interface ServicePort {
|
||||
containerPort: number;
|
||||
|
@ -1,10 +1,10 @@
|
||||
// const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: '', stderr: randomError }));
|
||||
|
||||
import { vi, it, describe, beforeEach, expect } from 'vitest';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import fs from 'fs';
|
||||
import { compose } from './docker-helpers';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { compose } from './docker-helpers';
|
||||
|
||||
const execAsync = vi.fn().mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { type SocketEvent, sanitizePath, socketEventSchema } from '@runtipi/shared';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import path from 'node:path';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { type SocketEvent, sanitizePath, socketEventSchema } from '@runtipi/shared';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import type { Socket } from 'socket.io';
|
||||
import { getRepoHash } from 'src/services/repo/repo.helpers';
|
||||
import { DEFAULT_REPO_URL } from '../system/system.helpers';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import dotenv from 'dotenv';
|
||||
import { z } from 'zod';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
dotenv.config({ path: '.env.dev', override: true });
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Logger } from '@runtipi/shared/node';
|
||||
import path from 'node:path';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
import { Logger } from '@runtipi/shared/node';
|
||||
|
||||
export const logger = new Logger('worker', path.join(DATA_DIR, 'logs'));
|
||||
|
@ -21,12 +21,8 @@ export class SocketManager implements ISocketManager {
|
||||
|
||||
io.on('connection', async (socket) => {
|
||||
this.logger.debug('Client connected to socket', socket.id);
|
||||
socket.on('app-logs-init', (event) =>
|
||||
handleViewAppLogsEvent(socket, event, this.emit.bind(this)),
|
||||
);
|
||||
socket.on('runtipi-logs-init', (event) =>
|
||||
handleViewRuntipiLogsEvent(socket, event, this.emit.bind(this)),
|
||||
);
|
||||
socket.on('app-logs-init', (event) => handleViewAppLogsEvent(socket, event, this.emit.bind(this)));
|
||||
socket.on('runtipi-logs-init', (event) => handleViewRuntipiLogsEvent(socket, event, this.emit.bind(this)));
|
||||
socket.on('disconnect', () => {});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { container } from 'src/inversify.config';
|
||||
import { ISocketManager } from './SocketManager';
|
||||
import type { ISocketManager } from './SocketManager';
|
||||
|
||||
export const socketManager = container.get<ISocketManager>('ISocketManager');
|
||||
|
@ -1,10 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import { it, describe, expect, beforeEach } from 'vitest';
|
||||
import path from 'path';
|
||||
import { envMapToString } from '@runtipi/shared';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { generateSystemEnvFile } from './system.helpers';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { envMapToString } from '@runtipi/shared';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { generateSystemEnvFile } from './system.helpers';
|
||||
|
||||
const envMap = new Map();
|
||||
|
||||
@ -29,10 +29,7 @@ describe('generateSystemEnvFile()', async () => {
|
||||
expect(envFileExists).toBe(true);
|
||||
|
||||
const settingsFileExists = fs.existsSync(path.join(DATA_DIR, 'state', 'settings.json'));
|
||||
const settingsFileContent = fs.readFileSync(
|
||||
path.join(DATA_DIR, 'state', 'settings.json'),
|
||||
'utf8',
|
||||
);
|
||||
const settingsFileContent = fs.readFileSync(path.join(DATA_DIR, 'state', 'settings.json'), 'utf8');
|
||||
expect(settingsFileExists).toBe(true);
|
||||
expect(settingsFileContent).toBe('{}');
|
||||
});
|
||||
@ -67,9 +64,7 @@ describe('generateSystemEnvFile()', async () => {
|
||||
const generated = await generateSystemEnvFile();
|
||||
|
||||
// assert
|
||||
expect(generated.get('APPS_REPO_ID')).toBe(
|
||||
'29ca930bfdaffa1dfabf5726336380ede7066bc53297e3c0c868b27c97282903',
|
||||
);
|
||||
expect(generated.get('APPS_REPO_ID')).toBe('29ca930bfdaffa1dfabf5726336380ede7066bc53297e3c0c868b27c97282903');
|
||||
expect(generated.get('APPS_REPO_URL')).toBe('https://github.com/runtipi/runtipi-appstore');
|
||||
expect(generated.get('JWT_SECRET')).toBeTruthy();
|
||||
expect(generated.get('DOMAIN')).toBe('example.com');
|
||||
|
@ -2,13 +2,13 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { APP_DATA_DIR, APP_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { envMapToString, envStringToMap, settingsSchema } from '@runtipi/shared';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { logger } from '../logger/logger';
|
||||
import { getRepoHash } from '../../services/repo/repo.helpers';
|
||||
import { APP_DATA_DIR, APP_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { logger } from '../logger/logger';
|
||||
|
||||
type EnvKeys =
|
||||
| 'APPS_REPO_ID'
|
||||
|
@ -1,14 +1,14 @@
|
||||
import fs from 'node:fs';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import * as sharedNode from '@runtipi/shared/node';
|
||||
import type { IAppExecutors } from '../app.executors';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import * as dockerHelpers from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import * as sharedNode from '@runtipi/shared/node';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { IAppExecutors } from '../app.executors';
|
||||
|
||||
const { pathExists } = sharedNode;
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { pathExists } from '@runtipi/shared/node';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { copyDataDir, generateEnvFile } from '../app.helpers';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import { getAppEnvMap } from '../env.helpers';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
|
||||
describe('app helpers', () => {
|
||||
describe('Test: generateEnvFile()', () => {
|
||||
@ -25,9 +25,7 @@ describe('app helpers', () => {
|
||||
expect(envmap.get('APP_PORT')).toBe(String(appConfig.port));
|
||||
expect(envmap.get('APP_ID')).toBe(appConfig.id);
|
||||
expect(envmap.get('ROOT_FOLDER_HOST')).toBe(process.env.ROOT_FOLDER_HOST);
|
||||
expect(envmap.get('APP_DATA_DIR')).toBe(
|
||||
`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`,
|
||||
);
|
||||
expect(envmap.get('APP_DATA_DIR')).toBe(`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`);
|
||||
expect(envmap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
|
||||
expect(envmap.get('APP_HOST')).toBe(`localhost`);
|
||||
expect(envmap.get('APP_PROTOCOL')).toBe(`http`);
|
||||
@ -39,9 +37,7 @@ describe('app helpers', () => {
|
||||
await fs.promises.writeFile(`${DATA_DIR}/apps/${appConfig.id}/config.json`, '{}');
|
||||
|
||||
// act & assert
|
||||
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(
|
||||
`App ${appConfig.id} has invalid config.json file`,
|
||||
);
|
||||
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
|
||||
});
|
||||
|
||||
it('Should automatically generate value for random field', async () => {
|
||||
@ -84,10 +80,7 @@ describe('app helpers', () => {
|
||||
});
|
||||
const randomField = faker.string.alphanumeric(32);
|
||||
await fs.promises.mkdir(`${APP_DATA_DIR}/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
`${APP_DATA_DIR}/${appConfig.id}/app.env`,
|
||||
`RANDOM_FIELD=${randomField}`,
|
||||
);
|
||||
await fs.promises.writeFile(`${APP_DATA_DIR}/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
|
||||
// act
|
||||
await generateEnvFile(appConfig.id, {});
|
||||
@ -259,9 +252,7 @@ describe('app helpers', () => {
|
||||
|
||||
// assert
|
||||
const appDataDir = `${APP_DATA_DIR}/${appConfig.id}`;
|
||||
expect(
|
||||
await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8'),
|
||||
).toBe('test');
|
||||
expect(await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8')).toBe('test');
|
||||
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
|
||||
});
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as Sentry from '@sentry/node';
|
||||
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 { compose } from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { getDockerCompose } from '@/config/docker-templates';
|
||||
import { ArchiveManager } from '@/lib/archive/ArchiveManager';
|
||||
import { type App, appTable, type IDbClient } from '@runtipi/db';
|
||||
import { compose } from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import type { ISocketManager } from '@/lib/socket/SocketManager';
|
||||
import { type App, type IDbClient, appTable } from '@runtipi/db';
|
||||
import { type AppEventForm, type SocketEvent, appInfoSchema, sanitizePath } from '@runtipi/shared';
|
||||
import { type ILogger, execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { and, eq, ne } from 'drizzle-orm';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import type { ISocketManager } from '@/lib/socket/SocketManager';
|
||||
import { copyDataDir, generateEnvFile } from './app.helpers';
|
||||
|
||||
export interface IAppExecutors {
|
||||
regenerateAppEnv(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;
|
||||
|
@ -1,18 +1,12 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
AppEventForm,
|
||||
appInfoSchema,
|
||||
envMapToString,
|
||||
envStringToMap,
|
||||
sanitizePath,
|
||||
} from '@runtipi/shared';
|
||||
import { pathExists, execAsync } from '@runtipi/shared/node';
|
||||
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { getMainEnvMap } from '@/lib/system/system.helpers';
|
||||
import { type AppEventForm, appInfoSchema, envMapToString, envStringToMap, sanitizePath } from '@runtipi/shared';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
|
||||
|
||||
/**
|
||||
* This function generates a random string of the provided length by using the SHA-256 hash algorithm.
|
||||
@ -45,9 +39,7 @@ const getEntropy = async (name: string, length: number) => {
|
||||
export const generateEnvFile = async (appId: string, form: AppEventForm) => {
|
||||
const { internalIp, appDataPath, rootFolderHost } = getEnv();
|
||||
|
||||
const configFile = await fs.promises.readFile(
|
||||
path.join(DATA_DIR, 'apps', sanitizePath(appId), 'config.json'),
|
||||
);
|
||||
const configFile = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(appId), 'config.json'));
|
||||
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile.toString()));
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
@ -115,17 +107,12 @@ export const generateEnvFile = async (appId: string, form: AppEventForm) => {
|
||||
}
|
||||
|
||||
// Create app-data folder if it doesn't exist
|
||||
const appDataDirectoryExists = await fs.promises
|
||||
.stat(path.join(APP_DATA_DIR, sanitizePath(appId)))
|
||||
.catch(() => false);
|
||||
const appDataDirectoryExists = await fs.promises.stat(path.join(APP_DATA_DIR, sanitizePath(appId))).catch(() => false);
|
||||
if (!appDataDirectoryExists) {
|
||||
await fs.promises.mkdir(path.join(APP_DATA_DIR, sanitizePath(appId)), { recursive: true });
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'),
|
||||
envMapToString(envMap),
|
||||
);
|
||||
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'), envMapToString(envMap));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -169,16 +156,10 @@ export const copyDataDir = async (id: string) => {
|
||||
|
||||
const processFile = async (file: string) => {
|
||||
if (file.endsWith('.template')) {
|
||||
const template = await fs.promises.readFile(
|
||||
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file),
|
||||
'utf-8',
|
||||
);
|
||||
const template = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file), 'utf-8');
|
||||
const renderedTemplate = renderTemplate(template, envMap);
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(APP_DATA_DIR, sanitizePath(id), 'data', file.replace('.template', '')),
|
||||
renderedTemplate,
|
||||
);
|
||||
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(id), 'data', file.replace('.template', '')), renderedTemplate);
|
||||
} else {
|
||||
await fs.promises.copyFile(
|
||||
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file),
|
||||
@ -192,9 +173,7 @@ export const copyDataDir = async (id: string) => {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const files = await fs.promises.readdir(
|
||||
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p),
|
||||
);
|
||||
const files = await fs.promises.readdir(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p));
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
@ -223,8 +202,6 @@ export const copyDataDir = async (id: string) => {
|
||||
|
||||
// Remove any .gitkeep files from the app-data folder at any level
|
||||
if (await pathExists(path.join(APP_DATA_DIR, sanitizePath(id), 'data'))) {
|
||||
await execAsync(`find ${APP_DATA_DIR}/${sanitizePath(id)}/data -name .gitkeep -delete`).catch(
|
||||
() => {},
|
||||
);
|
||||
await execAsync(`find ${APP_DATA_DIR}/${sanitizePath(id)}/data -name .gitkeep -delete`).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import webpush from 'web-push';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { APP_DATA_DIR } from '@/config/constants';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import webpush from 'web-push';
|
||||
|
||||
/**
|
||||
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
|
||||
|
@ -1,10 +1,10 @@
|
||||
import path from 'node:path';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
import { getRepoBaseUrlAndBranch, getRepoHash } from './repo.helpers';
|
||||
|
||||
export class RepoExecutors {
|
||||
private readonly logger;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import si from 'systeminformation';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { logger } from '@/lib/logger';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import si from 'systeminformation';
|
||||
|
||||
export class SystemExecutors {
|
||||
private readonly logger;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { RepoExecutors } from '@/services';
|
||||
import { eventSchema } from '@runtipi/shared';
|
||||
import { Worker } from 'bullmq';
|
||||
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';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import fs from 'fs';
|
||||
import { APP_CATEGORIES, AppInfo, appInfoSchema } from '@runtipi/shared';
|
||||
import { DATA_DIR, APP_DATA_DIR } from '@/config/constants';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { APP_CATEGORIES, type AppInfo, appInfoSchema } from '@runtipi/shared';
|
||||
|
||||
export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) => {
|
||||
const appInfo = appInfoSchema.parse({
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'reflect-metadata';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('@runtipi/shared/node', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as object;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { UserWorkspaceConfig, defineConfig } from 'vitest/config';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { type UserWorkspaceConfig, defineConfig } from 'vitest/config';
|
||||
|
||||
type Plugin = Exclude<UserWorkspaceConfig['plugins'], undefined>[number];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { cleanseErrorData, settingsSchema } from '@runtipi/shared';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { settingsSchema, cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
const getClientConfig = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { getCurrentLocale } from 'src/utils/getCurrentLocale';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import Image from 'next/image';
|
||||
import type React from 'react';
|
||||
import { getCurrentLocale } from 'src/utils/getCurrentLocale';
|
||||
import { LanguageSelector } from '../components/LanguageSelector';
|
||||
|
||||
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { loginAction } from '@/actions/login/login-action';
|
||||
import { verifyTotpAction } from '@/actions/verify-totp/verify-totp-action';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { LoginForm } from '../LoginForm';
|
||||
import { TotpForm } from '../TotpForm';
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import type React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import z from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { LoginContainer } from './components/LoginContainer';
|
||||
import { redirect } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { LoginContainer } from './components/LoginContainer';
|
||||
|
||||
export default async function LoginPage() {
|
||||
const authQueries = container.get<IAuthQueries>('IAuthQueries');
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { registerAction } from '@/actions/register/register-action';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type React from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { RegisterForm } from '../RegisterForm';
|
||||
|
||||
export const RegisterContainer: React.FC = () => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { RegisterContainer } from './components/RegisterContainer';
|
||||
import { redirect } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { RegisterContainer } from './components/RegisterContainer';
|
||||
|
||||
export default async function LoginPage() {
|
||||
const user = await getUserFromCookie();
|
||||
|
@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { resetPasswordAction } from '@/actions/reset-password/reset-password-action';
|
||||
import { cancelResetPasswordAction } from '@/actions/cancel-reset-password/cancel-reset-password-action';
|
||||
import { resetPasswordAction } from '@/actions/reset-password/reset-password-action';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type React from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ResetPasswordForm } from '../ResetPasswordForm';
|
||||
|
||||
export const ResetPasswordContainer: React.FC = () => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
import React from 'react';
|
||||
import { container } from 'src/inversify.config';
|
||||
import { ResetPasswordContainer } from './components/ResetPasswordContainer';
|
||||
|
||||
export default async function ResetPasswordPage() {
|
||||
const authService = container.get<IAuthService>('IAuthService');
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import React from 'react';
|
||||
import { vi, afterEach, describe, it, expect } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, userEvent, waitFor } from '../../../../../../../tests/test-utils';
|
||||
import { AppActions } from './AppActions';
|
||||
import { cleanup, render, screen, waitFor, userEvent } from '../../../../../../../tests/test-utils';
|
||||
import { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
|
@ -7,15 +7,17 @@ import {
|
||||
IconLockOff,
|
||||
IconPlayerPause,
|
||||
IconPlayerPlay,
|
||||
IconRotateClockwise,
|
||||
IconSettings,
|
||||
IconTrash,
|
||||
IconX,
|
||||
IconRotateClockwise,
|
||||
} from '@tabler/icons-react';
|
||||
import type React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { startAppAction } from '@/actions/app-actions/start-app-action';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { Button, type ButtonProps } from '@/components/ui/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -24,22 +26,20 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/DropdownMenu';
|
||||
import { Button, type ButtonProps } from '@/components/ui/Button';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
||||
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import { InstallModal } from '../InstallModal';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { startAppAction } from '@/actions/app-actions/start-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { StopModal } from '../StopModal';
|
||||
import { InstallModal } from '../InstallModal';
|
||||
import { ResetAppModal } from '../ResetAppModal';
|
||||
import { RestartModal } from '../RestartModal';
|
||||
import { StopModal } from '../StopModal';
|
||||
import { UninstallModal } from '../UninstallModal';
|
||||
import { UpdateModal } from '../UpdateModal';
|
||||
import { ResetAppModal } from '../ResetAppModal';
|
||||
import { castAppConfig } from '@/lib/helpers/castAppConfig';
|
||||
import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal';
|
||||
import styles from './AppActions.module.scss';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
|
||||
interface IProps {
|
||||
app: Awaited<ReturnType<GetAppCommand['execute']>>;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { AppBackupsApiResponse } from '@/api/app-backups/route';
|
||||
import { AppLogo } from '@/components/AppLogo';
|
||||
import { AppStatus } from '@/components/AppStatus';
|
||||
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
import { AppActions } from '../AppActions';
|
||||
import { AppDetailsTabs } from '../AppDetailsTabs';
|
||||
import { GetAppCommand } from '@/server/services/app-catalog/commands';
|
||||
import { AppBackupsApiResponse } from '@/api/app-backups/route';
|
||||
|
||||
type AppDetailsContainerProps = {
|
||||
app: Awaited<ReturnType<GetAppCommand['execute']>>;
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { createAppBackupAction } from '@/actions/backup/create-app-backup-action';
|
||||
import { deleteAppBackupAction } from '@/actions/backup/delete-app-backup';
|
||||
import { restoreAppBackupAction } from '@/actions/backup/restore-app-backup-action';
|
||||
import type { AppBackup, AppBackupsApiResponse } from '@/api/app-backups/route';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { DateFormat } from '@/components/DateFormat/DateFormat';
|
||||
import { FileSize } from '@/components/FileSize/FileSize';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/Table';
|
||||
import React from 'react';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { TablePagination } from 'src/app/components/TablePagination/TablePagination';
|
||||
import { BackupModal } from '../BackupModal';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import toast from 'react-hot-toast';
|
||||
import { RestoreModal } from '../RestoreModal';
|
||||
import { FileSize } from '@/components/FileSize/FileSize';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import { DateFormat } from '@/components/DateFormat/DateFormat';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DeleteBackupModal } from '../DeleteBackupModal/DeleteBackupModal';
|
||||
import { createAppBackupAction } from '@/actions/backup/create-app-backup-action';
|
||||
import { restoreAppBackupAction } from '@/actions/backup/restore-app-backup-action';
|
||||
import { deleteAppBackupAction } from '@/actions/backup/delete-app-backup';
|
||||
import { RestoreModal } from '../RestoreModal';
|
||||
|
||||
type Props = {
|
||||
info: AppInfo;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
status: AppStatus;
|
||||
|
@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { Tabs, TabsContent } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { DataGrid, DataGridItem } from '@/components/ui/DataGrid';
|
||||
import { AppLogs } from './AppLogs';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import { AppBackups } from './AppBackups';
|
||||
import type { AppBackupsApiResponse } from '@/api/app-backups/route';
|
||||
import { Markdown } from '@/components/Markdown';
|
||||
import { DataGrid, DataGridItem } from '@/components/ui/DataGrid';
|
||||
import { Tabs, TabsContent } from '@/components/ui/tabs';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { AppBackups } from './AppBackups';
|
||||
import { AppDetailsTabTriggers } from './AppDetailsTabTriggers';
|
||||
import { AppLogs } from './AppLogs';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useSocket } from '@/lib/socket/useSocket';
|
||||
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
|
||||
import { ClientOnly } from '@/components/ClientOnly/ClientOnly';
|
||||
import { useSocket } from '@/lib/socket/useSocket';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
|
||||
|
||||
export const AppLogs = ({ appId }: { appId: string }) => {
|
||||
let nextId = 0;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import type { AppBackup } from '@/api/app-backups/route';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useDateFormat } from '@/components/DateFormat/DateFormat';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
|
||||
interface IProps {
|
||||
backup?: AppBackup | null;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { FormField } from '@runtipi/shared';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { FormField } from '@runtipi/shared';
|
||||
import { vi, it, beforeEach, describe, expect } from 'vitest';
|
||||
import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '../../../../../../../tests/test-utils';
|
||||
import { InstallForm } from './InstallForm';
|
||||
|
||||
|
@ -2,18 +2,18 @@ import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { useClientSettings } from '@/hooks/useClientSettings';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import type { AppInfo, FormField } from '@runtipi/shared';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
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 type { AppStatus } from '@runtipi/db';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { validateAppConfig } from '../../utils/validators';
|
||||
import { InstallFormField } from './InstallFormField';
|
||||
import type { FormValues } from './InstallForm.types';
|
||||
import { useClientSettings } from '@/hooks/useClientSettings';
|
||||
import { InstallFormField } from './InstallFormField';
|
||||
|
||||
interface IProps {
|
||||
formFields: FormField[];
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { FormField } from '@runtipi/shared';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import type { FormField } from '@runtipi/shared';
|
||||
import clsx from 'clsx';
|
||||
import { Control, Controller, UseFormRegister } from 'react-hook-form';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { FormValues } from './InstallForm.types';
|
||||
import React from 'react';
|
||||
import { type Control, Controller, type UseFormRegister } from 'react-hook-form';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import type { FormValues } from './InstallForm.types';
|
||||
|
||||
type IProps = {
|
||||
field: FormField;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import React from 'react';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { InstallModal } from './InstallModal';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '../../../../../../../tests/test-utils';
|
||||
import { InstallModal } from './InstallModal';
|
||||
|
||||
describe('InstallModal', () => {
|
||||
const app = {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { ScrollArea } from '@/components/ui/ScrollArea';
|
||||
import { InstallForm } from '../InstallForm';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { installAppAction } from '@/actions/app-actions/install-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { ScrollArea } from '@/components/ui/ScrollArea';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { InstallForm } from '../InstallForm';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { resetAppAction } from '@/actions/app-actions/reset-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { restartAppAction } from '@/actions/app-actions/restart-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import type { AppBackup } from '@/api/app-backups/route';
|
||||
import { useDateFormat } from '@/components/DateFormat/DateFormat';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type React from 'react';
|
||||
|
||||
interface IProps {
|
||||
backup?: AppBackup | null;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { stopAppAction } from '@/actions/app-actions/stop-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { uninstallAppAction } from '@/actions/app-actions/uninstall-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { vi, expect, describe, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '../../../../../../../tests/test-utils';
|
||||
import { UpdateModal } from './UpdateModal';
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { updateAppAction } from '@/actions/app-actions/update-app-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { useAppStatus } from '@/hooks/useAppStatus';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface IProps {
|
||||
newVersion: string;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type React from 'react';
|
||||
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { ScrollArea } from '@/components/ui/ScrollArea';
|
||||
import type { AppStatus } from '@runtipi/db';
|
||||
import { InstallForm, type FormValues } from '../InstallForm';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { type FormValues, InstallForm } from '../InstallForm';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { ErrorPage } from '@/components/ui/ErrorPage';
|
||||
import { getTranslator } from '@/lib/get-translator';
|
||||
import { MessageKey, TranslatedError } from '@/server/utils/errors';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { appBackupService } from '@/server/services/app-backup/app-backup.service';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import { type MessageKey, TranslatedError } from '@/server/utils/errors';
|
||||
import type { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer';
|
||||
|
||||
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
||||
return {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FormField } from '@runtipi/shared';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FormField } from '@runtipi/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validateAppConfig, validateField } from './validators';
|
||||
|
||||
describe('Test: validateField', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import validator from 'validator';
|
||||
import { useUIStore } from '@/client/state/uiStore';
|
||||
import type { FormField } from '@runtipi/shared';
|
||||
import validator from 'validator';
|
||||
|
||||
export const validateField = (field: FormField, value: string | undefined | boolean): string | undefined => {
|
||||
const { translator } = useUIStore.getState();
|
||||
|
@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import type { AppStoreApiResponse } from '@/api/app-store/route';
|
||||
import { useInfiniteScroll } from '@/client/hooks/useInfiniteScroll';
|
||||
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import type React from 'react';
|
||||
import { EmptyPage } from '../../../../components/EmptyPage';
|
||||
import { AppStoreTile } from '../AppStoreTile';
|
||||
import { useAppStoreState } from '../../state/appStoreState';
|
||||
import { AppStoreTile } from '../AppStoreTile';
|
||||
|
||||
interface IProps {
|
||||
initialData: AppStoreApiResponse;
|
||||
@ -47,7 +47,7 @@ export const AppStoreTable: React.FC<IProps> = ({ initialData }) => {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const apps = data?.pages.map((page) => page.data).flat();
|
||||
const apps = data?.pages.flatMap((page) => page.data);
|
||||
|
||||
const { lastElementRef } = useInfiniteScroll({
|
||||
fetchNextPage,
|
||||
|
@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import styles from './AppStoreTableActions.module.scss';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useAppStoreState } from '../../state/appStoreState';
|
||||
import { CategorySelector } from '../CategorySelector';
|
||||
import styles from './AppStoreTableActions.module.scss';
|
||||
|
||||
export const AppStoreTableActions = () => {
|
||||
const { setCategory, category, search: initialSearch, setSearch } = useAppStoreState();
|
||||
|
@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppCategory } from '@runtipi/shared';
|
||||
import { AppLogo } from '@/components/AppLogo';
|
||||
import { limitText } from '@/lib/helpers/text-helpers';
|
||||
import styles from './AppStoreTile.module.scss';
|
||||
import type { AppCategory } from '@runtipi/shared';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import type React from 'react';
|
||||
import { colorSchemeForCategory } from '../../helpers/table.helpers';
|
||||
import styles from './AppStoreTile.module.scss';
|
||||
|
||||
type App = {
|
||||
id: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { CategorySelector } from './CategorySelector';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '../../../../../../tests/test-utils';
|
||||
import { CategorySelector } from './CategorySelector';
|
||||
|
||||
describe('Test: CategorySelector', () => {
|
||||
it('should render without crashing', () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Select, SelectItem, SelectValue, SelectContent, SelectTrigger } from '@/components/ui/Select';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
|
||||
import type { AppCategory } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppCategory } from '@runtipi/shared';
|
||||
import React, { useState } from 'react';
|
||||
import { iconForCategory } from '../../helpers/table.helpers';
|
||||
|
||||
interface Props {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { limitText } from '@/lib/helpers/text-helpers';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createAppConfig } from '../../../../../server/tests/apps.factory';
|
||||
import { sortTable } from '../table.helpers';
|
||||
import { AppTableData } from '../table.types';
|
||||
import type { AppTableData } from '../table.types';
|
||||
|
||||
describe('sortTable function', () => {
|
||||
const app = createAppConfig({ id: 'a', name: 'a', categories: ['social'] });
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppCategory, AppInfo } from '@runtipi/shared';
|
||||
import type { AppCategory, AppInfo } from '@runtipi/shared';
|
||||
import {
|
||||
IconBook,
|
||||
IconBrain,
|
||||
@ -16,7 +16,7 @@ import {
|
||||
IconTool,
|
||||
IconUsers,
|
||||
} from '@tabler/icons-react';
|
||||
import { AppTableData } from './table.types';
|
||||
import type { AppTableData } from './table.types';
|
||||
|
||||
type SortParams = {
|
||||
data: AppTableData;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import type { AppInfo } from '@runtipi/shared';
|
||||
|
||||
export type SortableColumns = keyof Pick<AppInfo, 'id'>;
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { getTranslator } from '@/lib/get-translator';
|
||||
import { AppStoreTable } from './components/AppStoreTable';
|
||||
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
|
||||
import type { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
import { AppStoreTable } from './components/AppStoreTable';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const translator = await getTranslator();
|
||||
|
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import clsx from 'clsx';
|
||||
import { IconNewSection } from '@tabler/icons-react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React from 'react';
|
||||
import { AddLinkModal } from './AddLinkModal';
|
||||
import styles from './addLink.module.css';
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { addLinkAction } from '@/actions/custom-links/add-link-action';
|
||||
import { editLinkAction } from '@/actions/custom-links/edit-link-action';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import React, { useEffect } from 'react';
|
||||
import { addLinkAction } from '@/actions/custom-links/add-link-action';
|
||||
import { editLinkAction } from '@/actions/custom-links/edit-link-action';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { z } from 'zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { LinkInfo } from '@runtipi/shared';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { LinkInfo } from '@runtipi/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { z } from 'zod';
|
||||
|
||||
type FormValues = { title: string; url: string; description: string | null; iconUrl: string | null };
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { deleteLinkAction } from '@/actions/custom-links/delete-link-action';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { deleteLinkAction } from '@/actions/custom-links/delete-link-action';
|
||||
import type React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type DeleteLinkModalProps = {
|
||||
isOpen: boolean;
|
||||
|
@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useCookies } from 'next-client-cookies';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React from 'react';
|
||||
import { OffCanvas } from 'src/app/components/OffCanvas/OffCanvas';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { useCookies } from 'next-client-cookies';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
|
||||
export const AtRiskBanner = ({ isInsecure }: { isInsecure: boolean }) => {
|
||||
const { isOpen, close } = useDisclosure(isInsecure);
|
||||
|
@ -1,21 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { IconBrandGithub, IconHeart, IconLogin, IconLogout, IconMoon, IconSun } from '@tabler/icons-react';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type React from 'react';
|
||||
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useUIStore } from '@/client/state/uiStore';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { logoutAction } from '@/actions/logout/logout-action';
|
||||
import Script from 'next/script';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { NavBar } from '../NavBar';
|
||||
import { useUIStore } from '@/client/state/uiStore';
|
||||
import { useClientSettings } from '@/hooks/useClientSettings';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { NavBar } from '../NavBar';
|
||||
|
||||
interface IProps {
|
||||
isUpdateAvailable?: boolean;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user