refactor: extract all db queries into their own class
This commit is contained in:
parent
f22e49f920
commit
06099c0ae4
8 changed files with 249 additions and 122 deletions
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { type AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||
import type { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||
import styles from './AppStatus.module.scss';
|
||||
|
||||
export const AppStatus: React.FC<{ status: AppStatusEnum; lite?: boolean }> = ({ status, lite }) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Link from 'next/link';
|
|||
import React from 'react';
|
||||
import { IconDownload } from '@tabler/icons-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||
import type { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||
import { AppStatus } from '../AppStatus';
|
||||
import { AppLogo } from '../AppLogo/AppLogo';
|
||||
import { limitText } from '../../modules/AppStore/helpers/table.helpers';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { AppStatus } from '@/server/db/schema';
|
||||
import type { AppStatus } from '@/server/db/schema';
|
||||
import { APP_CATEGORIES } from '../../../server/services/apps/apps.types';
|
||||
import { App, AppCategory, AppInfo, AppWithInfo } from '../../core/types';
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ export const userTable = pgTable('user', {
|
|||
salt: text('salt'),
|
||||
});
|
||||
export type User = InferModel<typeof userTable>;
|
||||
export type NewUser = InferModel<typeof userTable, 'insert'>;
|
||||
|
||||
export const update = pgTable('update', {
|
||||
id: serial('id').notNull(),
|
||||
|
|
90
src/server/queries/apps/apps.queries.ts
Normal file
90
src/server/queries/apps/apps.queries.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { and, asc, eq, ne, notInArray } from 'drizzle-orm';
|
||||
import { appTable, NewApp, AppStatus } from '../../db/schema';
|
||||
|
||||
export class AppQueries {
|
||||
private db;
|
||||
|
||||
constructor(p: NodePgDatabase) {
|
||||
this.db = p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app id, return the app
|
||||
*
|
||||
* @param {string} appId - The id of the app to return
|
||||
*/
|
||||
public async getApp(appId: string) {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, appId));
|
||||
return apps[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app id, update the app with the given data
|
||||
*
|
||||
* @param {string} appId - The id of the app to update
|
||||
* @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();
|
||||
return updatedApps[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app id, delete the app
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given app data, creates a new app
|
||||
*
|
||||
* @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();
|
||||
return newApps[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all apps installed with the given status sorted by id ascending
|
||||
*
|
||||
* @param {AppStatus} status - The status of the apps to return
|
||||
*/
|
||||
public async getAppsByStatus(status: AppStatus) {
|
||||
return this.db.select().from(appTable).where(eq(appTable.status, status)).orderBy(asc(appTable.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all apps installed sorted by id ascending
|
||||
*/
|
||||
public async getApps() {
|
||||
return this.db.select().from(appTable).orderBy(asc(appTable.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a domain, return all apps that have this domain, are exposed and not the given id
|
||||
*
|
||||
* @param {string} domain - The domain to search for
|
||||
* @param {string} id - The id of the app to exclude
|
||||
*/
|
||||
public async getAppsByDomain(domain: string, id: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(appTable)
|
||||
.where(and(eq(appTable.domain, domain), eq(appTable.exposed, true), ne(appTable.id, id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of app status, update all apps that have a status not in the array with new values
|
||||
*
|
||||
* @param {AppStatus[]} statuses - The statuses to exclude from the update
|
||||
* @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();
|
||||
}
|
||||
}
|
90
src/server/queries/auth/auth.queries.ts
Normal file
90
src/server/queries/auth/auth.queries.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { asc, eq } from 'drizzle-orm';
|
||||
import { userTable, NewUser } from '../../db/schema';
|
||||
|
||||
export class AuthQueries {
|
||||
private db;
|
||||
|
||||
constructor(p: NodePgDatabase) {
|
||||
this.db = p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a username, return the user associated to it
|
||||
*
|
||||
* @param {string} username - The username of the user to return
|
||||
*/
|
||||
public async getUserByUsername(username: string) {
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.username, username.trim().toLowerCase()));
|
||||
return users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a userId, return the user associated to it
|
||||
*
|
||||
* @param {number} id - The id of the user to return
|
||||
*/
|
||||
public async getUserById(id: number) {
|
||||
const users = await this.db
|
||||
.select()
|
||||
.from(userTable)
|
||||
.where(eq(userTable.id, Number(id)));
|
||||
|
||||
return users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a userId, return the user associated to it with only the id, username, and totpEnabled fields
|
||||
*
|
||||
* @param {number} id - The id of the user to return
|
||||
*/
|
||||
public async getUserDtoById(id: number) {
|
||||
const users = await this.db
|
||||
.select({ id: userTable.id, username: userTable.username, totpEnabled: userTable.totpEnabled })
|
||||
.from(userTable)
|
||||
.where(eq(userTable.id, Number(id)));
|
||||
|
||||
return users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a userId, update the user with the given data
|
||||
*
|
||||
* @param {number} id - The id of the user to update
|
||||
* @param {Partial<NewUser>} data - The data to update the user with
|
||||
*/
|
||||
public async updateUser(id: number, data: Partial<NewUser>) {
|
||||
const updatedUsers = await this.db
|
||||
.update(userTable)
|
||||
.set(data)
|
||||
.where(eq(userTable.id, Number(id)))
|
||||
.returning();
|
||||
|
||||
return updatedUsers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all operators registered in the system
|
||||
*/
|
||||
public async getOperators() {
|
||||
return this.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first operator found in the system
|
||||
*/
|
||||
public async getFirstOperator() {
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.operator, true)).orderBy(asc(userTable.id)).limit(1);
|
||||
return users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given user data, creates a new user
|
||||
*
|
||||
* @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();
|
||||
return newUsers[0];
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import validator from 'validator';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { appTable, App } from '@/server/db/schema';
|
||||
import { and, asc, eq, ne, notInArray } from 'drizzle-orm';
|
||||
import { App } from '@/server/db/schema';
|
||||
import { AppQueries } from '@/server/queries/apps/apps.queries';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, AppInfo, getAppInfo, getUpdateInfo } from './apps.helpers';
|
||||
import { getConfig } from '../../core/TipiConfig';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
|
@ -22,10 +22,10 @@ const filterApp = (app: AppInfo): boolean => {
|
|||
const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(filterApp);
|
||||
|
||||
export class AppServiceClass {
|
||||
private db;
|
||||
private queries;
|
||||
|
||||
constructor(p: NodePgDatabase) {
|
||||
this.db = p;
|
||||
this.queries = new AppQueries(p);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,17 +33,12 @@ export class AppServiceClass {
|
|||
* It finds all the running apps and starts them by regenerating the env file, checking the env file and dispatching the start event.
|
||||
* If the start event is successful, the app's status is updated to 'running', otherwise, it is updated to 'stopped'
|
||||
* If there is an error while starting the app, it logs the error and updates the app's status to 'stopped'.
|
||||
*
|
||||
* @returns {Promise<void>} - A promise that resolves when all apps are started.
|
||||
*/
|
||||
public async startAllApps() {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.status, 'running')).orderBy(asc(appTable.id));
|
||||
const apps = await this.queries.getAppsByStatus('running');
|
||||
|
||||
// Update all apps with status different than running or stopped to stopped
|
||||
await this.db
|
||||
.update(appTable)
|
||||
.set({ status: 'stopped' })
|
||||
.where(notInArray(appTable.status, ['running', 'stopped']));
|
||||
await this.queries.updateAppsByStatusNotIn(['running', 'stopped', 'missing'], { status: 'stopped' });
|
||||
|
||||
await Promise.all(
|
||||
apps.map(async (app) => {
|
||||
|
@ -53,17 +48,17 @@ export class AppServiceClass {
|
|||
generateEnvFile(app);
|
||||
checkEnvFile(app.id);
|
||||
|
||||
await this.db.update(appTable).set({ status: 'starting' }).where(eq(appTable.id, app.id));
|
||||
await this.queries.updateApp(app.id, { status: 'starting' });
|
||||
|
||||
EventDispatcher.dispatchEventAsync('app', ['start', app.id]).then(({ success }) => {
|
||||
if (success) {
|
||||
this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, app.id)).execute();
|
||||
this.queries.updateApp(app.id, { status: 'running' });
|
||||
} else {
|
||||
this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, app.id)).execute();
|
||||
this.queries.updateApp(app.id, { status: 'stopped' });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, app.id));
|
||||
await this.queries.updateApp(app.id, { status: 'stopped' });
|
||||
Logger.error(e);
|
||||
}
|
||||
}),
|
||||
|
@ -75,13 +70,10 @@ export class AppServiceClass {
|
|||
* It updates the app's status in the database to 'starting' and 'running' if the start process is successful, otherwise it updates the status to 'stopped'.
|
||||
*
|
||||
* @param {string} appName - The name of the app to start
|
||||
* @returns {Promise<App | null>} - Returns a promise that resolves with the updated app information.
|
||||
* @throws {Error} - If the app is not found or the start process fails.
|
||||
*/
|
||||
public startApp = async (appName: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, appName));
|
||||
const app = apps[0];
|
||||
|
||||
const app = await this.queries.getApp(appName);
|
||||
if (!app) {
|
||||
throw new Error(`App ${appName} not found`);
|
||||
}
|
||||
|
@ -91,18 +83,18 @@ export class AppServiceClass {
|
|||
generateEnvFile(app);
|
||||
checkEnvFile(appName);
|
||||
|
||||
await this.db.update(appTable).set({ status: 'starting' }).where(eq(appTable.id, appName));
|
||||
await this.queries.updateApp(appName, { status: 'starting' });
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['start', app.id]);
|
||||
|
||||
if (success) {
|
||||
await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, appName));
|
||||
await this.queries.updateApp(appName, { status: 'running' });
|
||||
} else {
|
||||
await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, appName));
|
||||
await this.queries.updateApp(appName, { status: 'stopped' });
|
||||
throw new Error(`App ${appName} failed to start\nstdout: ${stdout}`);
|
||||
}
|
||||
|
||||
const updateApps = await this.db.select().from(appTable).where(eq(appTable.id, appName));
|
||||
return updateApps[0];
|
||||
const updatedApp = await this.queries.getApp(appName);
|
||||
return updatedApp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -112,11 +104,9 @@ export class AppServiceClass {
|
|||
* @param {Record<string, string>} form - The form data submitted by the user
|
||||
* @param {boolean} [exposed] - A flag indicating if the app will be exposed to the internet
|
||||
* @param {string} [domain] - The domain name to expose the app to the internet, required if exposed is true
|
||||
* @returns {Promise<App | null>} Returns a promise that resolves to the installed app object
|
||||
*/
|
||||
public installApp = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
const app = apps[0];
|
||||
const app = await this.queries.getApp(id);
|
||||
|
||||
if (app) {
|
||||
await this.startApp(id);
|
||||
|
@ -150,21 +140,14 @@ export class AppServiceClass {
|
|||
}
|
||||
|
||||
if (exposed && domain) {
|
||||
const appsWithSameDomain = await this.db
|
||||
.select()
|
||||
.from(appTable)
|
||||
.where(and(eq(appTable.domain, domain), eq(appTable.exposed, true)));
|
||||
const appsWithSameDomain = await this.queries.getAppsByDomain(domain, id);
|
||||
|
||||
if (appsWithSameDomain.length > 0) {
|
||||
throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0]?.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const newApps = await this.db
|
||||
.insert(appTable)
|
||||
.values({ id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain: domain || null })
|
||||
.returning();
|
||||
const newApp = newApps[0];
|
||||
const newApp = await this.queries.createApp({ id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain: domain || null });
|
||||
|
||||
if (newApp) {
|
||||
// Create env file
|
||||
|
@ -175,19 +158,17 @@ export class AppServiceClass {
|
|||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['install', id]);
|
||||
|
||||
if (!success) {
|
||||
await this.db.delete(appTable).where(eq(appTable.id, id));
|
||||
await this.queries.deleteApp(id);
|
||||
throw new Error(`App ${id} failed to install\nstdout: ${stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedApp = await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, id)).returning();
|
||||
return updatedApp[0];
|
||||
const updatedApp = await this.queries.updateApp(id, { status: 'running' });
|
||||
return updatedApp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lists available apps
|
||||
*
|
||||
* @returns {Promise<{apps: Array<AppInfo>, total: number }>} An object containing list of apps and total number of apps
|
||||
*/
|
||||
public static listApps = async () => {
|
||||
const apps = await getAvailableApps();
|
||||
|
@ -203,7 +184,6 @@ export class AppServiceClass {
|
|||
* @param {object} form - The new configuration of the app.
|
||||
* @param {boolean} [exposed=false] - If the app should be exposed or not.
|
||||
* @param {string} [domain] - The domain for the app if exposed is true.
|
||||
* @returns {Promise<App | null>} The updated app
|
||||
*/
|
||||
public updateAppConfig = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string) => {
|
||||
if (exposed && !domain) {
|
||||
|
@ -214,8 +194,7 @@ export class AppServiceClass {
|
|||
throw new Error(`Domain ${domain} is not valid`);
|
||||
}
|
||||
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
const app = apps[0];
|
||||
const app = await this.queries.getApp(id);
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
|
@ -236,23 +215,14 @@ export class AppServiceClass {
|
|||
}
|
||||
|
||||
if (exposed && domain) {
|
||||
const appsWithSameDomain = await this.db
|
||||
.select()
|
||||
.from(appTable)
|
||||
.where(and(eq(appTable.domain, domain), eq(appTable.exposed, true), ne(appTable.id, id)));
|
||||
const appsWithSameDomain = await this.queries.getAppsByDomain(domain, id);
|
||||
|
||||
if (appsWithSameDomain.length > 0) {
|
||||
throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0]?.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const updateApps = await this.db
|
||||
.update(appTable)
|
||||
.set({ exposed: exposed || false, domain: domain || null, config: form })
|
||||
.where(eq(appTable.id, id))
|
||||
.returning();
|
||||
|
||||
const updatedApp = updateApps[0];
|
||||
const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form });
|
||||
|
||||
if (updatedApp) {
|
||||
generateEnvFile(updatedApp);
|
||||
|
@ -265,12 +235,10 @@ export class AppServiceClass {
|
|||
* Stops a running application by its id
|
||||
*
|
||||
* @param {string} id - The id of the application to stop
|
||||
* @returns {Promise<App>} - The stopped application
|
||||
* @throws {Error} - If the app cannot be found or if stopping the app failed
|
||||
*/
|
||||
public stopApp = async (id: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
const app = apps[0];
|
||||
const app = await this.queries.getApp(id);
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
|
@ -280,31 +248,29 @@ export class AppServiceClass {
|
|||
generateEnvFile(app);
|
||||
|
||||
// Run script
|
||||
await this.db.update(appTable).set({ status: 'stopping' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'stopping' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['stop', id]);
|
||||
|
||||
if (success) {
|
||||
await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
} else {
|
||||
await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'running' });
|
||||
throw new Error(`App ${id} failed to stop\nstdout: ${stdout}`);
|
||||
}
|
||||
|
||||
const updatedApps = await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id)).returning();
|
||||
return updatedApps[0];
|
||||
const updatedApp = await this.queries.getApp(id);
|
||||
return updatedApp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uninstalls an app by stopping it, running the app's `uninstall` script, and removing its data
|
||||
*
|
||||
* @param {string} id - The id of the app to uninstall
|
||||
* @returns {Promise<{id: string, status: string, config: object}>} - An object containing the id of the uninstalled app, the status of the app ('missing'), and the config object
|
||||
* @throws {Error} - If the app is not found or if the app's `uninstall` script fails
|
||||
*/
|
||||
public uninstallApp = async (id: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
const app = apps[0];
|
||||
const app = await this.queries.getApp(id);
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
|
@ -316,16 +282,16 @@ export class AppServiceClass {
|
|||
ensureAppFolder(id);
|
||||
generateEnvFile(app);
|
||||
|
||||
await this.db.update(appTable).set({ status: 'uninstalling' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'uninstalling' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['uninstall', id]);
|
||||
|
||||
if (!success) {
|
||||
await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
throw new Error(`App ${id} failed to uninstall\nstdout: ${stdout}`);
|
||||
}
|
||||
|
||||
await this.db.delete(appTable).where(eq(appTable.id, id));
|
||||
await this.queries.deleteApp(id);
|
||||
|
||||
return { id, status: 'missing', config: {} };
|
||||
};
|
||||
|
@ -334,11 +300,9 @@ export class AppServiceClass {
|
|||
* Returns the app with the provided id. If the app is not found, it returns a default app object
|
||||
*
|
||||
* @param {string} id - The id of the app to retrieve
|
||||
* @returns {Promise<App>} - The app object
|
||||
*/
|
||||
public getApp = async (id: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
let app = apps[0];
|
||||
let app = await this.queries.getApp(id);
|
||||
const info = getAppInfo(id, app?.status);
|
||||
const updateInfo = getUpdateInfo(id);
|
||||
|
||||
|
@ -357,12 +321,10 @@ export class AppServiceClass {
|
|||
* Updates an app with the specified ID
|
||||
*
|
||||
* @param {string} id - ID of the app to update
|
||||
* @returns {Promise<App>} - An object representing the updated app
|
||||
* @throws {Error} - If the app is not found or if the update process fails.
|
||||
*/
|
||||
public updateApp = async (id: string) => {
|
||||
const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
||||
const app = apps[0];
|
||||
const app = await this.queries.getApp(id);
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
|
@ -371,30 +333,28 @@ export class AppServiceClass {
|
|||
ensureAppFolder(id);
|
||||
generateEnvFile(app);
|
||||
|
||||
await this.db.update(appTable).set({ status: 'updating' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'updating' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['update', id]);
|
||||
|
||||
if (success) {
|
||||
const appInfo = getAppInfo(app.id, app.status);
|
||||
|
||||
await this.db.update(appTable).set({ status: 'running', version: appInfo?.tipi_version }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'running', version: appInfo?.tipi_version });
|
||||
} else {
|
||||
await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
||||
}
|
||||
|
||||
const updatedApps = await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id)).returning();
|
||||
return updatedApps[0];
|
||||
const updatedApp = await this.queries.updateApp(id, { status: 'stopped' });
|
||||
return updatedApp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of all installed apps
|
||||
*
|
||||
* @returns {Promise<App[]>} - An array of app objects
|
||||
*/
|
||||
public installedApps = async () => {
|
||||
const apps = await this.db.select().from(appTable).orderBy(asc(appTable.id));
|
||||
const apps = await this.queries.getApps();
|
||||
|
||||
return apps
|
||||
.map((app) => {
|
||||
|
|
|
@ -3,9 +3,8 @@ import jwt from 'jsonwebtoken';
|
|||
import validator from 'validator';
|
||||
import { TotpAuthenticator } from '@/server/utils/totp';
|
||||
import { generateSessionId } from '@/server/common/get-server-auth-session';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { userTable } from '@/server/db/schema';
|
||||
import { AuthQueries } from '@/server/queries/auth/auth.queries';
|
||||
import { getConfig } from '../../core/TipiConfig';
|
||||
import TipiCache from '../../core/TipiCache';
|
||||
import { fileExists, unlinkFile } from '../../common/fs.helpers';
|
||||
|
@ -21,10 +20,10 @@ type TokenResponse = {
|
|||
};
|
||||
|
||||
export class AuthServiceClass {
|
||||
private db;
|
||||
private queries;
|
||||
|
||||
constructor(p: NodePgDatabase) {
|
||||
this.db = p;
|
||||
this.queries = new AuthQueries(p);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,9 +34,7 @@ export class AuthServiceClass {
|
|||
*/
|
||||
public login = async (input: UsernamePasswordInput) => {
|
||||
const { password, username } = input;
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.username, username.trim().toLowerCase()));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -80,11 +77,7 @@ export class AuthServiceClass {
|
|||
throw new Error('TOTP session not found');
|
||||
}
|
||||
|
||||
const users = await this.db
|
||||
.select()
|
||||
.from(userTable)
|
||||
.where(eq(userTable.id, Number(userId)));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserById(Number(userId));
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -124,8 +117,7 @@ export class AuthServiceClass {
|
|||
|
||||
const { userId, password } = params;
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.id, userId));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -149,7 +141,7 @@ export class AuthServiceClass {
|
|||
|
||||
const encryptedTotpSecret = encrypt(newTotpSecret, salt);
|
||||
|
||||
await this.db.update(userTable).set({ totpSecret: encryptedTotpSecret, salt }).where(eq(userTable.id, userId));
|
||||
await this.queries.updateUser(userId, { totpSecret: encryptedTotpSecret, salt });
|
||||
|
||||
const uri = TotpAuthenticator.keyuri(user.username, 'Runtipi', newTotpSecret);
|
||||
|
||||
|
@ -162,8 +154,7 @@ export class AuthServiceClass {
|
|||
}
|
||||
|
||||
const { userId, totpCode } = params;
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.id, userId));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -180,7 +171,7 @@ export class AuthServiceClass {
|
|||
throw new Error('Invalid TOTP code');
|
||||
}
|
||||
|
||||
await this.db.update(userTable).set({ totpEnabled: true }).where(eq(userTable.id, userId));
|
||||
await this.queries.updateUser(userId, { totpEnabled: true });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -188,8 +179,7 @@ export class AuthServiceClass {
|
|||
public disableTotp = async (params: { userId: number; password: string }) => {
|
||||
const { userId, password } = params;
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.id, userId));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -204,7 +194,7 @@ export class AuthServiceClass {
|
|||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
await this.db.update(userTable).set({ totpEnabled: false, totpSecret: null }).where(eq(userTable.id, userId));
|
||||
await this.queries.updateUser(userId, { totpEnabled: false, totpSecret: null });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -217,9 +207,9 @@ export class AuthServiceClass {
|
|||
* @throws {Error} - If the email or password is missing, the email is invalid or the user already exists
|
||||
*/
|
||||
public register = async (input: UsernamePasswordInput) => {
|
||||
const operator = await this.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
const operators = await this.queries.getOperators();
|
||||
|
||||
if (operator.length > 0) {
|
||||
if (operators.length > 0) {
|
||||
throw new Error('There is already an admin user. Please login to create a new user from the admin panel.');
|
||||
}
|
||||
|
||||
|
@ -234,8 +224,7 @@ export class AuthServiceClass {
|
|||
throw new Error('Invalid username');
|
||||
}
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.username, email));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserByUsername(email);
|
||||
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
|
@ -243,8 +232,7 @@ export class AuthServiceClass {
|
|||
|
||||
const hash = await argon2.hash(password);
|
||||
|
||||
const newUsers = await this.db.insert(userTable).values({ username: email, password: hash, operator: true }).returning();
|
||||
const newUser = newUsers[0];
|
||||
const newUser = await this.queries.createUser({ username: email, password: hash, operator: true });
|
||||
|
||||
if (!newUser) {
|
||||
throw new Error('Error creating user');
|
||||
|
@ -267,8 +255,7 @@ export class AuthServiceClass {
|
|||
public me = async (userId: number | undefined) => {
|
||||
if (!userId) return null;
|
||||
|
||||
const users = await this.db.select({ id: userTable.id, username: userTable.username, totpEnabled: userTable.totpEnabled }).from(userTable).where(eq(userTable.id, userId));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserDtoById(userId);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
|
@ -317,7 +304,7 @@ export class AuthServiceClass {
|
|||
* @returns {Promise<boolean>} - A boolean indicating if the system is configured or not
|
||||
*/
|
||||
public isConfigured = async (): Promise<boolean> => {
|
||||
const operators = await this.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
const operators = await this.queries.getOperators();
|
||||
|
||||
return operators.length > 0;
|
||||
};
|
||||
|
@ -337,15 +324,15 @@ export class AuthServiceClass {
|
|||
|
||||
const { newPassword } = params;
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.operator, true));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getFirstOperator();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Operator user not found');
|
||||
}
|
||||
|
||||
const hash = await argon2.hash(newPassword);
|
||||
await this.db.update(userTable).set({ password: hash, totpEnabled: false, totpSecret: null }).where(eq(userTable.id, user.id));
|
||||
|
||||
await this.queries.updateUser(user.id, { password: hash, totpEnabled: false, totpSecret: null });
|
||||
|
||||
await unlinkFile(`/runtipi/state/password-change-request`);
|
||||
|
||||
|
@ -388,8 +375,7 @@ export class AuthServiceClass {
|
|||
|
||||
const { currentPassword, newPassword, userId } = params;
|
||||
|
||||
const users = await this.db.select().from(userTable).where(eq(userTable.id, userId));
|
||||
const user = users[0];
|
||||
const user = await this.queries.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
@ -406,7 +392,7 @@ export class AuthServiceClass {
|
|||
}
|
||||
|
||||
const hash = await argon2.hash(newPassword);
|
||||
await this.db.update(userTable).set({ password: hash }).where(eq(userTable.id, user.id));
|
||||
await this.queries.updateUser(user.id, { password: hash });
|
||||
|
||||
await TipiCache.delByValue(userId.toString(), 'auth');
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue