diff --git a/backend/src/collections/imagedb/imagedb.service.ts b/backend/src/collections/imagedb/imagedb.service.ts index ab5a827..d7e67b1 100644 --- a/backend/src/collections/imagedb/imagedb.service.ts +++ b/backend/src/collections/imagedb/imagedb.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { plainToClass } from 'class-transformer'; import Crypto from 'crypto'; import { AsyncFailable, @@ -36,8 +35,7 @@ export class ImageDBService { return Fail(e?.message); } - // Strips unwanted data - return plainToClass(EImageBackend, imageEntity); + return imageEntity; } public async findOne( diff --git a/backend/src/collections/roledb/roledb.service.ts b/backend/src/collections/roledb/roledb.service.ts index 4eebf99..a261c84 100644 --- a/backend/src/collections/roledb/roledb.service.ts +++ b/backend/src/collections/roledb/roledb.service.ts @@ -52,10 +52,7 @@ export class RolesService { } try { - // Makes sure we can return the id - const cloned = plainToClass(ERoleBackend, roleToModify); - await this.rolesRepository.remove(roleToModify); - return cloned; + return await this.rolesRepository.remove(roleToModify); } catch (e: any) { return Fail(e?.message); } diff --git a/backend/src/collections/syspreferencesdb/syspreferencedb.service.ts b/backend/src/collections/syspreferencesdb/syspreferencedb.service.ts index b8a25d7..8d61f09 100644 --- a/backend/src/collections/syspreferencesdb/syspreferencedb.service.ts +++ b/backend/src/collections/syspreferencesdb/syspreferencedb.service.ts @@ -1,10 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { PrefValueType, PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto'; import { - InternalSysPrefRepresentation, - SysPreference -} from 'picsur-shared/dist/dto/syspreferences.dto'; + DecodedSysPref, + PrefValueType, + PrefValueTypeStrings +} from 'picsur-shared/dist/dto/preferences.dto'; +import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto'; import { AsyncFailable, Fail, @@ -33,7 +34,7 @@ export class SysPreferenceService { public async setPreference( key: string, value: PrefValueType, - ): AsyncFailable { + ): AsyncFailable { // Validate let sysPreference = await this.validatePref(key, value); if (HasFailed(sysPreference)) return sysPreference; @@ -58,9 +59,7 @@ export class SysPreferenceService { }; } - public async getPreference( - key: string, - ): AsyncFailable { + public async getPreference(key: string): AsyncFailable { // Validate let validatedKey = this.validatePrefKey(key); if (HasFailed(validatedKey)) return validatedKey; @@ -116,9 +115,7 @@ export class SysPreferenceService { return pref.value; } - public async getAllPreferences(): AsyncFailable< - InternalSysPrefRepresentation[] - > { + public async getAllPreferences(): AsyncFailable { // TODO: We are fetching each value invidually, we should fetch all at once let internalSysPrefs = await Promise.all( SysPreferenceList.map((key) => this.getPreference(key)), @@ -127,21 +124,21 @@ export class SysPreferenceService { return Fail('Could not get all preferences'); } - return internalSysPrefs as InternalSysPrefRepresentation[]; + return internalSysPrefs as DecodedSysPref[]; } // Private private async saveDefault( key: SysPreference, // Force enum here because we dont validate - ): AsyncFailable { + ): AsyncFailable { return this.setPreference(key, this.defaultsService.sysDefaults[key]()); } // This converts the raw string representation of the value to the correct type private retrieveConvertedValue( preference: ESysPreferenceBackend, - ): Failable { + ): Failable { const key = this.validatePrefKey(preference.key); if (HasFailed(key)) return key; diff --git a/backend/src/collections/userdb/userdb.service.ts b/backend/src/collections/userdb/userdb.service.ts index 3c50045..ee1a225 100644 --- a/backend/src/collections/userdb/userdb.service.ts +++ b/backend/src/collections/userdb/userdb.service.ts @@ -53,7 +53,7 @@ export class UsersService { let user = new EUserBackend(); user.username = username; - user.password = hashedPassword; + user.hashedPassword = hashedPassword; if (byPassRoleCheck) { const rolesToAdd = roles ?? []; user.roles = makeUnique(rolesToAdd); @@ -64,13 +64,10 @@ export class UsersService { } try { - user = await this.usersRepository.save(user, { reload: true }); + return await this.usersRepository.save(user); } catch (e: any) { return Fail(e?.message); } - - // Strips unwanted data - return plainToClass(EUserBackend, user); } public async delete(uuid: string): AsyncFailable { @@ -153,8 +150,7 @@ export class UsersService { if (HasFailed(userToModify)) return userToModify; const strength = await this.getBCryptStrength(); - const hashedPassword = await bcrypt.hash(password, strength); - userToModify.password = hashedPassword; + userToModify.hashedPassword = await bcrypt.hash(password, strength); try { userToModify = await this.usersRepository.save(userToModify); @@ -180,7 +176,7 @@ export class UsersService { return Fail('Wrong username'); } - if (!(await bcrypt.compare(password, user.password))) + if (!(await bcrypt.compare(password, user.hashedPassword))) return Fail('Wrong password'); return await this.findOne(user.id); @@ -199,7 +195,11 @@ export class UsersService { try { const found = await this.usersRepository.findOne({ where: { username }, - select: getPrivate ? GetCols(this.usersRepository) : undefined, + ...(getPrivate + ? { + select: GetCols(this.usersRepository), + } + : {}), }); if (!found) return Fail('User not found'); diff --git a/backend/src/managers/auth/guards/main.guard.ts b/backend/src/managers/auth/guards/main.guard.ts index c746408..581eceb 100644 --- a/backend/src/managers/auth/guards/main.guard.ts +++ b/backend/src/managers/auth/guards/main.guard.ts @@ -41,6 +41,10 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { const user = await this.validateUser( 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 const permissions = this.extractPermissions(context); diff --git a/backend/src/models/entities/image.entity.ts b/backend/src/models/entities/image.entity.ts index a869f56..6a0f57e 100644 --- a/backend/src/models/entities/image.entity.ts +++ b/backend/src/models/entities/image.entity.ts @@ -1,19 +1,23 @@ +import { IsNotEmpty, IsOptional } from 'class-validator'; import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class EImageBackend extends EImage { - @PrimaryGeneratedColumn("uuid") - override id: string; + @PrimaryGeneratedColumn('uuid') + override id?: string; @Index() @Column({ unique: true, nullable: false }) override hash: string; - // Binary data - @Column({ type: 'bytea', nullable: false, select: false }) - override data?: Buffer; - @Column({ nullable: false }) override mime: string; + + // Binary data + @Column({ type: 'bytea', nullable: false, select: false }) + @IsOptional() + @IsNotEmpty() + // @ts-ignore + override data?: Buffer; } diff --git a/backend/src/models/entities/role.entity.ts b/backend/src/models/entities/role.entity.ts index 2cd21ea..0632857 100644 --- a/backend/src/models/entities/role.entity.ts +++ b/backend/src/models/entities/role.entity.ts @@ -5,7 +5,7 @@ import { Permissions } from '../dto/permissions.dto'; @Entity() export class ERoleBackend extends ERole { @PrimaryGeneratedColumn("uuid") - override id: string; + override id?: string; @Index() @Column({ nullable: false, unique: true }) diff --git a/backend/src/models/entities/user.entity.ts b/backend/src/models/entities/user.entity.ts index 9d14bf8..8079833 100644 --- a/backend/src/models/entities/user.entity.ts +++ b/backend/src/models/entities/user.entity.ts @@ -1,3 +1,4 @@ +import { IsOptional, IsString } from 'class-validator'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; @@ -6,7 +7,7 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class EUserBackend extends EUser { @PrimaryGeneratedColumn("uuid") - override id: string; + override id?: string; @Index() @Column({ nullable: false, unique: true }) @@ -16,5 +17,9 @@ export class EUserBackend extends EUser { override roles: string[]; @Column({ nullable: false, select: false }) - override password?: string; + @IsOptional() + @IsString() + // @ts-ignore + override hashedPassword?: string; } + diff --git a/backend/src/models/transformers/image.transformer.ts b/backend/src/models/transformers/image.transformer.ts new file mode 100644 index 0000000..b13ed3f --- /dev/null +++ b/backend/src/models/transformers/image.transformer.ts @@ -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, + }; +} diff --git a/backend/src/models/transformers/user.transformer.ts b/backend/src/models/transformers/user.transformer.ts new file mode 100644 index 0000000..5e832a6 --- /dev/null +++ b/backend/src/models/transformers/user.transformer.ts @@ -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, + }; +} diff --git a/backend/src/routes/api/user/user.controller.ts b/backend/src/routes/api/user/user.controller.ts index f3de767..e45d204 100644 --- a/backend/src/routes/api/user/user.controller.ts +++ b/backend/src/routes/api/user/user.controller.ts @@ -24,6 +24,7 @@ import { import { AuthManagerService } from '../../../managers/auth/auth.service'; import { Permission } from '../../../models/dto/permissions.dto'; import AuthFasityRequest from '../../../models/requests/authrequest.dto'; +import { EUserBackend2EUser } from '../../../models/transformers/user.transformer'; @Controller('api/user') export class UserController { @@ -60,12 +61,14 @@ export class UserController { throw new InternalServerErrorException('Could not register user'); } - return user; + return EUserBackend2EUser(user); } @Get('me') @RequiredPermissions(Permission.UserKeepLogin) async me(@Request() req: AuthFasityRequest): Promise { + if (!req.user.id) throw new InternalServerErrorException('User is corrupt'); + const user = await this.usersService.findOne(req.user.id); if (HasFailed(user)) { @@ -79,7 +82,7 @@ export class UserController { throw new InternalServerErrorException('Could not get new token'); } - return { user, token }; + return { user: EUserBackend2EUser(user), token }; } // You can always check your permissions @@ -88,6 +91,8 @@ export class UserController { async refresh( @Request() req: AuthFasityRequest, ): Promise { + if (!req.user.id) throw new InternalServerErrorException('User is corrupt'); + const permissions = await this.usersService.getPermissions(req.user.id); if (HasFailed(permissions)) { this.logger.warn(permissions.getReason()); diff --git a/backend/src/routes/api/user/usermanage.controller.ts b/backend/src/routes/api/user/usermanage.controller.ts index fc117a5..88e968d 100644 --- a/backend/src/routes/api/user/usermanage.controller.ts +++ b/backend/src/routes/api/user/usermanage.controller.ts @@ -24,7 +24,12 @@ import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; 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') @RequiredPermissions(Permission.UserManage) @@ -53,7 +58,7 @@ export class UserManageController { } return { - users, + users: users.map(EUserBackend2EUser), count: users.length, page: body.page, }; @@ -73,20 +78,18 @@ export class UserManageController { throw new InternalServerErrorException('Could not create user'); } - return user; + return EUserBackend2EUser(user); } @Post('delete') - async delete( - @Body() body: UserDeleteRequest, - ): Promise { + async delete(@Body() body: UserDeleteRequest): Promise { const user = await this.usersService.delete(body.id); if (HasFailed(user)) { this.logger.warn(user.getReason()); throw new InternalServerErrorException('Could not delete user'); } - return user; + return EUserBackend2EUser(user); } @Post('info') @@ -97,7 +100,7 @@ export class UserManageController { throw new InternalServerErrorException('Could not find user'); } - return user; + return EUserBackend2EUser(user); } @Post('update') @@ -111,7 +114,7 @@ export class UserManageController { } if (body.roles) { - user = await this.usersService.setRoles(user.id, body.roles); + user = await this.usersService.setRoles(body.id, body.roles); if (HasFailed(user)) { this.logger.warn(user.getReason()); throw new InternalServerErrorException('Could not update user'); @@ -119,14 +122,14 @@ export class UserManageController { } if (body.password) { - user = await this.usersService.updatePassword(user.id, body.password); + user = await this.usersService.updatePassword(body.id, body.password); if (HasFailed(user)) { this.logger.warn(user.getReason()); throw new InternalServerErrorException('Could not update user'); } } - return user; + return EUserBackend2EUser(user); } @Get('special') diff --git a/backend/src/routes/image/imageroute.controller.ts b/backend/src/routes/image/imageroute.controller.ts index 61a5eb0..f04be4d 100644 --- a/backend/src/routes/image/imageroute.controller.ts +++ b/backend/src/routes/image/imageroute.controller.ts @@ -1,11 +1,12 @@ import { - Controller, - Get, - InternalServerErrorException, - Logger, - NotFoundException, - Param, - Post, Res + Controller, + Get, + InternalServerErrorException, + Logger, + NotFoundException, + Param, + Post, + Res } from '@nestjs/common'; import { FastifyReply } from 'fastify'; 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 { Permission } from '../../models/dto/permissions.dto'; import { ImageUploadDto } from '../../models/requests/imageroute.dto'; +import { EImageBackend2EImage } from '../../models/transformers/image.transformer'; import { ImageIdValidator } from './imageid.validator'; // This is the only controller with CORS enabled @@ -52,7 +54,7 @@ export class ImageController { throw new NotFoundException('Could not find image'); } - return image; + return EImageBackend2EImage(image); } @Post() @@ -67,6 +69,6 @@ export class ImageController { throw new InternalServerErrorException('Could not upload image'); } - return image; + return EImageBackend2EImage(image); } } diff --git a/frontend/src/app/routes/settings/syspref/settings-syspref.component.ts b/frontend/src/app/routes/settings/syspref/settings-syspref.component.ts index e8f69ae..beb237e 100644 --- a/frontend/src/app/routes/settings/syspref/settings-syspref.component.ts +++ b/frontend/src/app/routes/settings/syspref/settings-syspref.component.ts @@ -1,5 +1,5 @@ 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 { 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', }) export class SettingsSysprefComponent { - preferences: Observable; + preferences: Observable; constructor(sysprefService: SysPrefService) { this.preferences = sysprefService.live; diff --git a/frontend/src/app/routes/settings/syspref/syspref-option/settings-syspref-option.component.ts b/frontend/src/app/routes/settings/syspref/syspref-option/settings-syspref-option.component.ts index 6b75449..489f11c 100644 --- a/frontend/src/app/routes/settings/syspref/syspref-option/settings-syspref-option.component.ts +++ b/frontend/src/app/routes/settings/syspref/syspref-option/settings-syspref-option.component.ts @@ -1,7 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { SysPreferenceBaseResponse } from 'picsur-shared/dist/dto/api/syspref.dto'; -import { PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; +import { DecodedSysPref, PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { Subject, throttleTime } from 'rxjs'; @@ -16,7 +15,7 @@ import { UtilService } from 'src/app/util/util.service'; styleUrls: ['./settings-syspref-option.component.scss'], }) export class SettingsSysprefOptionComponent implements OnInit { - @Input() pref: SysPreferenceBaseResponse; + @Input() pref: DecodedSysPref; private updateSubject = new Subject(); diff --git a/frontend/src/app/routes/settings/users/settings-users.component.ts b/frontend/src/app/routes/settings/users/settings-users.component.ts index 9420c93..c432834 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.ts +++ b/frontend/src/app/routes/settings/users/settings-users.component.ts @@ -73,7 +73,7 @@ export class SettingsUsersComponent implements OnInit { }); if (pressedButton === 'delete') { - const result = await this.userManageService.deleteUser(user.id); + const result = await this.userManageService.deleteUser(user.id ?? ''); if (HasFailed(result)) { this.utilService.showSnackBar( 'Failed to delete user', diff --git a/frontend/src/app/services/api/syspref.service.ts b/frontend/src/app/services/api/syspref.service.ts index 8057082..5f79204 100644 --- a/frontend/src/app/services/api/syspref.service.ts +++ b/frontend/src/app/services/api/syspref.service.ts @@ -3,12 +3,11 @@ import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { GetSyspreferenceResponse, MultipleSysPreferencesResponse, - SysPreferenceBaseResponse, UpdateSysPreferenceRequest, UpdateSysPreferenceResponse } from 'picsur-shared/dist/dto/api/syspref.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 { BehaviorSubject } from 'rxjs'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; @@ -25,7 +24,7 @@ export class SysprefService { private hasPermission = false; - private sysprefObservable = new BehaviorSubject( + private sysprefObservable = new BehaviorSubject( [] ); @@ -57,7 +56,7 @@ export class SysprefService { } } - public async getPreferences(): AsyncFailable { + public async getPreferences(): AsyncFailable { if (!this.hasPermission) return Fail('You do not have permission to edit system preferences'); @@ -105,7 +104,7 @@ export class SysprefService { return response; } - private updatePrefArray(pref: SysPreferenceBaseResponse) { + private updatePrefArray(pref: DecodedSysPref) { const prefArray = this.snapshot; // Replace the old pref with the new one const index = prefArray.findIndex((i) => pref.key === i.key); diff --git a/shared/src/dto/api/api.dto.ts b/shared/src/dto/api/api.dto.ts index 1d5b262..c41eae2 100644 --- a/shared/src/dto/api/api.dto.ts +++ b/shared/src/dto/api/api.dto.ts @@ -1,7 +1,5 @@ import { - IsBoolean, - IsDefined, - IsInt, + IsBoolean, IsInt, IsNotEmpty, IsString, Max, @@ -10,21 +8,17 @@ import { class BaseApiResponse { @IsBoolean() - @IsDefined() success: W; @IsInt() @Min(0) @Max(1000) - @IsDefined() statusCode: number; @IsString() - @IsNotEmpty() timestamp: string; - //@ValidateNested() - @IsDefined() + @IsNotEmpty() data: T; } @@ -35,7 +29,6 @@ export class ApiSuccessResponse extends BaseApiResponse< export class ApiErrorData { @IsString() - @IsNotEmpty() message: string; } export class ApiErrorResponse extends BaseApiResponse {} diff --git a/shared/src/dto/api/info.dto.ts b/shared/src/dto/api/info.dto.ts index 0984a99..4feca98 100644 --- a/shared/src/dto/api/info.dto.ts +++ b/shared/src/dto/api/info.dto.ts @@ -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'; export class InfoResponse { @IsBoolean() - @IsDefined() production: boolean; @IsBoolean() - @IsDefined() demo: boolean; - @IsDefined() - @IsString() @IsSemVer() version: string; } // AllPermissions export class AllPermissionsResponse { - @IsDefined() @IsStringList() permissions: string[]; } diff --git a/shared/src/dto/api/roles.dto.ts b/shared/src/dto/api/roles.dto.ts index 76c19bf..ef3e822 100644 --- a/shared/src/dto/api/roles.dto.ts +++ b/shared/src/dto/api/roles.dto.ts @@ -1,23 +1,21 @@ -import { Type } from 'class-transformer'; -import { IsArray, IsDefined, ValidateNested } from 'class-validator'; -import { - ERole, - RoleNameObject, - RoleNamePermsObject -} from '../../entities/role.entity'; +import { IsArray } from 'class-validator'; +import { ERole, SimpleRole } from '../../entities/role.entity'; +import { IsNested } from '../../validators/nested.validator'; import { IsPosInt } from '../../validators/positive-int.validator'; +import { IsRoleName } from '../../validators/role.validators'; import { IsStringList } from '../../validators/string-list.validator'; // RoleInfo -export class RoleInfoRequest extends RoleNameObject {} +export class RoleInfoRequest { + @IsRoleName() + name: string; +} export class RoleInfoResponse extends ERole {} // RoleList export class RoleListResponse { @IsArray() - @IsDefined() - @ValidateNested() - @Type(() => ERole) + @IsNested(ERole) roles: ERole[]; @IsPosInt() @@ -25,32 +23,31 @@ export class RoleListResponse { } // RoleUpdate -export class RoleUpdateRequest extends RoleNamePermsObject {} +export class RoleUpdateRequest extends SimpleRole {} export class RoleUpdateResponse extends ERole {} // RoleCreate -export class RoleCreateRequest extends RoleNamePermsObject {} +export class RoleCreateRequest extends SimpleRole {} export class RoleCreateResponse extends ERole {} // RoleDelete -export class RoleDeleteRequest extends RoleNameObject {} +export class RoleDeleteRequest { + @IsRoleName() + name: string; +} export class RoleDeleteResponse extends ERole {} // SpecialRoles export class SpecialRolesResponse { - @IsDefined() @IsStringList() SoulBoundRoles: string[]; - @IsDefined() @IsStringList() ImmutableRoles: string[]; - @IsDefined() @IsStringList() UndeletableRoles: string[]; - @IsDefined() @IsStringList() DefaultRoles: string[]; } diff --git a/shared/src/dto/api/syspref.dto.ts b/shared/src/dto/api/syspref.dto.ts index f6b899a..9744f3d 100644 --- a/shared/src/dto/api/syspref.dto.ts +++ b/shared/src/dto/api/syspref.dto.ts @@ -1,36 +1,21 @@ -import { Type } from 'class-transformer'; import { - IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested + IsArray } from 'class-validator'; +import { IsNested } from '../../validators/nested.validator'; import { IsPosInt } from '../../validators/positive-int.validator'; -import { IsSysPrefValue } from '../../validators/syspref.validator'; -import { PrefValueType, PrefValueTypes, PrefValueTypeStrings } from '../preferences.dto'; +import { IsPrefValue } from '../../validators/pref-value.validator'; +import { DecodedSysPref, PrefValueType } from '../preferences.dto'; -export class SysPreferenceBaseResponse { - @IsNotEmpty() - @IsString() - key: string; - - @IsNotEmpty() - @IsSysPrefValue() - value: PrefValueType; - - @IsNotEmpty() - @IsEnum(PrefValueTypes) - type: PrefValueTypeStrings; -} // Get Syspreference // Request is done via url parameters -export class GetSyspreferenceResponse extends SysPreferenceBaseResponse {} +export class GetSyspreferenceResponse extends DecodedSysPref {} // Get syspreferences export class MultipleSysPreferencesResponse { @IsArray() - @IsNotEmpty() - @ValidateNested({ each: true }) - @Type(() => SysPreferenceBaseResponse) - preferences: SysPreferenceBaseResponse[]; + @IsNested(DecodedSysPref) + preferences: DecodedSysPref[]; @IsPosInt() total: number; @@ -38,10 +23,9 @@ export class MultipleSysPreferencesResponse { // Update Syspreference export class UpdateSysPreferenceRequest { - @IsNotEmpty() - @IsSysPrefValue() + @IsPrefValue() value: PrefValueType; } -export class UpdateSysPreferenceResponse extends SysPreferenceBaseResponse {} +export class UpdateSysPreferenceResponse extends DecodedSysPref {} diff --git a/shared/src/dto/api/user.dto.ts b/shared/src/dto/api/user.dto.ts index 009f3e1..8100993 100644 --- a/shared/src/dto/api/user.dto.ts +++ b/shared/src/dto/api/user.dto.ts @@ -1,45 +1,47 @@ -import { Type } from 'class-transformer'; import { - IsDefined, IsJWT, - IsString, - ValidateNested + IsJWT } 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 { IsPlainTextPwd, IsUsername } from '../../validators/user.validators'; // Api // UserLogin -export class UserLoginRequest extends NamePassUser {} +export class UserLoginRequest { + @IsUsername() + username: string; + @IsPlainTextPwd() + password: string; +} export class UserLoginResponse { - @IsString() - @IsDefined() @IsJWT() jwt_token: string; } // UserRegister -export class UserRegisterRequest extends NamePassUser {} +export class UserRegisterRequest { + @IsUsername() + username: string; + @IsPlainTextPwd() + password: string; +} export class UserRegisterResponse extends EUser {} // UserMe export class UserMeResponse { - @IsDefined() - @ValidateNested() - @Type(() => EUser) + @IsNested(EUser) user: EUser; - @IsString() - @IsDefined() @IsJWT() token: string; } // UserMePermissions export class UserMePermissionsResponse { - @IsDefined() @IsStringList() permissions: string[]; } diff --git a/shared/src/dto/api/usermanage.dto.ts b/shared/src/dto/api/usermanage.dto.ts index c23d01e..655a387 100644 --- a/shared/src/dto/api/usermanage.dto.ts +++ b/shared/src/dto/api/usermanage.dto.ts @@ -1,14 +1,10 @@ -import { Type } from 'class-transformer'; -import { - IsArray, - IsDefined, - IsOptional, - ValidateNested -} from 'class-validator'; -import { EUser, NamePassUser } from '../../entities/user.entity'; +import { IsArray, IsOptional } from 'class-validator'; +import { EUser, SimpleUser } from '../../entities/user.entity'; +import { Newable } from '../../types'; +import { IsEntityID } from '../../validators/entity-id.validator'; +import { IsNested } from '../../validators/nested.validator'; import { IsPosInt } from '../../validators/positive-int.validator'; import { IsStringList } from '../../validators/string-list.validator'; -import { IsPlainTextPwd, IsUsername } from '../../validators/user.validators'; import { EntityIDObject } from '../idobject.dto'; // UserList @@ -22,9 +18,7 @@ export class UserListRequest { export class UserListResponse { @IsArray() - @IsDefined() - @ValidateNested() - @Type(() => EUser) + @IsNested(EUser) users: EUser[]; @IsPosInt() @@ -35,11 +29,7 @@ export class UserListResponse { } // UserCreate -export class UserCreateRequest extends NamePassUser { - @IsOptional() - @IsStringList() - roles?: string[]; -} +export class UserCreateRequest extends SimpleUser {} export class UserCreateResponse extends EUser {} // UserDelete @@ -51,33 +41,31 @@ export class UserInfoRequest extends EntityIDObject {} export class UserInfoResponse extends EUser {} // UserUpdate -export class UserUpdateRequest extends EntityIDObject { - @IsOptional() - @IsUsername() - username?: string; +export class UserUpdateRequest extends (SimpleUser as Newable< + Partial +>) { + @IsEntityID() + id: string; @IsOptional() - @IsStringList() - roles?: string[]; + override username?: string; - @IsPlainTextPwd() @IsOptional() - password?: string; + override password?: string; + + @IsOptional() + override roles?: string[]; } - export class UserUpdateResponse extends EUser {} // GetSpecialUsers export class GetSpecialUsersResponse { - @IsDefined() @IsStringList() UndeletableUsersList: string[]; - @IsDefined() @IsStringList() ImmutableUsersList: string[]; - - @IsDefined() + @IsStringList() LockedLoginUsersList: string[]; } diff --git a/shared/src/dto/idobject.dto.ts b/shared/src/dto/idobject.dto.ts index 2d050cb..35558a5 100644 --- a/shared/src/dto/idobject.dto.ts +++ b/shared/src/dto/idobject.dto.ts @@ -1,6 +1,6 @@ -import { EntityID } from '../validators/entity-id.validator'; +import { IsEntityID } from '../validators/entity-id.validator'; export class EntityIDObject { - @EntityID() + @IsEntityID() id: string; } diff --git a/shared/src/dto/jwt.dto.ts b/shared/src/dto/jwt.dto.ts index f30704f..0c072eb 100644 --- a/shared/src/dto/jwt.dto.ts +++ b/shared/src/dto/jwt.dto.ts @@ -1,11 +1,9 @@ -import { Type } from 'class-transformer'; -import { IsDefined, IsInt, IsOptional, ValidateNested } from 'class-validator'; +import { IsInt, IsOptional } from 'class-validator'; import { EUser } from '../entities/user.entity'; +import { IsNested } from '../validators/nested.validator'; export class JwtDataDto { - @IsDefined() - @ValidateNested() - @Type(() => EUser) + @IsNested(EUser) user: EUser; @IsOptional() diff --git a/shared/src/dto/preferences.dto.ts b/shared/src/dto/preferences.dto.ts index ea84f14..6aebcb8 100644 --- a/shared/src/dto/preferences.dto.ts +++ b/shared/src/dto/preferences.dto.ts @@ -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 export type PrefValueType = string | number | boolean; export type PrefValueTypeStrings = '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; +} diff --git a/shared/src/dto/syspreferences.dto.ts b/shared/src/dto/syspreferences.dto.ts index 53e8443..711762e 100644 --- a/shared/src/dto/syspreferences.dto.ts +++ b/shared/src/dto/syspreferences.dto.ts @@ -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 export enum SysPreference { @@ -9,10 +8,3 @@ export enum SysPreference { TestNumber = 'test_number', TestBoolean = 'test_boolean', } - -// Interfaces -export interface InternalSysPrefRepresentation { - key: string; - value: PrefValueType; - type: PrefValueTypeStrings; -} diff --git a/shared/src/dto/usrpreferences.dto.ts b/shared/src/dto/usrpreferences.dto.ts index 132fad7..a87e0a6 100644 --- a/shared/src/dto/usrpreferences.dto.ts +++ b/shared/src/dto/usrpreferences.dto.ts @@ -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 export enum UsrPreference { @@ -7,10 +6,3 @@ export enum UsrPreference { TestBoolean = 'test_boolean', } -// Interfaces -export interface InternalUsrPrefRepresentation { - key: string; - value: PrefValueType; - type: PrefValueTypeStrings; - user: number; -} diff --git a/shared/src/entities/image.entity.ts b/shared/src/entities/image.entity.ts index 16b1861..a9f4402 100644 --- a/shared/src/entities/image.entity.ts +++ b/shared/src/entities/image.entity.ts @@ -1,20 +1,19 @@ -import { Exclude } from 'class-transformer'; -import { IsHash, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { EntityID } from '../validators/entity-id.validator'; +import { IsHash, IsOptional, IsString } from 'class-validator'; +import { IsEntityID } from '../validators/entity-id.validator'; +import { IsNotDefined } from '../validators/not-defined.validator'; export class EImage { - @EntityID() - id: string; + @IsOptional() + @IsEntityID() + id?: string; @IsHash('sha256') hash: string; - // Binary data - @IsOptional() - @Exclude() // Dont send this by default - data?: object; - - @IsNotEmpty() + // Because typescript does not support exact types, we have to do this stupidness + @IsNotDefined() + data: undefined; + @IsString() mime: string; } diff --git a/shared/src/entities/role.entity.ts b/shared/src/entities/role.entity.ts index d828ec3..7ef5785 100644 --- a/shared/src/entities/role.entity.ts +++ b/shared/src/entities/role.entity.ts @@ -1,23 +1,24 @@ -import { IsDefined } from 'class-validator'; -import { EntityID } from '../validators/entity-id.validator'; +import { IsOptional } from 'class-validator'; +import { IsEntityID } from '../validators/entity-id.validator'; import { IsRoleName } from '../validators/role.validators'; import { IsStringList } from '../validators/string-list.validator'; -// This entity is build from multiple smaller enitities -// Theses smaller entities are used in other places - -export class RoleNameObject { +export class SimpleRole { @IsRoleName() name: string; -} -export class RoleNamePermsObject extends RoleNameObject { - @IsDefined() @IsStringList() permissions: string[]; } -export class ERole extends RoleNamePermsObject { - @EntityID() - id: string; +export class ERole { + @IsOptional() + @IsEntityID() + id?: string; + + @IsRoleName() + name: string; + + @IsStringList() + permissions: string[]; } diff --git a/shared/src/entities/syspreference.entity.ts b/shared/src/entities/syspreference.entity.ts index 5fd78c7..1f8a228 100644 --- a/shared/src/entities/syspreference.entity.ts +++ b/shared/src/entities/syspreference.entity.ts @@ -1,15 +1,14 @@ -import { IsNotEmpty, IsString } from 'class-validator'; -import { EntityIDOptional } from '../validators/entity-id.validator'; +import { IsOptional, IsString } from 'class-validator'; +import { IsEntityID } from '../validators/entity-id.validator'; export class ESysPreference { - @EntityIDOptional() + @IsOptional() + @IsEntityID() id?: string; - @IsNotEmpty() @IsString() key: string; - @IsNotEmpty() @IsString() value: string; } diff --git a/shared/src/entities/user.entity.ts b/shared/src/entities/user.entity.ts index cd7b27a..893c77c 100644 --- a/shared/src/entities/user.entity.ts +++ b/shared/src/entities/user.entity.ts @@ -1,37 +1,32 @@ -import { Exclude } from 'class-transformer'; -import { IsDefined, IsOptional, IsString } from 'class-validator'; -import { EntityID } from '../validators/entity-id.validator'; +import { IsOptional } from 'class-validator'; +import { IsEntityID } from '../validators/entity-id.validator'; +import { IsNotDefined } from '../validators/not-defined.validator'; import { IsStringList } from '../validators/string-list.validator'; import { IsPlainTextPwd, IsUsername } from '../validators/user.validators'; -// This entity is build from multiple smaller enitities -// Theses smaller entities are used in other places - -export class UsernameUser { +export class SimpleUser { @IsUsername() username: string; -} -// This is a simple user object with just the username and unhashed password -export class NamePassUser extends UsernameUser { @IsPlainTextPwd() password: string; -} -// Add a user object with just the username and roles for jwt -export class NameRolesUser extends UsernameUser { - @IsDefined() @IsStringList() roles: string[]; } -// Actual entity that goes in the db -export class EUser extends NameRolesUser { - @EntityID() - id: string; - +export class EUser { @IsOptional() - @Exclude() - @IsString() - password?: string; + @IsEntityID() + id?: string; + + @IsUsername() + username: string; + + @IsStringList() + roles: string[]; + + // Because typescript does not support exact types, we have to do this stupidness + @IsNotDefined() + hashedPassword: undefined; } diff --git a/shared/src/entities/usrpreference.ts b/shared/src/entities/usrpreference.ts index 7245fee..c802625 100644 --- a/shared/src/entities/usrpreference.ts +++ b/shared/src/entities/usrpreference.ts @@ -1,20 +1,18 @@ -import { IsDefined, IsNotEmpty, IsString } from 'class-validator'; -import { EntityIDOptional } from '../validators/entity-id.validator'; +import { IsOptional, IsString } from 'class-validator'; +import { IsEntityID } from '../validators/entity-id.validator'; import { IsPosInt } from '../validators/positive-int.validator'; export class EUsrPreference { - @EntityIDOptional() + @IsOptional() + @IsEntityID() id?: string; - @IsNotEmpty() @IsString() key: string; - @IsNotEmpty() @IsString() value: string; - @IsDefined() @IsPosInt() userId: number; } diff --git a/shared/src/util/decorator.ts b/shared/src/util/decorator.ts index e2836b2..b35f051 100644 --- a/shared/src/util/decorator.ts +++ b/shared/src/util/decorator.ts @@ -3,7 +3,7 @@ type FCDecorator = MethodDecorator & ClassDecorator; export function CombineFCDecorators(...decorators: FCDecorator[]) { return (target: any, key: string, descriptor: PropertyDescriptor) => { decorators.forEach(decorator => { - decorator(target, key, descriptor); + decorator(target, key, descriptor as any); }); } } diff --git a/shared/src/util/validate.ts b/shared/src/util/validate.ts index 1cbc3a6..3e36ba6 100644 --- a/shared/src/util/validate.ts +++ b/shared/src/util/validate.ts @@ -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 // So now we have to do it this way -export const ValidateOptions = { - disableErrorMessages: true, +export const ValidateOptions: ValidatorOptions = { forbidNonWhitelisted: true, forbidUnknownValues: true, stopAtFirstError: true, whitelist: true, + strictGroups: true, }; export const strictValidate = (object: object) => diff --git a/shared/src/validators/entity-id.validator.ts b/shared/src/validators/entity-id.validator.ts index 9a5fda9..5e65623 100644 --- a/shared/src/validators/entity-id.validator.ts +++ b/shared/src/validators/entity-id.validator.ts @@ -1,5 +1,4 @@ -import { IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { IsUUID } from 'class-validator'; import { CombinePDecorators } from '../util/decorator'; -export const EntityID = CombinePDecorators(IsNotEmpty(), IsUUID('4')); -export const EntityIDOptional = CombinePDecorators(IsOptional(), IsUUID('4')); +export const IsEntityID = CombinePDecorators(IsUUID('4')); diff --git a/shared/src/validators/nested.validator.ts b/shared/src/validators/nested.validator.ts new file mode 100644 index 0000000..8ce0b21 --- /dev/null +++ b/shared/src/validators/nested.validator.ts @@ -0,0 +1,15 @@ +import { Type } from 'class-transformer'; +import { IsNotEmpty, ValidateNested } from 'class-validator'; +import { Newable } from '../types'; + +export const IsNested = (nestedClass: Newable) => { + 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); + }; +}; diff --git a/shared/src/validators/not-defined.validator.ts b/shared/src/validators/not-defined.validator.ts new file mode 100644 index 0000000..949a65e --- /dev/null +++ b/shared/src/validators/not-defined.validator.ts @@ -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); + }; +} diff --git a/shared/src/validators/positive-int.validator.ts b/shared/src/validators/positive-int.validator.ts index 05b4cc5..6d352d3 100644 --- a/shared/src/validators/positive-int.validator.ts +++ b/shared/src/validators/positive-int.validator.ts @@ -1,4 +1,4 @@ -import { IsDefined, IsInt, Min } from 'class-validator'; +import { IsInt, Min } from 'class-validator'; import { CombinePDecorators } from '../util/decorator'; -export const IsPosInt = CombinePDecorators(IsInt(), Min(0), IsDefined()); +export const IsPosInt = CombinePDecorators(IsInt(), Min(0)); diff --git a/shared/src/validators/syspref.validator.ts b/shared/src/validators/pref-value.validator.ts similarity index 63% rename from shared/src/validators/syspref.validator.ts rename to shared/src/validators/pref-value.validator.ts index a9016d8..7e7d8df 100644 --- a/shared/src/validators/syspref.validator.ts +++ b/shared/src/validators/pref-value.validator.ts @@ -1,20 +1,20 @@ import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; import { PrefValueTypes } from '../dto/preferences.dto'; -export function isSysPrefValue(value: any, args: ValidationArguments) { +export function isPrefValue(value: any, args: ValidationArguments) { const type = typeof value; return PrefValueTypes.includes(type); } -export function IsSysPrefValue(validationOptions?: ValidationOptions) { +export function IsPrefValue(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ - name: 'isSysPrefValue', + name: 'isPrefValue', target: object.constructor, propertyName: propertyName, - options: validationOptions, + options: validationOptions ?? {}, validator: { - validate: isSysPrefValue, + validate: isPrefValue, }, }); }; diff --git a/shared/src/validators/role.validators.ts b/shared/src/validators/role.validators.ts index 02b4332..ac114ca 100644 --- a/shared/src/validators/role.validators.ts +++ b/shared/src/validators/role.validators.ts @@ -1,8 +1,7 @@ -import { IsAlphanumeric, IsNotEmpty, IsString, Length } from 'class-validator'; +import { IsAlphanumeric, IsString, Length } from 'class-validator'; import { CombinePDecorators } from '../util/decorator'; export const IsRoleName = CombinePDecorators( - IsNotEmpty(), IsString(), Length(4, 32), IsAlphanumeric(), diff --git a/shared/src/validators/string-list.validator.ts b/shared/src/validators/string-list.validator.ts index 132b4d7..41afe2b 100644 --- a/shared/src/validators/string-list.validator.ts +++ b/shared/src/validators/string-list.validator.ts @@ -1,12 +1,9 @@ import { - IsArray, - IsNotEmpty, - IsString + IsArray, IsString } from 'class-validator'; import { CombinePDecorators } from '../util/decorator'; export const IsStringList = CombinePDecorators( IsArray(), IsString({ each: true }), - IsNotEmpty({ each: true }), ); diff --git a/shared/src/validators/user.validators.ts b/shared/src/validators/user.validators.ts index d833cf9..dd8fac4 100644 --- a/shared/src/validators/user.validators.ts +++ b/shared/src/validators/user.validators.ts @@ -1,18 +1,16 @@ -import { IsAlphanumeric, IsNotEmpty, IsString, Length } from 'class-validator'; +import { IsAlphanumeric, IsString, Length } from 'class-validator'; import { CombinePDecorators } from '../util/decorator'; // Match this with user validators in frontend // (Frontend is not security focused, but it tells the user what is wrong) export const IsUsername = CombinePDecorators( - IsNotEmpty(), IsString(), Length(4, 32), IsAlphanumeric(), ); export const IsPlainTextPwd = CombinePDecorators( - IsNotEmpty(), IsString(), Length(4, 1024), );