2024-10-02 10:54:35 -04:00
|
|
|
import { Injectable } from '@nestjs/common';
|
2025-07-18 10:57:29 -04:00
|
|
|
import { isAbsolute } from 'node:path';
|
2024-05-26 18:15:52 -04:00
|
|
|
import { SALT_ROUNDS } from 'src/constants';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
2024-05-26 18:15:52 -04:00
|
|
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { SystemMetadataKey } from 'src/enum';
|
2024-09-30 17:31:21 -04:00
|
|
|
import { BaseService } from 'src/services/base.service';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { createMaintenanceLoginUrl, generateMaintenanceSecret, sendOneShotAppRestart } from 'src/utils/maintenance';
|
|
|
|
|
import { getExternalDomain } from 'src/utils/misc';
|
2024-05-22 16:23:47 -04:00
|
|
|
|
|
|
|
|
@Injectable()
|
2024-09-30 17:31:21 -04:00
|
|
|
export class CliService extends BaseService {
|
2024-05-26 18:15:52 -04:00
|
|
|
async listUsers(): Promise<UserAdminResponseDto[]> {
|
2024-05-22 16:23:47 -04:00
|
|
|
const users = await this.userRepository.getList({ withDeleted: true });
|
2024-05-26 18:15:52 -04:00
|
|
|
return users.map((user) => mapUserAdmin(user));
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
async resetAdminPassword(ask: (admin: UserAdminResponseDto) => Promise<string | undefined>) {
|
2024-05-22 16:23:47 -04:00
|
|
|
const admin = await this.userRepository.getAdmin();
|
|
|
|
|
if (!admin) {
|
|
|
|
|
throw new Error('Admin account does not exist');
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
const providedPassword = await ask(mapUserAdmin(admin));
|
2025-05-15 13:34:33 -05:00
|
|
|
const password = providedPassword || this.cryptoRepository.randomBytesAsText(24);
|
2024-05-26 18:15:52 -04:00
|
|
|
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
|
2024-05-22 16:23:47 -04:00
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
await this.userRepository.update(admin.id, { password: hashedPassword });
|
2024-05-22 16:23:47 -04:00
|
|
|
|
|
|
|
|
return { admin, password, provided: !!providedPassword };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disablePasswordLogin(): Promise<void> {
|
2024-09-30 17:31:21 -04:00
|
|
|
const config = await this.getConfig({ withCache: false });
|
2024-05-22 16:23:47 -04:00
|
|
|
config.passwordLogin.enabled = false;
|
2024-09-30 17:31:21 -04:00
|
|
|
await this.updateConfig(config);
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async enablePasswordLogin(): Promise<void> {
|
2024-09-30 17:31:21 -04:00
|
|
|
const config = await this.getConfig({ withCache: false });
|
2024-05-22 16:23:47 -04:00
|
|
|
config.passwordLogin.enabled = true;
|
2024-09-30 17:31:21 -04:00
|
|
|
await this.updateConfig(config);
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 17:15:44 +00:00
|
|
|
async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> {
|
|
|
|
|
const currentState = await this.systemMetadataRepository
|
|
|
|
|
.get(SystemMetadataKey.MaintenanceMode)
|
|
|
|
|
.then((state) => state ?? { isMaintenanceMode: false as const });
|
|
|
|
|
|
|
|
|
|
if (!currentState.isMaintenanceMode) {
|
|
|
|
|
return {
|
|
|
|
|
alreadyDisabled: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = { isMaintenanceMode: false as const };
|
|
|
|
|
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
|
|
|
|
|
|
|
|
|
|
sendOneShotAppRestart(state);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
alreadyDisabled: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async enableMaintenanceMode(): Promise<{ authUrl: string; alreadyEnabled: boolean }> {
|
|
|
|
|
const { server } = await this.getConfig({ withCache: true });
|
|
|
|
|
const baseUrl = getExternalDomain(server);
|
|
|
|
|
|
|
|
|
|
const payload: MaintenanceAuthDto = {
|
|
|
|
|
username: 'cli-admin',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const state = await this.systemMetadataRepository
|
|
|
|
|
.get(SystemMetadataKey.MaintenanceMode)
|
|
|
|
|
.then((state) => state ?? { isMaintenanceMode: false as const });
|
|
|
|
|
|
|
|
|
|
if (state.isMaintenanceMode) {
|
|
|
|
|
return {
|
|
|
|
|
authUrl: await createMaintenanceLoginUrl(baseUrl, payload, state.secret),
|
|
|
|
|
alreadyEnabled: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const secret = generateMaintenanceSecret();
|
|
|
|
|
|
|
|
|
|
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
|
|
|
|
|
isMaintenanceMode: true,
|
|
|
|
|
secret,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendOneShotAppRestart({
|
|
|
|
|
isMaintenanceMode: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
authUrl: await createMaintenanceLoginUrl(baseUrl, payload, secret),
|
|
|
|
|
alreadyEnabled: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 21:11:13 -05:00
|
|
|
async grantAdminAccess(email: string): Promise<void> {
|
|
|
|
|
const user = await this.userRepository.getByEmail(email);
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new Error('User does not exist');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.userRepository.update(user.id, { isAdmin: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async revokeAdminAccess(email: string): Promise<void> {
|
|
|
|
|
const user = await this.userRepository.getByEmail(email);
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new Error('User does not exist');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.userRepository.update(user.id, { isAdmin: false });
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-22 16:23:47 -04:00
|
|
|
async disableOAuthLogin(): Promise<void> {
|
2024-09-30 17:31:21 -04:00
|
|
|
const config = await this.getConfig({ withCache: false });
|
2024-05-22 16:23:47 -04:00
|
|
|
config.oauth.enabled = false;
|
2024-09-30 17:31:21 -04:00
|
|
|
await this.updateConfig(config);
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async enableOAuthLogin(): Promise<void> {
|
2024-09-30 17:31:21 -04:00
|
|
|
const config = await this.getConfig({ withCache: false });
|
2024-05-22 16:23:47 -04:00
|
|
|
config.oauth.enabled = true;
|
2024-09-30 17:31:21 -04:00
|
|
|
await this.updateConfig(config);
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|
2025-01-13 19:30:34 -06:00
|
|
|
|
2025-07-18 10:57:29 -04:00
|
|
|
async getSampleFilePaths(): Promise<string[]> {
|
|
|
|
|
const [assets, people, users] = await Promise.all([
|
|
|
|
|
this.assetRepository.getFileSamples(),
|
|
|
|
|
this.personRepository.getFileSamples(),
|
|
|
|
|
this.userRepository.getFileSamples(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const paths = [];
|
|
|
|
|
|
|
|
|
|
for (const person of people) {
|
|
|
|
|
paths.push(person.thumbnailPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
|
paths.push(user.profileImagePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const asset of assets) {
|
2025-08-01 00:58:35 +02:00
|
|
|
paths.push(asset.path);
|
2025-07-18 10:57:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return paths.filter(Boolean) as string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async migrateFilePaths({
|
|
|
|
|
oldValue,
|
|
|
|
|
newValue,
|
|
|
|
|
confirm,
|
|
|
|
|
}: {
|
|
|
|
|
oldValue: string;
|
|
|
|
|
newValue: string;
|
|
|
|
|
confirm: (data: { sourceFolder: string; targetFolder: string }) => Promise<boolean>;
|
|
|
|
|
}): Promise<boolean> {
|
|
|
|
|
let sourceFolder = oldValue;
|
|
|
|
|
if (sourceFolder.startsWith('./')) {
|
|
|
|
|
sourceFolder = sourceFolder.slice(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetFolder = newValue;
|
|
|
|
|
if (!isAbsolute(targetFolder)) {
|
|
|
|
|
throw new Error('Target media location must be an absolute path');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(await confirm({ sourceFolder, targetFolder }))) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.databaseRepository.migrateFilePaths(sourceFolder, targetFolder);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-13 19:30:34 -06:00
|
|
|
cleanup() {
|
|
|
|
|
return this.databaseRepository.shutdown();
|
|
|
|
|
}
|
2024-05-22 16:23:47 -04:00
|
|
|
}
|