clean up validation, still not happy

This commit is contained in:
rubikscraft
2022-04-02 23:25:49 +02:00
parent 805ff8ab0e
commit bcd427f5a7
43 changed files with 307 additions and 277 deletions

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import Crypto from 'crypto'; import Crypto from 'crypto';
import { import {
AsyncFailable, AsyncFailable,
@@ -36,8 +35,7 @@ export class ImageDBService {
return Fail(e?.message); return Fail(e?.message);
} }
// Strips unwanted data return imageEntity;
return plainToClass(EImageBackend, imageEntity);
} }
public async findOne<B extends true | undefined = undefined>( public async findOne<B extends true | undefined = undefined>(

View File

@@ -52,10 +52,7 @@ export class RolesService {
} }
try { try {
// Makes sure we can return the id return await this.rolesRepository.remove(roleToModify);
const cloned = plainToClass(ERoleBackend, roleToModify);
await this.rolesRepository.remove(roleToModify);
return cloned;
} catch (e: any) { } catch (e: any) {
return Fail(e?.message); return Fail(e?.message);
} }

View File

@@ -1,10 +1,11 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { PrefValueType, PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto';
import { import {
InternalSysPrefRepresentation, DecodedSysPref,
SysPreference PrefValueType,
} from 'picsur-shared/dist/dto/syspreferences.dto'; PrefValueTypeStrings
} from 'picsur-shared/dist/dto/preferences.dto';
import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto';
import { import {
AsyncFailable, AsyncFailable,
Fail, Fail,
@@ -33,7 +34,7 @@ export class SysPreferenceService {
public async setPreference( public async setPreference(
key: string, key: string,
value: PrefValueType, value: PrefValueType,
): AsyncFailable<InternalSysPrefRepresentation> { ): AsyncFailable<DecodedSysPref> {
// Validate // Validate
let sysPreference = await this.validatePref(key, value); let sysPreference = await this.validatePref(key, value);
if (HasFailed(sysPreference)) return sysPreference; if (HasFailed(sysPreference)) return sysPreference;
@@ -58,9 +59,7 @@ export class SysPreferenceService {
}; };
} }
public async getPreference( public async getPreference(key: string): AsyncFailable<DecodedSysPref> {
key: string,
): AsyncFailable<InternalSysPrefRepresentation> {
// Validate // Validate
let validatedKey = this.validatePrefKey(key); let validatedKey = this.validatePrefKey(key);
if (HasFailed(validatedKey)) return validatedKey; if (HasFailed(validatedKey)) return validatedKey;
@@ -116,9 +115,7 @@ export class SysPreferenceService {
return pref.value; return pref.value;
} }
public async getAllPreferences(): AsyncFailable< public async getAllPreferences(): AsyncFailable<DecodedSysPref[]> {
InternalSysPrefRepresentation[]
> {
// TODO: We are fetching each value invidually, we should fetch all at once // 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)),
@@ -127,21 +124,21 @@ export class SysPreferenceService {
return Fail('Could not get all preferences'); return Fail('Could not get all preferences');
} }
return internalSysPrefs as InternalSysPrefRepresentation[]; return internalSysPrefs as DecodedSysPref[];
} }
// Private // Private
private async saveDefault( private async saveDefault(
key: SysPreference, // Force enum here because we dont validate key: SysPreference, // Force enum here because we dont validate
): AsyncFailable<InternalSysPrefRepresentation> { ): AsyncFailable<DecodedSysPref> {
return this.setPreference(key, this.defaultsService.sysDefaults[key]()); return this.setPreference(key, this.defaultsService.sysDefaults[key]());
} }
// This converts the raw string representation of the value to the correct type // This converts the raw string representation of the value to the correct type
private retrieveConvertedValue( private retrieveConvertedValue(
preference: ESysPreferenceBackend, preference: ESysPreferenceBackend,
): Failable<InternalSysPrefRepresentation> { ): Failable<DecodedSysPref> {
const key = this.validatePrefKey(preference.key); const key = this.validatePrefKey(preference.key);
if (HasFailed(key)) return key; if (HasFailed(key)) return key;

View File

@@ -53,7 +53,7 @@ export class UsersService {
let user = new EUserBackend(); let user = new EUserBackend();
user.username = username; user.username = username;
user.password = hashedPassword; user.hashedPassword = hashedPassword;
if (byPassRoleCheck) { if (byPassRoleCheck) {
const rolesToAdd = roles ?? []; const rolesToAdd = roles ?? [];
user.roles = makeUnique(rolesToAdd); user.roles = makeUnique(rolesToAdd);
@@ -64,13 +64,10 @@ export class UsersService {
} }
try { try {
user = await this.usersRepository.save(user, { reload: true }); return await this.usersRepository.save(user);
} catch (e: any) { } catch (e: any) {
return Fail(e?.message); return Fail(e?.message);
} }
// Strips unwanted data
return plainToClass(EUserBackend, user);
} }
public async delete(uuid: string): AsyncFailable<EUserBackend> { public async delete(uuid: string): AsyncFailable<EUserBackend> {
@@ -153,8 +150,7 @@ export class UsersService {
if (HasFailed(userToModify)) return userToModify; if (HasFailed(userToModify)) return userToModify;
const strength = await this.getBCryptStrength(); const strength = await this.getBCryptStrength();
const hashedPassword = await bcrypt.hash(password, strength); userToModify.hashedPassword = await bcrypt.hash(password, strength);
userToModify.password = hashedPassword;
try { try {
userToModify = await this.usersRepository.save(userToModify); userToModify = await this.usersRepository.save(userToModify);
@@ -180,7 +176,7 @@ export class UsersService {
return Fail('Wrong username'); return Fail('Wrong username');
} }
if (!(await bcrypt.compare(password, user.password))) if (!(await bcrypt.compare(password, user.hashedPassword)))
return Fail('Wrong password'); return Fail('Wrong password');
return await this.findOne(user.id); return await this.findOne(user.id);
@@ -199,7 +195,11 @@ export class UsersService {
try { try {
const found = await this.usersRepository.findOne({ const found = await this.usersRepository.findOne({
where: { username }, where: { username },
select: getPrivate ? GetCols(this.usersRepository) : undefined, ...(getPrivate
? {
select: GetCols(this.usersRepository),
}
: {}),
}); });
if (!found) return Fail('User not found'); if (!found) return Fail('User not found');

View File

@@ -41,6 +41,10 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
const user = await this.validateUser( const user = await this.validateUser(
context.switchToHttp().getRequest().user, context.switchToHttp().getRequest().user,
); );
if (!user.id) {
this.logger.error('User has no id, this should not happen');
throw new InternalServerErrorException();
}
// These are the permissions required to access the route // These are the permissions required to access the route
const permissions = this.extractPermissions(context); const permissions = this.extractPermissions(context);

View File

@@ -1,19 +1,23 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class EImageBackend extends EImage { export class EImageBackend extends EImage {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn('uuid')
override id: string; override id?: string;
@Index() @Index()
@Column({ unique: true, nullable: false }) @Column({ unique: true, nullable: false })
override hash: string; override hash: string;
// Binary data
@Column({ type: 'bytea', nullable: false, select: false })
override data?: Buffer;
@Column({ nullable: false }) @Column({ nullable: false })
override mime: string; override mime: string;
// Binary data
@Column({ type: 'bytea', nullable: false, select: false })
@IsOptional()
@IsNotEmpty()
// @ts-ignore
override data?: Buffer;
} }

View File

@@ -5,7 +5,7 @@ import { Permissions } from '../dto/permissions.dto';
@Entity() @Entity()
export class ERoleBackend extends ERole { export class ERoleBackend extends ERole {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
override id: string; override id?: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })

View File

@@ -1,3 +1,4 @@
import { IsOptional, IsString } from 'class-validator';
import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@@ -6,7 +7,7 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class EUserBackend extends EUser { export class EUserBackend extends EUser {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
override id: string; override id?: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })
@@ -16,5 +17,9 @@ export class EUserBackend extends EUser {
override roles: string[]; override roles: string[];
@Column({ nullable: false, select: false }) @Column({ nullable: false, select: false })
override password?: string; @IsOptional()
@IsString()
// @ts-ignore
override hashedPassword?: string;
} }

View File

@@ -0,0 +1,14 @@
import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { EImageBackend } from '../entities/image.entity';
export function EImageBackend2EImage(
eImage: EImageBackend,
): EImage {
if (eImage.data === undefined)
return eImage as EImage;
return {
...eImage,
data: undefined,
};
}

View File

@@ -0,0 +1,14 @@
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { EUserBackend } from '../entities/user.entity';
export function EUserBackend2EUser(
eUser: EUserBackend,
): EUser {
if (eUser.hashedPassword === undefined)
return eUser as EUser;
return {
...eUser,
hashedPassword: undefined,
};
}

View File

@@ -24,6 +24,7 @@ import {
import { AuthManagerService } from '../../../managers/auth/auth.service'; import { AuthManagerService } from '../../../managers/auth/auth.service';
import { Permission } from '../../../models/dto/permissions.dto'; import { Permission } from '../../../models/dto/permissions.dto';
import AuthFasityRequest from '../../../models/requests/authrequest.dto'; import AuthFasityRequest from '../../../models/requests/authrequest.dto';
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
@Controller('api/user') @Controller('api/user')
export class UserController { export class UserController {
@@ -60,12 +61,14 @@ export class UserController {
throw new InternalServerErrorException('Could not register user'); throw new InternalServerErrorException('Could not register user');
} }
return user; return EUserBackend2EUser(user);
} }
@Get('me') @Get('me')
@RequiredPermissions(Permission.UserKeepLogin) @RequiredPermissions(Permission.UserKeepLogin)
async me(@Request() req: AuthFasityRequest): Promise<UserMeResponse> { async me(@Request() req: AuthFasityRequest): Promise<UserMeResponse> {
if (!req.user.id) throw new InternalServerErrorException('User is corrupt');
const user = await this.usersService.findOne(req.user.id); const user = await this.usersService.findOne(req.user.id);
if (HasFailed(user)) { if (HasFailed(user)) {
@@ -79,7 +82,7 @@ export class UserController {
throw new InternalServerErrorException('Could not get new token'); throw new InternalServerErrorException('Could not get new token');
} }
return { user, token }; return { user: EUserBackend2EUser(user), token };
} }
// You can always check your permissions // You can always check your permissions
@@ -88,6 +91,8 @@ export class UserController {
async refresh( async refresh(
@Request() req: AuthFasityRequest, @Request() req: AuthFasityRequest,
): Promise<UserMePermissionsResponse> { ): Promise<UserMePermissionsResponse> {
if (!req.user.id) throw new InternalServerErrorException('User is corrupt');
const permissions = await this.usersService.getPermissions(req.user.id); const permissions = await this.usersService.getPermissions(req.user.id);
if (HasFailed(permissions)) { if (HasFailed(permissions)) {
this.logger.warn(permissions.getReason()); this.logger.warn(permissions.getReason());

View File

@@ -24,7 +24,12 @@ import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Permission } from '../../../models/dto/permissions.dto'; import { Permission } from '../../../models/dto/permissions.dto';
import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto'; import {
ImmutableUsersList,
LockedLoginUsersList,
UndeletableUsersList
} from '../../../models/dto/specialusers.dto';
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
@Controller('api/user') @Controller('api/user')
@RequiredPermissions(Permission.UserManage) @RequiredPermissions(Permission.UserManage)
@@ -53,7 +58,7 @@ export class UserManageController {
} }
return { return {
users, users: users.map(EUserBackend2EUser),
count: users.length, count: users.length,
page: body.page, page: body.page,
}; };
@@ -73,20 +78,18 @@ export class UserManageController {
throw new InternalServerErrorException('Could not create user'); throw new InternalServerErrorException('Could not create user');
} }
return user; return EUserBackend2EUser(user);
} }
@Post('delete') @Post('delete')
async delete( async delete(@Body() body: UserDeleteRequest): Promise<UserDeleteResponse> {
@Body() body: UserDeleteRequest,
): Promise<UserDeleteResponse> {
const user = await this.usersService.delete(body.id); const user = await this.usersService.delete(body.id);
if (HasFailed(user)) { if (HasFailed(user)) {
this.logger.warn(user.getReason()); this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not delete user'); throw new InternalServerErrorException('Could not delete user');
} }
return user; return EUserBackend2EUser(user);
} }
@Post('info') @Post('info')
@@ -97,7 +100,7 @@ export class UserManageController {
throw new InternalServerErrorException('Could not find user'); throw new InternalServerErrorException('Could not find user');
} }
return user; return EUserBackend2EUser(user);
} }
@Post('update') @Post('update')
@@ -111,7 +114,7 @@ export class UserManageController {
} }
if (body.roles) { if (body.roles) {
user = await this.usersService.setRoles(user.id, body.roles); user = await this.usersService.setRoles(body.id, body.roles);
if (HasFailed(user)) { if (HasFailed(user)) {
this.logger.warn(user.getReason()); this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not update user'); throw new InternalServerErrorException('Could not update user');
@@ -119,14 +122,14 @@ export class UserManageController {
} }
if (body.password) { if (body.password) {
user = await this.usersService.updatePassword(user.id, body.password); user = await this.usersService.updatePassword(body.id, body.password);
if (HasFailed(user)) { if (HasFailed(user)) {
this.logger.warn(user.getReason()); this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not update user'); throw new InternalServerErrorException('Could not update user');
} }
} }
return user; return EUserBackend2EUser(user);
} }
@Get('special') @Get('special')

View File

@@ -1,11 +1,12 @@
import { import {
Controller, Controller,
Get, Get,
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
NotFoundException, NotFoundException,
Param, Param,
Post, Res Post,
Res
} from '@nestjs/common'; } from '@nestjs/common';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
@@ -15,6 +16,7 @@ import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto'; import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto'; import { ImageUploadDto } from '../../models/requests/imageroute.dto';
import { EImageBackend2EImage } from '../../models/transformers/image.transformer';
import { ImageIdValidator } from './imageid.validator'; import { ImageIdValidator } from './imageid.validator';
// This is the only controller with CORS enabled // This is the only controller with CORS enabled
@@ -52,7 +54,7 @@ export class ImageController {
throw new NotFoundException('Could not find image'); throw new NotFoundException('Could not find image');
} }
return image; return EImageBackend2EImage(image);
} }
@Post() @Post()
@@ -67,6 +69,6 @@ export class ImageController {
throw new InternalServerErrorException('Could not upload image'); throw new InternalServerErrorException('Could not upload image');
} }
return image; return EImageBackend2EImage(image);
} }
} }

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SysPreferenceBaseResponse } from 'picsur-shared/dist/dto/api/syspref.dto'; import { DecodedSysPref } from 'picsur-shared/dist/dto/preferences.dto';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { SysprefService as SysPrefService } from 'src/app/services/api/syspref.service'; import { SysprefService as SysPrefService } from 'src/app/services/api/syspref.service';
@@ -7,7 +7,7 @@ import { SysprefService as SysPrefService } from 'src/app/services/api/syspref.s
templateUrl: './settings-syspref.component.html', templateUrl: './settings-syspref.component.html',
}) })
export class SettingsSysprefComponent { export class SettingsSysprefComponent {
preferences: Observable<SysPreferenceBaseResponse[]>; preferences: Observable<DecodedSysPref[]>;
constructor(sysprefService: SysPrefService) { constructor(sysprefService: SysPrefService) {
this.preferences = sysprefService.live; this.preferences = sysprefService.live;

View File

@@ -1,7 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { SysPreferenceBaseResponse } from 'picsur-shared/dist/dto/api/syspref.dto'; import { DecodedSysPref, PrefValueType } from 'picsur-shared/dist/dto/preferences.dto';
import { PrefValueType } from 'picsur-shared/dist/dto/preferences.dto';
import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto'; import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { Subject, throttleTime } from 'rxjs'; import { Subject, throttleTime } from 'rxjs';
@@ -16,7 +15,7 @@ import { UtilService } from 'src/app/util/util.service';
styleUrls: ['./settings-syspref-option.component.scss'], styleUrls: ['./settings-syspref-option.component.scss'],
}) })
export class SettingsSysprefOptionComponent implements OnInit { export class SettingsSysprefOptionComponent implements OnInit {
@Input() pref: SysPreferenceBaseResponse; @Input() pref: DecodedSysPref;
private updateSubject = new Subject<PrefValueType>(); private updateSubject = new Subject<PrefValueType>();

View File

@@ -73,7 +73,7 @@ export class SettingsUsersComponent implements OnInit {
}); });
if (pressedButton === 'delete') { if (pressedButton === 'delete') {
const result = await this.userManageService.deleteUser(user.id); const result = await this.userManageService.deleteUser(user.id ?? '');
if (HasFailed(result)) { if (HasFailed(result)) {
this.utilService.showSnackBar( this.utilService.showSnackBar(
'Failed to delete user', 'Failed to delete user',

View File

@@ -3,12 +3,11 @@ import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { import {
GetSyspreferenceResponse, GetSyspreferenceResponse,
MultipleSysPreferencesResponse, MultipleSysPreferencesResponse,
SysPreferenceBaseResponse,
UpdateSysPreferenceRequest, UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/syspref.dto'; } from 'picsur-shared/dist/dto/api/syspref.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; import { DecodedSysPref, PrefValueType } from 'picsur-shared/dist/dto/preferences.dto';
import { AsyncFailable, Fail, HasFailed, Map } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail, HasFailed, Map } from 'picsur-shared/dist/types';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
@@ -25,7 +24,7 @@ export class SysprefService {
private hasPermission = false; private hasPermission = false;
private sysprefObservable = new BehaviorSubject<SysPreferenceBaseResponse[]>( private sysprefObservable = new BehaviorSubject<DecodedSysPref[]>(
[] []
); );
@@ -57,7 +56,7 @@ export class SysprefService {
} }
} }
public async getPreferences(): AsyncFailable<SysPreferenceBaseResponse[]> { public async getPreferences(): AsyncFailable<DecodedSysPref[]> {
if (!this.hasPermission) if (!this.hasPermission)
return Fail('You do not have permission to edit system preferences'); return Fail('You do not have permission to edit system preferences');
@@ -105,7 +104,7 @@ export class SysprefService {
return response; return response;
} }
private updatePrefArray(pref: SysPreferenceBaseResponse) { private updatePrefArray(pref: DecodedSysPref) {
const prefArray = this.snapshot; const prefArray = this.snapshot;
// Replace the old pref with the new one // Replace the old pref with the new one
const index = prefArray.findIndex((i) => pref.key === i.key); const index = prefArray.findIndex((i) => pref.key === i.key);

View File

@@ -1,7 +1,5 @@
import { import {
IsBoolean, IsBoolean, IsInt,
IsDefined,
IsInt,
IsNotEmpty, IsNotEmpty,
IsString, IsString,
Max, Max,
@@ -10,21 +8,17 @@ import {
class BaseApiResponse<T extends Object, W extends boolean> { class BaseApiResponse<T extends Object, W extends boolean> {
@IsBoolean() @IsBoolean()
@IsDefined()
success: W; success: W;
@IsInt() @IsInt()
@Min(0) @Min(0)
@Max(1000) @Max(1000)
@IsDefined()
statusCode: number; statusCode: number;
@IsString() @IsString()
@IsNotEmpty()
timestamp: string; timestamp: string;
//@ValidateNested() @IsNotEmpty()
@IsDefined()
data: T; data: T;
} }
@@ -35,7 +29,6 @@ export class ApiSuccessResponse<T extends Object> extends BaseApiResponse<
export class ApiErrorData { export class ApiErrorData {
@IsString() @IsString()
@IsNotEmpty()
message: string; message: string;
} }
export class ApiErrorResponse extends BaseApiResponse<ApiErrorData, false> {} export class ApiErrorResponse extends BaseApiResponse<ApiErrorData, false> {}

View File

@@ -1,24 +1,19 @@
import { IsBoolean, IsDefined, IsSemVer, IsString } from 'class-validator'; import { IsBoolean, IsSemVer } from 'class-validator';
import { IsStringList } from '../../validators/string-list.validator'; import { IsStringList } from '../../validators/string-list.validator';
export class InfoResponse { export class InfoResponse {
@IsBoolean() @IsBoolean()
@IsDefined()
production: boolean; production: boolean;
@IsBoolean() @IsBoolean()
@IsDefined()
demo: boolean; demo: boolean;
@IsDefined()
@IsString()
@IsSemVer() @IsSemVer()
version: string; version: string;
} }
// AllPermissions // AllPermissions
export class AllPermissionsResponse { export class AllPermissionsResponse {
@IsDefined()
@IsStringList() @IsStringList()
permissions: string[]; permissions: string[];
} }

View File

@@ -1,23 +1,21 @@
import { Type } from 'class-transformer'; import { IsArray } from 'class-validator';
import { IsArray, IsDefined, ValidateNested } from 'class-validator'; import { ERole, SimpleRole } from '../../entities/role.entity';
import { import { IsNested } from '../../validators/nested.validator';
ERole,
RoleNameObject,
RoleNamePermsObject
} from '../../entities/role.entity';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.validator';
import { IsRoleName } from '../../validators/role.validators';
import { IsStringList } from '../../validators/string-list.validator'; import { IsStringList } from '../../validators/string-list.validator';
// RoleInfo // RoleInfo
export class RoleInfoRequest extends RoleNameObject {} export class RoleInfoRequest {
@IsRoleName()
name: string;
}
export class RoleInfoResponse extends ERole {} export class RoleInfoResponse extends ERole {}
// RoleList // RoleList
export class RoleListResponse { export class RoleListResponse {
@IsArray() @IsArray()
@IsDefined() @IsNested(ERole)
@ValidateNested()
@Type(() => ERole)
roles: ERole[]; roles: ERole[];
@IsPosInt() @IsPosInt()
@@ -25,32 +23,31 @@ export class RoleListResponse {
} }
// RoleUpdate // RoleUpdate
export class RoleUpdateRequest extends RoleNamePermsObject {} export class RoleUpdateRequest extends SimpleRole {}
export class RoleUpdateResponse extends ERole {} export class RoleUpdateResponse extends ERole {}
// RoleCreate // RoleCreate
export class RoleCreateRequest extends RoleNamePermsObject {} export class RoleCreateRequest extends SimpleRole {}
export class RoleCreateResponse extends ERole {} export class RoleCreateResponse extends ERole {}
// RoleDelete // RoleDelete
export class RoleDeleteRequest extends RoleNameObject {} export class RoleDeleteRequest {
@IsRoleName()
name: string;
}
export class RoleDeleteResponse extends ERole {} export class RoleDeleteResponse extends ERole {}
// SpecialRoles // SpecialRoles
export class SpecialRolesResponse { export class SpecialRolesResponse {
@IsDefined()
@IsStringList() @IsStringList()
SoulBoundRoles: string[]; SoulBoundRoles: string[];
@IsDefined()
@IsStringList() @IsStringList()
ImmutableRoles: string[]; ImmutableRoles: string[];
@IsDefined()
@IsStringList() @IsStringList()
UndeletableRoles: string[]; UndeletableRoles: string[];
@IsDefined()
@IsStringList() @IsStringList()
DefaultRoles: string[]; DefaultRoles: string[];
} }

View File

@@ -1,36 +1,21 @@
import { Type } from 'class-transformer';
import { import {
IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested IsArray
} from 'class-validator'; } from 'class-validator';
import { IsNested } from '../../validators/nested.validator';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.validator';
import { IsSysPrefValue } from '../../validators/syspref.validator'; import { IsPrefValue } from '../../validators/pref-value.validator';
import { PrefValueType, PrefValueTypes, PrefValueTypeStrings } from '../preferences.dto'; import { DecodedSysPref, PrefValueType } from '../preferences.dto';
export class SysPreferenceBaseResponse {
@IsNotEmpty()
@IsString()
key: string;
@IsNotEmpty()
@IsSysPrefValue()
value: PrefValueType;
@IsNotEmpty()
@IsEnum(PrefValueTypes)
type: PrefValueTypeStrings;
}
// Get Syspreference // Get Syspreference
// Request is done via url parameters // Request is done via url parameters
export class GetSyspreferenceResponse extends SysPreferenceBaseResponse {} export class GetSyspreferenceResponse extends DecodedSysPref {}
// Get syspreferences // Get syspreferences
export class MultipleSysPreferencesResponse { export class MultipleSysPreferencesResponse {
@IsArray() @IsArray()
@IsNotEmpty() @IsNested(DecodedSysPref)
@ValidateNested({ each: true }) preferences: DecodedSysPref[];
@Type(() => SysPreferenceBaseResponse)
preferences: SysPreferenceBaseResponse[];
@IsPosInt() @IsPosInt()
total: number; total: number;
@@ -38,10 +23,9 @@ export class MultipleSysPreferencesResponse {
// Update Syspreference // Update Syspreference
export class UpdateSysPreferenceRequest { export class UpdateSysPreferenceRequest {
@IsNotEmpty() @IsPrefValue()
@IsSysPrefValue()
value: PrefValueType; value: PrefValueType;
} }
export class UpdateSysPreferenceResponse extends SysPreferenceBaseResponse {} export class UpdateSysPreferenceResponse extends DecodedSysPref {}

View File

@@ -1,45 +1,47 @@
import { Type } from 'class-transformer';
import { import {
IsDefined, IsJWT, IsJWT
IsString,
ValidateNested
} from 'class-validator'; } from 'class-validator';
import { EUser, NamePassUser } from '../../entities/user.entity'; import { EUser } from '../../entities/user.entity';
import { IsNested } from '../../validators/nested.validator';
import { IsStringList } from '../../validators/string-list.validator'; import { IsStringList } from '../../validators/string-list.validator';
import { IsPlainTextPwd, IsUsername } from '../../validators/user.validators';
// Api // Api
// UserLogin // UserLogin
export class UserLoginRequest extends NamePassUser {} export class UserLoginRequest {
@IsUsername()
username: string;
@IsPlainTextPwd()
password: string;
}
export class UserLoginResponse { export class UserLoginResponse {
@IsString()
@IsDefined()
@IsJWT() @IsJWT()
jwt_token: string; jwt_token: string;
} }
// UserRegister // UserRegister
export class UserRegisterRequest extends NamePassUser {} export class UserRegisterRequest {
@IsUsername()
username: string;
@IsPlainTextPwd()
password: string;
}
export class UserRegisterResponse extends EUser {} export class UserRegisterResponse extends EUser {}
// UserMe // UserMe
export class UserMeResponse { export class UserMeResponse {
@IsDefined() @IsNested(EUser)
@ValidateNested()
@Type(() => EUser)
user: EUser; user: EUser;
@IsString()
@IsDefined()
@IsJWT() @IsJWT()
token: string; token: string;
} }
// UserMePermissions // UserMePermissions
export class UserMePermissionsResponse { export class UserMePermissionsResponse {
@IsDefined()
@IsStringList() @IsStringList()
permissions: string[]; permissions: string[];
} }

View File

@@ -1,14 +1,10 @@
import { Type } from 'class-transformer'; import { IsArray, IsOptional } from 'class-validator';
import { import { EUser, SimpleUser } from '../../entities/user.entity';
IsArray, import { Newable } from '../../types';
IsDefined, import { IsEntityID } from '../../validators/entity-id.validator';
IsOptional, import { IsNested } from '../../validators/nested.validator';
ValidateNested
} from 'class-validator';
import { EUser, NamePassUser } from '../../entities/user.entity';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.validator';
import { IsStringList } from '../../validators/string-list.validator'; import { IsStringList } from '../../validators/string-list.validator';
import { IsPlainTextPwd, IsUsername } from '../../validators/user.validators';
import { EntityIDObject } from '../idobject.dto'; import { EntityIDObject } from '../idobject.dto';
// UserList // UserList
@@ -22,9 +18,7 @@ export class UserListRequest {
export class UserListResponse { export class UserListResponse {
@IsArray() @IsArray()
@IsDefined() @IsNested(EUser)
@ValidateNested()
@Type(() => EUser)
users: EUser[]; users: EUser[];
@IsPosInt() @IsPosInt()
@@ -35,11 +29,7 @@ export class UserListResponse {
} }
// UserCreate // UserCreate
export class UserCreateRequest extends NamePassUser { export class UserCreateRequest extends SimpleUser {}
@IsOptional()
@IsStringList()
roles?: string[];
}
export class UserCreateResponse extends EUser {} export class UserCreateResponse extends EUser {}
// UserDelete // UserDelete
@@ -51,33 +41,31 @@ export class UserInfoRequest extends EntityIDObject {}
export class UserInfoResponse extends EUser {} export class UserInfoResponse extends EUser {}
// UserUpdate // UserUpdate
export class UserUpdateRequest extends EntityIDObject { export class UserUpdateRequest extends (SimpleUser as Newable<
@IsOptional() Partial<SimpleUser>
@IsUsername() >) {
username?: string; @IsEntityID()
id: string;
@IsOptional() @IsOptional()
@IsStringList() override username?: string;
roles?: string[];
@IsPlainTextPwd()
@IsOptional() @IsOptional()
password?: string; override password?: string;
@IsOptional()
override roles?: string[];
} }
export class UserUpdateResponse extends EUser {} export class UserUpdateResponse extends EUser {}
// GetSpecialUsers // GetSpecialUsers
export class GetSpecialUsersResponse { export class GetSpecialUsersResponse {
@IsDefined()
@IsStringList() @IsStringList()
UndeletableUsersList: string[]; UndeletableUsersList: string[];
@IsDefined()
@IsStringList() @IsStringList()
ImmutableUsersList: string[]; ImmutableUsersList: string[];
@IsDefined()
@IsStringList() @IsStringList()
LockedLoginUsersList: string[]; LockedLoginUsersList: string[];
} }

View File

@@ -1,6 +1,6 @@
import { EntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export class EntityIDObject { export class EntityIDObject {
@EntityID() @IsEntityID()
id: string; id: string;
} }

View File

@@ -1,11 +1,9 @@
import { Type } from 'class-transformer'; import { IsInt, IsOptional } from 'class-validator';
import { IsDefined, IsInt, IsOptional, ValidateNested } from 'class-validator';
import { EUser } from '../entities/user.entity'; import { EUser } from '../entities/user.entity';
import { IsNested } from '../validators/nested.validator';
export class JwtDataDto { export class JwtDataDto {
@IsDefined() @IsNested(EUser)
@ValidateNested()
@Type(() => EUser)
user: EUser; user: EUser;
@IsOptional() @IsOptional()

View File

@@ -1,4 +1,26 @@
import { IsEnum, IsString } from 'class-validator';
import { IsEntityID } from '../validators/entity-id.validator';
import { IsPrefValue } from '../validators/pref-value.validator';
// Variable value type // Variable value type
export type PrefValueType = string | number | boolean; export type PrefValueType = string | number | boolean;
export type PrefValueTypeStrings = 'string' | 'number' | 'boolean'; export type PrefValueTypeStrings = 'string' | 'number' | 'boolean';
export const PrefValueTypes = ['string', 'number', 'boolean']; export const PrefValueTypes = ['string', 'number', 'boolean'];
// Decoded Representations
export class DecodedSysPref {
@IsString()
key: string;
@IsPrefValue()
value: PrefValueType;
@IsEnum(PrefValueTypes)
type: PrefValueTypeStrings;
}
export class DecodedUsrPref extends DecodedSysPref {
@IsEntityID()
user: string;
}

View File

@@ -1,4 +1,3 @@
import { PrefValueType, PrefValueTypeStrings } from './preferences.dto';
// This enum is only here to make accessing the values easier, and type checking in the backend // This enum is only here to make accessing the values easier, and type checking in the backend
export enum SysPreference { export enum SysPreference {
@@ -9,10 +8,3 @@ export enum SysPreference {
TestNumber = 'test_number', TestNumber = 'test_number',
TestBoolean = 'test_boolean', TestBoolean = 'test_boolean',
} }
// Interfaces
export interface InternalSysPrefRepresentation {
key: string;
value: PrefValueType;
type: PrefValueTypeStrings;
}

View File

@@ -1,4 +1,3 @@
import { PrefValueType, PrefValueTypeStrings } from './preferences.dto';
// This enum is only here to make accessing the values easier, and type checking in the backend // This enum is only here to make accessing the values easier, and type checking in the backend
export enum UsrPreference { export enum UsrPreference {
@@ -7,10 +6,3 @@ export enum UsrPreference {
TestBoolean = 'test_boolean', TestBoolean = 'test_boolean',
} }
// Interfaces
export interface InternalUsrPrefRepresentation {
key: string;
value: PrefValueType;
type: PrefValueTypeStrings;
user: number;
}

View File

@@ -1,20 +1,19 @@
import { Exclude } from 'class-transformer'; import { IsHash, IsOptional, IsString } from 'class-validator';
import { IsHash, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { EntityID } from '../validators/entity-id.validator'; import { IsNotDefined } from '../validators/not-defined.validator';
export class EImage { export class EImage {
@EntityID() @IsOptional()
id: string; @IsEntityID()
id?: string;
@IsHash('sha256') @IsHash('sha256')
hash: string; hash: string;
// Binary data // Because typescript does not support exact types, we have to do this stupidness
@IsOptional() @IsNotDefined()
@Exclude() // Dont send this by default data: undefined;
data?: object;
@IsNotEmpty()
@IsString() @IsString()
mime: string; mime: string;
} }

View File

@@ -1,23 +1,24 @@
import { IsDefined } from 'class-validator'; import { IsOptional } from 'class-validator';
import { EntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { IsRoleName } from '../validators/role.validators'; import { IsRoleName } from '../validators/role.validators';
import { IsStringList } from '../validators/string-list.validator'; import { IsStringList } from '../validators/string-list.validator';
// This entity is build from multiple smaller enitities export class SimpleRole {
// Theses smaller entities are used in other places
export class RoleNameObject {
@IsRoleName() @IsRoleName()
name: string; name: string;
}
export class RoleNamePermsObject extends RoleNameObject {
@IsDefined()
@IsStringList() @IsStringList()
permissions: string[]; permissions: string[];
} }
export class ERole extends RoleNamePermsObject { export class ERole {
@EntityID() @IsOptional()
id: string; @IsEntityID()
id?: string;
@IsRoleName()
name: string;
@IsStringList()
permissions: string[];
} }

View File

@@ -1,15 +1,14 @@
import { IsNotEmpty, IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
import { EntityIDOptional } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export class ESysPreference { export class ESysPreference {
@EntityIDOptional() @IsOptional()
@IsEntityID()
id?: string; id?: string;
@IsNotEmpty()
@IsString() @IsString()
key: string; key: string;
@IsNotEmpty()
@IsString() @IsString()
value: string; value: string;
} }

View File

@@ -1,37 +1,32 @@
import { Exclude } from 'class-transformer'; import { IsOptional } from 'class-validator';
import { IsDefined, IsOptional, IsString } from 'class-validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { EntityID } from '../validators/entity-id.validator'; import { IsNotDefined } from '../validators/not-defined.validator';
import { IsStringList } from '../validators/string-list.validator'; import { IsStringList } from '../validators/string-list.validator';
import { IsPlainTextPwd, IsUsername } from '../validators/user.validators'; import { IsPlainTextPwd, IsUsername } from '../validators/user.validators';
// This entity is build from multiple smaller enitities export class SimpleUser {
// Theses smaller entities are used in other places
export class UsernameUser {
@IsUsername() @IsUsername()
username: string; username: string;
}
// This is a simple user object with just the username and unhashed password
export class NamePassUser extends UsernameUser {
@IsPlainTextPwd() @IsPlainTextPwd()
password: string; password: string;
}
// Add a user object with just the username and roles for jwt
export class NameRolesUser extends UsernameUser {
@IsDefined()
@IsStringList() @IsStringList()
roles: string[]; roles: string[];
} }
// Actual entity that goes in the db export class EUser {
export class EUser extends NameRolesUser {
@EntityID()
id: string;
@IsOptional() @IsOptional()
@Exclude() @IsEntityID()
@IsString() id?: string;
password?: string;
@IsUsername()
username: string;
@IsStringList()
roles: string[];
// Because typescript does not support exact types, we have to do this stupidness
@IsNotDefined()
hashedPassword: undefined;
} }

View File

@@ -1,20 +1,18 @@
import { IsDefined, IsNotEmpty, IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
import { EntityIDOptional } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { IsPosInt } from '../validators/positive-int.validator'; import { IsPosInt } from '../validators/positive-int.validator';
export class EUsrPreference { export class EUsrPreference {
@EntityIDOptional() @IsOptional()
@IsEntityID()
id?: string; id?: string;
@IsNotEmpty()
@IsString() @IsString()
key: string; key: string;
@IsNotEmpty()
@IsString() @IsString()
value: string; value: string;
@IsDefined()
@IsPosInt() @IsPosInt()
userId: number; userId: number;
} }

View File

@@ -3,7 +3,7 @@ type FCDecorator = MethodDecorator & ClassDecorator;
export function CombineFCDecorators(...decorators: FCDecorator[]) { export function CombineFCDecorators(...decorators: FCDecorator[]) {
return (target: any, key: string, descriptor: PropertyDescriptor) => { return (target: any, key: string, descriptor: PropertyDescriptor) => {
decorators.forEach(decorator => { decorators.forEach(decorator => {
decorator(target, key, descriptor); decorator(target, key, descriptor as any);
}); });
} }
} }

View File

@@ -1,14 +1,14 @@
import { validate } from 'class-validator'; import { validate, ValidatorOptions } from 'class-validator';
// For some stupid reason, the class-validator library does not have a way to set global defaults // For some stupid reason, the class-validator library does not have a way to set global defaults
// So now we have to do it this way // So now we have to do it this way
export const ValidateOptions = { export const ValidateOptions: ValidatorOptions = {
disableErrorMessages: true,
forbidNonWhitelisted: true, forbidNonWhitelisted: true,
forbidUnknownValues: true, forbidUnknownValues: true,
stopAtFirstError: true, stopAtFirstError: true,
whitelist: true, whitelist: true,
strictGroups: true,
}; };
export const strictValidate = (object: object) => export const strictValidate = (object: object) =>

View File

@@ -1,5 +1,4 @@
import { IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; import { IsUUID } from 'class-validator';
import { CombinePDecorators } from '../util/decorator'; import { CombinePDecorators } from '../util/decorator';
export const EntityID = CombinePDecorators(IsNotEmpty(), IsUUID('4')); export const IsEntityID = CombinePDecorators(IsUUID('4'));
export const EntityIDOptional = CombinePDecorators(IsOptional(), IsUUID('4'));

View File

@@ -0,0 +1,15 @@
import { Type } from 'class-transformer';
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Newable } from '../types';
export const IsNested = (nestedClass: Newable<any>) => {
const nestedValidator = ValidateNested();
const isNotEmptyValidator = IsNotEmpty();
const typeValidator = Type(() => nestedClass);
return (target: Object, propertyKey: string | symbol): void => {
nestedValidator(target, propertyKey);
isNotEmptyValidator(target, propertyKey);
typeValidator(target, propertyKey);
};
};

View File

@@ -0,0 +1,26 @@
import {
IsOptional,
registerDecorator,
ValidationArguments,
ValidationOptions
} from 'class-validator';
export function isNotDefined(value: any, args: ValidationArguments) {
return value === undefined || value === null;
}
export function IsNotDefined(validationOptions?: ValidationOptions) {
const optional = IsOptional();
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isNotDefined',
target: object.constructor,
propertyName: propertyName,
options: validationOptions ?? {},
validator: {
validate: isNotDefined,
},
});
optional(object, propertyName);
};
}

View File

@@ -1,4 +1,4 @@
import { IsDefined, IsInt, Min } from 'class-validator'; import { IsInt, Min } from 'class-validator';
import { CombinePDecorators } from '../util/decorator'; import { CombinePDecorators } from '../util/decorator';
export const IsPosInt = CombinePDecorators(IsInt(), Min(0), IsDefined()); export const IsPosInt = CombinePDecorators(IsInt(), Min(0));

View File

@@ -1,20 +1,20 @@
import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator';
import { PrefValueTypes } from '../dto/preferences.dto'; import { PrefValueTypes } from '../dto/preferences.dto';
export function isSysPrefValue(value: any, args: ValidationArguments) { export function isPrefValue(value: any, args: ValidationArguments) {
const type = typeof value; const type = typeof value;
return PrefValueTypes.includes(type); return PrefValueTypes.includes(type);
} }
export function IsSysPrefValue(validationOptions?: ValidationOptions) { export function IsPrefValue(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) { return function (object: Object, propertyName: string) {
registerDecorator({ registerDecorator({
name: 'isSysPrefValue', name: 'isPrefValue',
target: object.constructor, target: object.constructor,
propertyName: propertyName, propertyName: propertyName,
options: validationOptions, options: validationOptions ?? {},
validator: { validator: {
validate: isSysPrefValue, validate: isPrefValue,
}, },
}); });
}; };

View File

@@ -1,8 +1,7 @@
import { IsAlphanumeric, IsNotEmpty, IsString, Length } from 'class-validator'; import { IsAlphanumeric, IsString, Length } from 'class-validator';
import { CombinePDecorators } from '../util/decorator'; import { CombinePDecorators } from '../util/decorator';
export const IsRoleName = CombinePDecorators( export const IsRoleName = CombinePDecorators(
IsNotEmpty(),
IsString(), IsString(),
Length(4, 32), Length(4, 32),
IsAlphanumeric(), IsAlphanumeric(),

View File

@@ -1,12 +1,9 @@
import { import {
IsArray, IsArray, IsString
IsNotEmpty,
IsString
} from 'class-validator'; } from 'class-validator';
import { CombinePDecorators } from '../util/decorator'; import { CombinePDecorators } from '../util/decorator';
export const IsStringList = CombinePDecorators( export const IsStringList = CombinePDecorators(
IsArray(), IsArray(),
IsString({ each: true }), IsString({ each: true }),
IsNotEmpty({ each: true }),
); );

View File

@@ -1,18 +1,16 @@
import { IsAlphanumeric, IsNotEmpty, IsString, Length } from 'class-validator'; import { IsAlphanumeric, IsString, Length } from 'class-validator';
import { CombinePDecorators } from '../util/decorator'; import { CombinePDecorators } from '../util/decorator';
// Match this with user validators in frontend // Match this with user validators in frontend
// (Frontend is not security focused, but it tells the user what is wrong) // (Frontend is not security focused, but it tells the user what is wrong)
export const IsUsername = CombinePDecorators( export const IsUsername = CombinePDecorators(
IsNotEmpty(),
IsString(), IsString(),
Length(4, 32), Length(4, 32),
IsAlphanumeric(), IsAlphanumeric(),
); );
export const IsPlainTextPwd = CombinePDecorators( export const IsPlainTextPwd = CombinePDecorators(
IsNotEmpty(),
IsString(), IsString(),
Length(4, 1024), Length(4, 1024),
); );