mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-15 16:05:49 +01:00
refactor collections
This commit is contained in:
@@ -4,14 +4,12 @@ import { plainToClass } from 'class-transformer';
|
|||||||
import Crypto from 'crypto';
|
import Crypto from 'crypto';
|
||||||
import {
|
import {
|
||||||
AsyncFailable,
|
AsyncFailable,
|
||||||
Fail,
|
Fail, HasSuccess
|
||||||
HasFailed,
|
|
||||||
HasSuccess
|
|
||||||
} from 'picsur-shared/dist/types';
|
} from 'picsur-shared/dist/types';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { SupportedMime } from '../../models/dto/mimes.dto';
|
import { SupportedMime } from '../../models/dto/mimes.dto';
|
||||||
import { EImageBackend } from '../../models/entities/image.entity';
|
import { EImageBackend } from '../../models/entities/image.entity';
|
||||||
import { GetCols } from '../collectionutils';
|
import { GetCols } from '../../models/util/collection';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageDBService {
|
export class ImageDBService {
|
||||||
@@ -46,7 +44,9 @@ export class ImageDBService {
|
|||||||
public async findOne<B extends true | undefined = undefined>(
|
public async findOne<B extends true | undefined = undefined>(
|
||||||
hash: string,
|
hash: string,
|
||||||
getPrivate?: B,
|
getPrivate?: B,
|
||||||
): AsyncFailable<B extends undefined ? EImageBackend : Required<EImageBackend>> {
|
): AsyncFailable<
|
||||||
|
B extends undefined ? EImageBackend : Required<EImageBackend>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const found = await this.imageRepository.findOne({
|
const found = await this.imageRepository.findOne({
|
||||||
where: { hash },
|
where: { hash },
|
||||||
@@ -54,21 +54,27 @@ export class ImageDBService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!found) return Fail('Image not found');
|
if (!found) return Fail('Image not found');
|
||||||
return found as B extends undefined ? EImageBackend : Required<EImageBackend>;
|
return found as B extends undefined
|
||||||
|
? EImageBackend
|
||||||
|
: Required<EImageBackend>;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findMany(
|
public async findMany(
|
||||||
startId: number,
|
count: number,
|
||||||
limit: number,
|
page: number,
|
||||||
): AsyncFailable<EImageBackend[]> {
|
): AsyncFailable<EImageBackend[]> {
|
||||||
|
if (count < 1 || page < 0) return Fail('Invalid page');
|
||||||
|
if (count > 100) return Fail('Too many results');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const found = await this.imageRepository.find({
|
const found = await this.imageRepository.find({
|
||||||
where: { id: { gte: startId } },
|
skip: count * page,
|
||||||
take: limit,
|
take: count,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (found === undefined) return Fail('Images not found');
|
if (found === undefined) return Fail('Images not found');
|
||||||
return found;
|
return found;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -77,18 +83,19 @@ export class ImageDBService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async delete(hash: string): AsyncFailable<true> {
|
public async delete(hash: string): AsyncFailable<true> {
|
||||||
const image = await this.findOne(hash);
|
|
||||||
if (HasFailed(image)) return image;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.imageRepository.delete(image);
|
const result = await this.imageRepository.delete({ hash });
|
||||||
|
if (result.affected === 0) return Fail('Image not found');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAll(): AsyncFailable<true> {
|
public async deleteAll(IAmSure: boolean): AsyncFailable<true> {
|
||||||
|
if (!IAmSure)
|
||||||
|
return Fail('You must confirm that you want to delete all images');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.imageRepository.delete({});
|
await this.imageRepository.delete({});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export class RolesModule implements OnModuleInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
|
// Nuking roles in dev environment makes testing easier
|
||||||
|
// This ensures that the roles are always started with their default permissions
|
||||||
if (!this.hostConfig.isProduction()) {
|
if (!this.hostConfig.isProduction()) {
|
||||||
await this.nukeRoles();
|
await this.nukeRoles();
|
||||||
}
|
}
|
||||||
@@ -38,6 +40,7 @@ export class RolesModule implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ensureSystemRolesExist() {
|
private async ensureSystemRolesExist() {
|
||||||
|
// The UndeletableRolesList is also the list of systemroles
|
||||||
for (const systemRole of UndeletableRolesList) {
|
for (const systemRole of UndeletableRolesList) {
|
||||||
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
|
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
|
||||||
|
|
||||||
@@ -58,6 +61,9 @@ export class RolesModule implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateImmutableRoles() {
|
private async updateImmutableRoles() {
|
||||||
|
// Immutable roles can not be updated via the gui
|
||||||
|
// They therefore do have to be kept up to date from the backend
|
||||||
|
|
||||||
for (const immutableRole of ImmutableRolesList) {
|
for (const immutableRole of ImmutableRolesList) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updating permissions for immutable role "${immutableRole}"`,
|
`Updating permissions for immutable role "${immutableRole}"`,
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import {
|
|||||||
HasFailed,
|
HasFailed,
|
||||||
HasSuccess
|
HasSuccess
|
||||||
} from 'picsur-shared/dist/types';
|
} from 'picsur-shared/dist/types';
|
||||||
|
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||||
import { In, Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
import { Permissions } from '../../models/dto/permissions.dto';
|
import { Permissions } from '../../models/dto/permissions.dto';
|
||||||
import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto';
|
import {
|
||||||
|
ImmutableRolesList,
|
||||||
|
UndeletableRolesList
|
||||||
|
} from '../../models/dto/roles.dto';
|
||||||
import { ERoleBackend } from '../../models/entities/role.entity';
|
import { ERoleBackend } from '../../models/entities/role.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -33,12 +37,10 @@ export class RolesService {
|
|||||||
role.permissions = permissions;
|
role.permissions = permissions;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
role = await this.rolesRepository.save(role, { reload: true });
|
return await this.rolesRepository.save(role, { reload: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plainToClass(ERoleBackend, role);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(
|
public async delete(
|
||||||
@@ -69,7 +71,7 @@ export class RolesService {
|
|||||||
permissions.push(...foundRole.permissions);
|
permissions.push(...foundRole.permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...new Set(...[permissions])];
|
return makeUnique(permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addPermissions(
|
public async addPermissions(
|
||||||
@@ -79,10 +81,10 @@ export class RolesService {
|
|||||||
const roleToModify = await this.resolve(role);
|
const roleToModify = await this.resolve(role);
|
||||||
if (HasFailed(roleToModify)) return roleToModify;
|
if (HasFailed(roleToModify)) return roleToModify;
|
||||||
|
|
||||||
// This is stupid
|
const newPermissions = makeUnique([
|
||||||
const newPermissions = [
|
...roleToModify.permissions,
|
||||||
...new Set([...roleToModify.permissions, ...permissions]),
|
...permissions,
|
||||||
];
|
]);
|
||||||
|
|
||||||
return this.setPermissions(roleToModify, newPermissions);
|
return this.setPermissions(roleToModify, newPermissions);
|
||||||
}
|
}
|
||||||
@@ -101,9 +103,11 @@ export class RolesService {
|
|||||||
return this.setPermissions(roleToModify, newPermissions);
|
return this.setPermissions(roleToModify, newPermissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permission specific validation is done here
|
||||||
public async setPermissions(
|
public async setPermissions(
|
||||||
role: string | ERoleBackend,
|
role: string | ERoleBackend,
|
||||||
permissions: Permissions,
|
permissions: Permissions,
|
||||||
|
// Extra bypass for internal use
|
||||||
allowImmutable: boolean = false,
|
allowImmutable: boolean = false,
|
||||||
): AsyncFailable<ERoleBackend> {
|
): AsyncFailable<ERoleBackend> {
|
||||||
const roleToModify = await this.resolve(role);
|
const roleToModify = await this.resolve(role);
|
||||||
@@ -113,7 +117,7 @@ export class RolesService {
|
|||||||
return Fail('Cannot modify immutable role');
|
return Fail('Cannot modify immutable role');
|
||||||
}
|
}
|
||||||
|
|
||||||
roleToModify.permissions = [...new Set(permissions)];
|
roleToModify.permissions = makeUnique(permissions);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.rolesRepository.save(roleToModify);
|
return await this.rolesRepository.save(roleToModify);
|
||||||
@@ -129,7 +133,7 @@ export class RolesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!found) return Fail('Role not found');
|
if (!found) return Fail('Role not found');
|
||||||
return found as ERoleBackend;
|
return found;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
@@ -139,7 +143,7 @@ export class RolesService {
|
|||||||
try {
|
try {
|
||||||
const found = await this.rolesRepository.find();
|
const found = await this.rolesRepository.find();
|
||||||
if (!found) return Fail('No roles found');
|
if (!found) return Fail('No roles found');
|
||||||
return found as ERoleBackend[];
|
return found;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
@@ -149,8 +153,10 @@ export class RolesService {
|
|||||||
return HasSuccess(await this.findOne(username));
|
return HasSuccess(await this.findOne(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async nukeSystemRoles(iamsure: boolean = false): AsyncFailable<true> {
|
public async nukeSystemRoles(IAmSure: boolean = false): AsyncFailable<true> {
|
||||||
if (!iamsure) return Fail('Nuke aborted');
|
if (!IAmSure)
|
||||||
|
return Fail('You must confirm that you want to delete all roles');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.rolesRepository.delete({
|
await this.rolesRepository.delete({
|
||||||
name: In(UndeletableRolesList),
|
name: In(UndeletableRolesList),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { plainToClass } from 'class-transformer';
|
|
||||||
import {
|
import {
|
||||||
InternalSysprefRepresentation,
|
InternalSysprefRepresentation,
|
||||||
SysPreference,
|
SysPreference,
|
||||||
SysPrefValueType
|
SysPrefValueType,
|
||||||
|
SysPrefValueTypeStrings
|
||||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||||
import {
|
import {
|
||||||
AsyncFailable,
|
AsyncFailable,
|
||||||
@@ -41,6 +41,7 @@ export class SysPreferenceService {
|
|||||||
|
|
||||||
// Set
|
// Set
|
||||||
try {
|
try {
|
||||||
|
// Upsert here, because we want to create a new record if it does not exist
|
||||||
await this.sysPreferenceRepository.upsert(sysPreference, {
|
await this.sysPreferenceRepository.upsert(sysPreference, {
|
||||||
conflictPaths: ['key'],
|
conflictPaths: ['key'],
|
||||||
});
|
});
|
||||||
@@ -70,7 +71,7 @@ export class SysPreferenceService {
|
|||||||
try {
|
try {
|
||||||
foundSysPreference = await this.sysPreferenceRepository.findOne(
|
foundSysPreference = await this.sysPreferenceRepository.findOne(
|
||||||
{ key: validatedKey },
|
{ key: validatedKey },
|
||||||
{ cache: 60000 },
|
{ cache: 60000 }, // Enable cache for 1 minute
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.warn(e);
|
this.logger.warn(e);
|
||||||
@@ -80,16 +81,13 @@ export class SysPreferenceService {
|
|||||||
// Fallback
|
// Fallback
|
||||||
if (!foundSysPreference) {
|
if (!foundSysPreference) {
|
||||||
return this.saveDefault(validatedKey);
|
return this.saveDefault(validatedKey);
|
||||||
} else {
|
}
|
||||||
foundSysPreference = plainToClass(
|
|
||||||
ESysPreferenceBackend,
|
// Validate
|
||||||
foundSysPreference,
|
const errors = await strictValidate(foundSysPreference);
|
||||||
);
|
if (errors.length > 0) {
|
||||||
const errors = await strictValidate(foundSysPreference);
|
this.logger.warn(errors);
|
||||||
if (errors.length > 0) {
|
return Fail('Invalid preference');
|
||||||
this.logger.warn(errors);
|
|
||||||
return Fail('Invalid preference');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return
|
// Return
|
||||||
@@ -97,32 +95,32 @@ export class SysPreferenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getStringPreference(key: string): AsyncFailable<string> {
|
public async getStringPreference(key: string): AsyncFailable<string> {
|
||||||
const pref = await this.getPreference(key);
|
return this.getPreferencePinned(key, 'string') as AsyncFailable<string>;
|
||||||
if (HasFailed(pref)) return pref;
|
|
||||||
if (pref.type !== 'string') return Fail('Invalid preference type');
|
|
||||||
|
|
||||||
return pref.value as string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNumberPreference(key: string): AsyncFailable<number> {
|
public async getNumberPreference(key: string): AsyncFailable<number> {
|
||||||
const pref = await this.getPreference(key);
|
return this.getPreferencePinned(key, 'number') as AsyncFailable<number>;
|
||||||
if (HasFailed(pref)) return pref;
|
|
||||||
if (pref.type !== 'number') return Fail('Invalid preference type');
|
|
||||||
|
|
||||||
return pref.value as number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBooleanPreference(key: string): AsyncFailable<boolean> {
|
public async getBooleanPreference(key: string): AsyncFailable<boolean> {
|
||||||
const pref = await this.getPreference(key);
|
return this.getPreferencePinned(key, 'boolean') as AsyncFailable<boolean>;
|
||||||
if (HasFailed(pref)) return pref;
|
}
|
||||||
if (pref.type !== 'boolean') return Fail('Invalid preference type');
|
|
||||||
|
|
||||||
return pref.value as boolean;
|
private async getPreferencePinned(
|
||||||
|
key: string,
|
||||||
|
type: SysPrefValueTypeStrings,
|
||||||
|
): AsyncFailable<SysPrefValueType> {
|
||||||
|
let pref = await this.getPreference(key);
|
||||||
|
if (HasFailed(pref)) return pref;
|
||||||
|
if (pref.type !== type) return Fail('Invalid preference type');
|
||||||
|
|
||||||
|
return pref.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllPreferences(): AsyncFailable<
|
public async getAllPreferences(): AsyncFailable<
|
||||||
InternalSysprefRepresentation[]
|
InternalSysprefRepresentation[]
|
||||||
> {
|
> {
|
||||||
|
// TODO: We are fetching each value invidually, we should fetch all at once
|
||||||
let internalSysPrefs = await Promise.all(
|
let internalSysPrefs = await Promise.all(
|
||||||
SysPreferenceList.map((key) => this.getPreference(key)),
|
SysPreferenceList.map((key) => this.getPreference(key)),
|
||||||
);
|
);
|
||||||
@@ -132,6 +130,7 @@ export class SysPreferenceService {
|
|||||||
|
|
||||||
return internalSysPrefs as InternalSysprefRepresentation[];
|
return internalSysPrefs as InternalSysprefRepresentation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
private async saveDefault(
|
private async saveDefault(
|
||||||
@@ -140,6 +139,7 @@ export class SysPreferenceService {
|
|||||||
return this.setPreference(key, this.defaultsService.defaults[key]());
|
return this.setPreference(key, this.defaultsService.defaults[key]());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This converts the raw string representation of the value to the correct type
|
||||||
private retrieveConvertedValue(
|
private retrieveConvertedValue(
|
||||||
preference: ESysPreferenceBackend,
|
preference: ESysPreferenceBackend,
|
||||||
): Failable<InternalSysprefRepresentation> {
|
): Failable<InternalSysprefRepresentation> {
|
||||||
@@ -185,7 +185,7 @@ export class SysPreferenceService {
|
|||||||
verifySysPreference.key = validatedKey;
|
verifySysPreference.key = validatedKey;
|
||||||
verifySysPreference.value = validatedValue;
|
verifySysPreference.value = validatedValue;
|
||||||
|
|
||||||
// Just to be sure
|
// It should already be valid, but these two validators might go out of sync
|
||||||
const errors = await strictValidate(verifySysPreference);
|
const errors = await strictValidate(verifySysPreference);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
this.logger.warn(errors);
|
this.logger.warn(errors);
|
||||||
@@ -196,14 +196,13 @@ export class SysPreferenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private validatePrefKey(key: string): Failable<SysPreference> {
|
private validatePrefKey(key: string): Failable<SysPreference> {
|
||||||
if (!SysPreferenceList.includes(key)) {
|
if (!SysPreferenceList.includes(key)) return Fail('Invalid preference key');
|
||||||
return Fail('Invalid preference key');
|
|
||||||
}
|
|
||||||
|
|
||||||
return key as SysPreference;
|
return key as SysPreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
private validatePrefValue(
|
private validatePrefValue(
|
||||||
|
// Key is required, because the type of the value depends on the key
|
||||||
key: SysPreference,
|
key: SysPreference,
|
||||||
value: SysPrefValueType,
|
value: SysPrefValueType,
|
||||||
): Failable<string> {
|
): Failable<string> {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import {
|
|||||||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||||
import { EnvJwtConfigService } from '../../config/jwt.config.service';
|
import { EnvJwtConfigService } from '../../config/jwt.config.service';
|
||||||
|
|
||||||
|
// This specific service is used to store default values for system preferences
|
||||||
|
// It needs to be in a service because the values depend on the environment
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SysPreferenceDefaultsService {
|
export class SysPreferenceDefaultsService {
|
||||||
private readonly logger = new Logger('SysPreferenceDefaultsService');
|
private readonly logger = new Logger('SysPreferenceDefaultsService');
|
||||||
@@ -26,8 +29,8 @@ export class SysPreferenceDefaultsService {
|
|||||||
return generateRandomString(64);
|
return generateRandomString(64);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[SysPreference.JwtExpiresIn]: () => this.jwtConfigService.getJwtExpiresIn() ?? '7d',
|
[SysPreference.JwtExpiresIn]: () =>
|
||||||
|
this.jwtConfigService.getJwtExpiresIn() ?? '7d',
|
||||||
[SysPreference.TestString]: () => 'test_string',
|
[SysPreference.TestString]: () => 'test_string',
|
||||||
[SysPreference.TestNumber]: () => 123,
|
[SysPreference.TestNumber]: () => 123,
|
||||||
[SysPreference.TestBoolean]: () => true,
|
[SysPreference.TestBoolean]: () => true,
|
||||||
|
|||||||
@@ -28,36 +28,27 @@ export class UsersModule implements OnModuleInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.ensureGuestExists();
|
await this.ensureUserExists(
|
||||||
await this.ensureAdminExists();
|
'guest',
|
||||||
}
|
// Guest should never be able to login
|
||||||
|
// It should be prevented even if you know the password
|
||||||
private async ensureGuestExists() {
|
// But to be sure, we set it to a random string
|
||||||
const username = 'guest';
|
generateRandomString(128),
|
||||||
const password = generateRandomString(128);
|
|
||||||
this.logger.debug(`Ensuring guest user exists`);
|
|
||||||
|
|
||||||
const exists = await this.usersService.exists(username);
|
|
||||||
if (exists) return;
|
|
||||||
|
|
||||||
const newUser = await this.usersService.create(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
['guest'],
|
['guest'],
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
if (HasFailed(newUser)) {
|
await this.ensureUserExists(
|
||||||
this.logger.error(
|
'admin',
|
||||||
`Failed to create guest user because: ${newUser.getReason()}`,
|
this.authConfigService.getDefaultAdminPassword(),
|
||||||
);
|
['user', 'admin'],
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureAdminExists() {
|
private async ensureUserExists(
|
||||||
const username = 'admin';
|
username: string,
|
||||||
const password = this.authConfigService.getDefaultAdminPassword();
|
password: string,
|
||||||
this.logger.debug(`Ensuring admin user exists`);
|
roles: string[],
|
||||||
|
) {
|
||||||
|
this.logger.debug(`Ensuring user "${username}" exists`);
|
||||||
|
|
||||||
const exists = await this.usersService.exists(username);
|
const exists = await this.usersService.exists(username);
|
||||||
if (exists) return;
|
if (exists) return;
|
||||||
@@ -65,12 +56,12 @@ export class UsersModule implements OnModuleInit {
|
|||||||
const newUser = await this.usersService.create(
|
const newUser = await this.usersService.create(
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
['user', 'admin'],
|
roles,
|
||||||
true,
|
false,
|
||||||
);
|
);
|
||||||
if (HasFailed(newUser)) {
|
if (HasFailed(newUser)) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to create admin user because: ${newUser.getReason()}`,
|
`Failed to create user "${username}" because: ${newUser.getReason()}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,22 @@ import {
|
|||||||
HasFailed,
|
HasFailed,
|
||||||
HasSuccess
|
HasSuccess
|
||||||
} from 'picsur-shared/dist/types';
|
} from 'picsur-shared/dist/types';
|
||||||
|
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
DefaultRolesList,
|
DefaultRolesList,
|
||||||
SoulBoundRolesList
|
SoulBoundRolesList
|
||||||
} from '../../models/dto/roles.dto';
|
} from '../../models/dto/roles.dto';
|
||||||
import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../models/dto/specialusers.dto';
|
import {
|
||||||
|
ImmutableUsersList,
|
||||||
|
LockedLoginUsersList,
|
||||||
|
UndeletableUsersList
|
||||||
|
} from '../../models/dto/specialusers.dto';
|
||||||
import { EUserBackend } from '../../models/entities/user.entity';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
import { GetCols } from '../collectionutils';
|
import { GetCols } from '../../models/util/collection';
|
||||||
import { RolesService } from '../roledb/roledb.service';
|
|
||||||
|
|
||||||
|
// TODO: make this a configurable value
|
||||||
const BCryptStrength = 12;
|
const BCryptStrength = 12;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -28,7 +33,6 @@ export class UsersService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(EUserBackend)
|
@InjectRepository(EUserBackend)
|
||||||
private usersRepository: Repository<EUserBackend>,
|
private usersRepository: Repository<EUserBackend>,
|
||||||
private rolesService: RolesService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Creation and deletion
|
// Creation and deletion
|
||||||
@@ -37,6 +41,7 @@ export class UsersService {
|
|||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
roles?: string[],
|
roles?: string[],
|
||||||
|
// Add option to create "invalid" users, should only be used by system
|
||||||
byPassRoleCheck?: boolean,
|
byPassRoleCheck?: boolean,
|
||||||
): AsyncFailable<EUserBackend> {
|
): AsyncFailable<EUserBackend> {
|
||||||
if (await this.exists(username)) return Fail('User already exists');
|
if (await this.exists(username)) return Fail('User already exists');
|
||||||
@@ -48,10 +53,11 @@ export class UsersService {
|
|||||||
user.password = hashedPassword;
|
user.password = hashedPassword;
|
||||||
if (byPassRoleCheck) {
|
if (byPassRoleCheck) {
|
||||||
const rolesToAdd = roles ?? [];
|
const rolesToAdd = roles ?? [];
|
||||||
user.roles = [...new Set([...rolesToAdd])];
|
user.roles = makeUnique(rolesToAdd);
|
||||||
} else {
|
} else {
|
||||||
|
// Strip soulbound roles and add default roles
|
||||||
const rolesToAdd = this.filterAddedRoles(roles ?? []);
|
const rolesToAdd = this.filterAddedRoles(roles ?? []);
|
||||||
user.roles = [...new Set([...DefaultRolesList, ...rolesToAdd])];
|
user.roles = makeUnique([...DefaultRolesList, ...rolesToAdd]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -60,10 +66,10 @@ export class UsersService {
|
|||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plainToClass(EUserBackend, user); // Strips unwanted data
|
// Strips unwanted data
|
||||||
|
return plainToClass(EUserBackend, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns user object without id
|
|
||||||
public async delete(
|
public async delete(
|
||||||
user: string | EUserBackend,
|
user: string | EUserBackend,
|
||||||
): AsyncFailable<EUserBackend> {
|
): AsyncFailable<EUserBackend> {
|
||||||
@@ -92,6 +98,7 @@ export class UsersService {
|
|||||||
|
|
||||||
if (ImmutableUsersList.includes(userToModify.username)) {
|
if (ImmutableUsersList.includes(userToModify.username)) {
|
||||||
// Just fail silently
|
// Just fail silently
|
||||||
|
this.logger.log("Can't modify system user");
|
||||||
return userToModify;
|
return userToModify;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +106,7 @@ export class UsersService {
|
|||||||
SoulBoundRolesList.includes(role),
|
SoulBoundRolesList.includes(role),
|
||||||
);
|
);
|
||||||
const rolesToAdd = this.filterAddedRoles(roles);
|
const rolesToAdd = this.filterAddedRoles(roles);
|
||||||
|
const newRoles = makeUnique([...rolesToKeep, ...rolesToAdd]);
|
||||||
const newRoles = [...new Set([...rolesToKeep, ...rolesToAdd])];
|
|
||||||
|
|
||||||
userToModify.roles = newRoles;
|
userToModify.roles = newRoles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -115,19 +120,20 @@ export class UsersService {
|
|||||||
user: string | EUserBackend,
|
user: string | EUserBackend,
|
||||||
password: string,
|
password: string,
|
||||||
): AsyncFailable<EUserBackend> {
|
): AsyncFailable<EUserBackend> {
|
||||||
const userToModify = await this.resolve(user);
|
let userToModify = await this.resolve(user);
|
||||||
if (HasFailed(userToModify)) return userToModify;
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
||||||
|
|
||||||
userToModify.password = hashedPassword;
|
userToModify.password = hashedPassword;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fullUser = await this.usersRepository.save(userToModify);
|
userToModify = await this.usersRepository.save(userToModify);
|
||||||
return plainToClass(EUserBackend, fullUser);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return Fail(e?.message);
|
return Fail(e?.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strips unwanted data
|
||||||
|
return plainToClass(EUserBackend, userToModify);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
@@ -140,7 +146,8 @@ export class UsersService {
|
|||||||
if (HasFailed(user)) return user;
|
if (HasFailed(user)) return user;
|
||||||
|
|
||||||
if (LockedLoginUsersList.includes(user.username)) {
|
if (LockedLoginUsersList.includes(user.username)) {
|
||||||
return Fail('Wrong password');
|
// Error should be kept in backend
|
||||||
|
return Fail('Wrong username');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await bcrypt.compare(password, user.password)))
|
if (!(await bcrypt.compare(password, user.password)))
|
||||||
@@ -153,6 +160,8 @@ export class UsersService {
|
|||||||
|
|
||||||
public async findOne<B extends true | undefined = undefined>(
|
public async findOne<B extends true | undefined = undefined>(
|
||||||
username: string,
|
username: string,
|
||||||
|
// Also fetch fields that aren't normally sent to the client
|
||||||
|
// (e.g. hashed password)
|
||||||
getPrivate?: B,
|
getPrivate?: B,
|
||||||
): AsyncFailable<
|
): AsyncFailable<
|
||||||
B extends undefined ? EUserBackend : Required<EUserBackend>
|
B extends undefined ? EUserBackend : Required<EUserBackend>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||||
import { Permissions } from '../../models/dto/permissions.dto';
|
import { Permissions } from '../../models/dto/permissions.dto';
|
||||||
import { EUserBackend } from '../../models/entities/user.entity';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
import { RolesService } from '../roledb/roledb.service';
|
import { RolesService } from '../roledb/roledb.service';
|
||||||
import { UsersService } from './userdb.service';
|
import { UsersService } from './userdb.service';
|
||||||
|
|
||||||
|
// Move some code here so it doesnt make the userdb service gigantic
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRolesService {
|
export class UserRolesService {
|
||||||
constructor(private usersService: UsersService, private rolesService: RolesService){}
|
constructor(
|
||||||
|
private usersService: UsersService,
|
||||||
|
private rolesService: RolesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
// Permissions and roles
|
// Permissions and roles
|
||||||
|
|
||||||
public async getPermissions(
|
public async getPermissions(
|
||||||
user: string | EUserBackend,
|
user: string | EUserBackend,
|
||||||
): AsyncFailable<Permissions> {
|
): AsyncFailable<Permissions> {
|
||||||
@@ -27,7 +32,7 @@ export class UserRolesService {
|
|||||||
const userToModify = await this.usersService.resolve(user);
|
const userToModify = await this.usersService.resolve(user);
|
||||||
if (HasFailed(userToModify)) return userToModify;
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
const newRoles = [...new Set([...userToModify.roles, ...roles])];
|
const newRoles = makeUnique([...userToModify.roles, ...roles]);
|
||||||
|
|
||||||
return this.usersService.setRoles(userToModify, newRoles);
|
return this.usersService.setRoles(userToModify, newRoles);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||||||
|
|
||||||
const permissions = this.extractPermissions(context);
|
const permissions = this.extractPermissions(context);
|
||||||
if (HasFailed(permissions)) {
|
if (HasFailed(permissions)) {
|
||||||
this.logger.warn('222' + permissions.getReason());
|
this.logger.warn('Route Permissions: ' + permissions.getReason());
|
||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPermissions = await this.userRolesService.getPermissions(user);
|
const userPermissions = await this.userRolesService.getPermissions(user);
|
||||||
if (HasFailed(userPermissions)) {
|
if (HasFailed(userPermissions)) {
|
||||||
this.logger.warn('111' + userPermissions.getReason());
|
this.logger.warn('User Permissions: ' + userPermissions.getReason());
|
||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ export class DemoManagerService {
|
|||||||
|
|
||||||
private async executeAsync() {
|
private async executeAsync() {
|
||||||
this.logger.log('Executing demo cleanup');
|
this.logger.log('Executing demo cleanup');
|
||||||
await this.imagesService.deleteAll();
|
await this.imagesService.deleteAll(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
shared/src/util/unique.ts
Normal file
8
shared/src/util/unique.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function makeUnique<T>(arr: T[]): T[] {
|
||||||
|
return arr.reduce(function (accum, current) {
|
||||||
|
if (accum.indexOf(current) < 0) {
|
||||||
|
accum.push(current);
|
||||||
|
}
|
||||||
|
return accum;
|
||||||
|
}, [] as T[]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user