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 { 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<B extends true | undefined = undefined>(

View File

@@ -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);
}

View File

@@ -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<InternalSysPrefRepresentation> {
): AsyncFailable<DecodedSysPref> {
// 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<InternalSysPrefRepresentation> {
public async getPreference(key: string): AsyncFailable<DecodedSysPref> {
// 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<DecodedSysPref[]> {
// 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<InternalSysPrefRepresentation> {
): AsyncFailable<DecodedSysPref> {
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<InternalSysPrefRepresentation> {
): Failable<DecodedSysPref> {
const key = this.validatePrefKey(preference.key);
if (HasFailed(key)) return key;

View File

@@ -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<EUserBackend> {
@@ -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');

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 })

View File

@@ -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;
}

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 { 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<UserMeResponse> {
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<UserMePermissionsResponse> {
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());

View File

@@ -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<UserDeleteResponse> {
async delete(@Body() body: UserDeleteRequest): Promise<UserDeleteResponse> {
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')

View File

@@ -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);
}
}

View File

@@ -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<SysPreferenceBaseResponse[]>;
preferences: Observable<DecodedSysPref[]>;
constructor(sysprefService: SysPrefService) {
this.preferences = sysprefService.live;

View File

@@ -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<PrefValueType>();

View File

@@ -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',

View File

@@ -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<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)
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);

View File

@@ -1,7 +1,5 @@
import {
IsBoolean,
IsDefined,
IsInt,
IsBoolean, IsInt,
IsNotEmpty,
IsString,
Max,
@@ -10,21 +8,17 @@ import {
class BaseApiResponse<T extends Object, W extends boolean> {
@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<T extends Object> extends BaseApiResponse<
export class ApiErrorData {
@IsString()
@IsNotEmpty()
message: string;
}
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';
export class InfoResponse {
@IsBoolean()
@IsDefined()
production: boolean;
@IsBoolean()
@IsDefined()
demo: boolean;
@IsDefined()
@IsString()
@IsSemVer()
version: string;
}
// AllPermissions
export class AllPermissionsResponse {
@IsDefined()
@IsStringList()
permissions: string[];
}

View File

@@ -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[];
}

View File

@@ -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 {}

View File

@@ -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[];
}

View File

@@ -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<SimpleUser>
>) {
@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[];
}

View File

@@ -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;
}

View File

@@ -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()

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
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;
}

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
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;
}

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
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;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
});
}
}

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
// 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) =>

View File

@@ -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'));

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';
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 { 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,
},
});
};

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';
export const IsRoleName = CombinePDecorators(
IsNotEmpty(),
IsString(),
Length(4, 32),
IsAlphanumeric(),

View File

@@ -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 }),
);

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';
// 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),
);