switch to zod

This commit is contained in:
rubikscraft 2022-04-04 10:36:59 +02:00
parent bcd427f5a7
commit 380b9d3456
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
65 changed files with 867 additions and 864 deletions

View file

@ -29,8 +29,6 @@
"@nestjs/serve-static": "^2.2.2", "@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^8.0.3", "@nestjs/typeorm": "^8.0.3",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"fastify-helmet": "^7.0.1", "fastify-helmet": "^7.0.1",
"fastify-multipart": "^5.3.1", "fastify-multipart": "^5.3.1",
"fastify-static": "^4.6.1", "fastify-static": "^4.6.1",
@ -44,7 +42,8 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"typeorm": "0.3.4" "typeorm": "0.3.4",
"zod": "^3.14.3"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^8.2.4", "@nestjs/cli": "^8.2.4",

View file

@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer'; import { ERoleSchema } from 'picsur-shared/dist/entities/role.entity';
import { import {
AsyncFailable, AsyncFailable,
Fail, Fail,
@ -8,7 +8,6 @@ import {
HasSuccess HasSuccess
} from 'picsur-shared/dist/types'; } from 'picsur-shared/dist/types';
import { makeUnique } from 'picsur-shared/dist/util/unique'; import { makeUnique } from 'picsur-shared/dist/util/unique';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Permissions } from '../../models/dto/permissions.dto'; import { Permissions } from '../../models/dto/permissions.dto';
import { import {
@ -171,13 +170,13 @@ export class RolesService {
if (typeof role === 'string') { if (typeof role === 'string') {
return await this.findOne(role); return await this.findOne(role);
} else { } else {
role = plainToClass(ERoleBackend, role); const result = ERoleSchema.safeParse(role);
const errors = await strictValidate(role); if (!result.success) {
if (errors.length > 0) { this.logger.warn(result.error);
this.logger.warn(errors);
return Fail('Invalid role'); return Fail('Invalid role');
} }
return role; // This is safe
return result.data as ERoleBackend;
} }
} }
} }

View file

@ -6,13 +6,13 @@ import {
PrefValueTypeStrings PrefValueTypeStrings
} from 'picsur-shared/dist/dto/preferences.dto'; } 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 { ESysPreferenceSchema } from 'picsur-shared/dist/entities/syspreference.entity';
import { import {
AsyncFailable, AsyncFailable,
Fail, Fail,
Failable, Failable,
HasFailed HasFailed
} from 'picsur-shared/dist/types'; } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { import {
SysPreferenceList, SysPreferenceList,
@ -82,14 +82,14 @@ export class SysPreferenceService {
} }
// Validate // Validate
const errors = await strictValidate(foundSysPreference); const result = ESysPreferenceSchema.safeParse(foundSysPreference);
if (errors.length > 0) { if (!result.success) {
this.logger.warn(errors); this.logger.warn(result.error);
return Fail('Invalid preference'); return Fail('Invalid preference');
} }
// Return // Return
return this.retrieveConvertedValue(foundSysPreference); return this.retrieveConvertedValue(result.data);
} }
public async getStringPreference(key: string): AsyncFailable<string> { public async getStringPreference(key: string): AsyncFailable<string> {
@ -182,13 +182,13 @@ export class SysPreferenceService {
verifySysPreference.value = validatedValue; verifySysPreference.value = validatedValue;
// It should already be valid, but these two validators might go out of sync // It should already be valid, but these two validators might go out of sync
const errors = await strictValidate(verifySysPreference); const result = ESysPreferenceSchema.safeParse(verifySysPreference);
if (errors.length > 0) { if (!result.success) {
this.logger.warn(errors); this.logger.warn(result.error);
return Fail('Invalid preference'); return Fail('Invalid preference');
} }
return verifySysPreference; return result.data;
} }
private validatePrefKey(key: string): Failable<SysPreference> { private validatePrefKey(key: string): Failable<SysPreference> {

View file

@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { plainToClass } from 'class-transformer';
import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto'; import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto';
import { import {
AsyncFailable, AsyncFailable,
@ -79,10 +78,7 @@ export class UsersService {
} }
try { try {
// Makes sure we can return the id return await this.usersRepository.remove(userToModify);
const cloned = plainToClass(EUserBackend, userToModify);
await this.usersRepository.remove(userToModify);
return cloned;
} catch (e: any) { } catch (e: any) {
return Fail(e?.message); return Fail(e?.message);
} }
@ -158,8 +154,7 @@ export class UsersService {
return Fail(e?.message); return Fail(e?.message);
} }
// Strips unwanted data return userToModify;
return plainToClass(EUserBackend, userToModify);
} }
// Authentication // Authentication
@ -176,36 +171,28 @@ export class UsersService {
return Fail('Wrong username'); return Fail('Wrong username');
} }
if (!(await bcrypt.compare(password, user.hashedPassword))) 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 ?? '');
} }
// Listing // Listing
public async findByUsername<B extends true | undefined = undefined>( public async findByUsername(
username: string, username: string,
// Also fetch fields that aren't normally sent to the client // Also fetch fields that aren't normally sent to the client
// (e.g. hashed password) // (e.g. hashed password)
getPrivate?: B, getPrivate: boolean = false,
): AsyncFailable< ): AsyncFailable<EUserBackend> {
B extends undefined ? EUserBackend : Required<EUserBackend>
> {
try { try {
const found = await this.usersRepository.findOne({ const found = await this.usersRepository.findOne({
where: { username }, where: { username },
...(getPrivate select: getPrivate ? GetCols(this.usersRepository) : undefined,
? {
select: GetCols(this.usersRepository),
}
: {}),
}); });
if (!found) return Fail('User not found'); if (!found) return Fail('User not found');
return found as B extends undefined return found;
? EUserBackend
: Required<EUserBackend>;
} catch (e: any) { } catch (e: any) {
return Fail(e?.message); return Fail(e?.message);
} }

View file

@ -1,12 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Newable } from 'picsur-shared/dist/types';
// Since pipes dont have direct access to the request object, we need this decorator to inject it // Since pipes dont have direct access to the request object, we need this decorator to inject it
export const InjectRequest = createParamDecorator( export const InjectRequest = createParamDecorator(
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => { async (data: any, ctx: ExecutionContext) => {
return { return ctx.switchToHttp().getRequest();
req: ctx.switchToHttp().getRequest(),
data,
};
}, },
); );

View file

@ -1,9 +1,8 @@
import { Newable } from 'picsur-shared/dist/types';
import { InjectRequest } from './injectrequest.decorator'; import { InjectRequest } from './injectrequest.decorator';
import { MultiPartPipe } from './multipart.pipe'; import { MultiPartPipe } from './multipart.pipe';
import { PostFilePipe } from './postfile.pipe'; import { PostFilePipe } from './postfile.pipe';
export const PostFile = () => InjectRequest(PostFilePipe); export const PostFile = () => InjectRequest(PostFilePipe);
export const MultiPart = <T extends Object>(data: Newable<T>) => export const MultiPart = () =>
InjectRequest(data, MultiPartPipe); InjectRequest(MultiPartPipe);

View file

@ -1,18 +1,19 @@
import { import {
ArgumentMetadata,
BadRequestException, BadRequestException,
Injectable, Injectable,
InternalServerErrorException,
Logger, Logger,
PipeTransform, PipeTransform,
Scope Scope
} from '@nestjs/common'; } from '@nestjs/common';
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { MultipartFields, MultipartFile } from 'fastify-multipart'; import { MultipartFields, MultipartFile } from 'fastify-multipart';
import { Newable } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { MultipartConfigService } from '../config/early/multipart.config.service'; import { MultipartConfigService } from '../config/early/multipart.config.service';
import { import {
MultiPartFieldDto, CreateMultiPartFieldDto,
MultiPartFileDto CreateMultiPartFileDto
} from '../models/requests/multipart.dto'; } from '../models/requests/multipart.dto';
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
@ -21,16 +22,17 @@ export class MultiPartPipe implements PipeTransform {
constructor(private multipartConfigService: MultipartConfigService) {} constructor(private multipartConfigService: MultipartConfigService) {}
async transform<T extends Object>({ async transform<T extends Object>(
req, req: FastifyRequest,
data, metadata: ArgumentMetadata,
}: { ) {
req: FastifyRequest; let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
data: Newable<T>; if (!zodSchema) {
}) { this.logger.warn('Invalid scheme on multipart body');
// Data should be a validatable class constructor throw new InternalServerErrorException('Invalid scheme on backend');
const dtoClass = new data(); }
let multipartData = {};
if (!req.isMultipart()) throw new BadRequestException('Invalid file'); if (!req.isMultipart()) throw new BadRequestException('Invalid file');
// Fetch all fields from the request // Fetch all fields from the request
@ -56,11 +58,11 @@ export class MultiPartPipe implements PipeTransform {
// Use the value property to differentiate between a field and a file // Use the value property to differentiate between a field and a file
// And then put the value into the correct property on the validatable class // And then put the value into the correct property on the validatable class
if ((fields[key] as any).value) { if ((fields[key] as any).value) {
(dtoClass as any)[key] = new MultiPartFieldDto( (multipartData as any)[key] = CreateMultiPartFieldDto(
fields[key] as MultipartFile, fields[key] as MultipartFile,
); );
} else { } else {
(dtoClass as any)[key] = new MultiPartFileDto( (multipartData as any)[key] = CreateMultiPartFileDto(
fields[key] as MultipartFile, fields[key] as MultipartFile,
new BadRequestException('Invalid file'), new BadRequestException('Invalid file'),
); );
@ -68,12 +70,12 @@ export class MultiPartPipe implements PipeTransform {
} }
// Now validate the class we made, if any properties were invalid, it will error here // Now validate the class we made, if any properties were invalid, it will error here
const errors = await strictValidate(dtoClass); const result = zodSchema.safeParse(multipartData);
if (errors.length > 0) { if (!result.success) {
this.logger.warn(errors); this.logger.warn(result.error);
throw new BadRequestException('Invalid file'); throw new BadRequestException('Invalid file');
} }
return dtoClass; return result.data;
} }
} }

View file

@ -2,25 +2,82 @@ import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
Injectable, Injectable,
NestInterceptor InternalServerErrorException,
Logger,
NestInterceptor,
Optional
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiResponse } from 'picsur-shared/dist/dto/api/api.dto'; import { Reflector } from '@nestjs/core';
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
// This interceptor will neatly wrap any json response made within nest // This interceptor will neatly wrap any json response made within nest
export interface ZodValidationInterceptorOptions {
strict?: boolean;
}
@Injectable() @Injectable()
export class SuccessInterceptor<T> implements NestInterceptor { export class SuccessInterceptor<T> implements NestInterceptor {
private readonly logger = new Logger();
// TODO: make work
private strict: boolean;
constructor(
private reflector: Reflector,
@Optional() options?: ZodValidationInterceptorOptions,
) {
this.strict = options?.strict ?? true;
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
map((data) => { map((data) => {
if (data instanceof Buffer) { if (data instanceof Buffer) {
return data; return data;
} else if (typeof data === 'object') { } else if (typeof data === 'object') {
// TODO: mabye validate outgoing requests? const validated = this.validate(context, data);
return this.createResponse(context, validated);
} else {
return data;
}
}),
);
}
private validate(context: ExecutionContext, data: any): any {
const schemaStatic = this.reflector.get<ZodDtoStatic>(
'returns',
context.getHandler(),
);
if (!schemaStatic) {
this.logger.warn(
`No zodSchema found on handler ${context.getHandler().name}`,
);
throw new InternalServerErrorException("Couldn't find schema");
}
let schema = schemaStatic.zodSchema;
const parseResult = schema.safeParse(data);
if (!parseResult.success) {
this.logger.warn(
`Function ${context.getHandler().name} failed validation`,
);
throw new InternalServerErrorException(
'Server produced invalid response',
);
}
return parseResult.data;
}
private createResponse(context: ExecutionContext, data: any): any {
const status = context.switchToHttp().getResponse().statusCode; const status = context.switchToHttp().getResponse().statusCode;
const response: ApiResponse<any> = { const response = {
success: true, success: true,
statusCode: status, statusCode: status,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -29,10 +86,5 @@ export class SuccessInterceptor<T> implements NestInterceptor {
}; };
return response; return response;
} else {
return data;
}
}),
);
} }
} }

View file

@ -0,0 +1,47 @@
/**
* This file was originally taken directly from:
* https://github.com/anatine/zod-plugins/blob/main/libs/zod-nestjs/src/lib/zod-validation-pipe.ts
*/
import {
ArgumentMetadata,
BadRequestException,
Injectable,
Optional,
PipeTransform
} from '@nestjs/common';
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
export interface ZodValidationPipeOptions {
strict?: boolean;
validateCustom?: boolean;
}
@Injectable()
export class ZodValidationPipe implements PipeTransform {
private strict: boolean;
private validateCustom: boolean;
constructor(@Optional() options?: ZodValidationPipeOptions) {
this.strict = options?.strict ?? true;
this.validateCustom = options?.validateCustom ?? false;
}
public transform(value: unknown, metadata: ArgumentMetadata): unknown {
if (!this.validateCustom && metadata.type === 'custom') return value;
let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
if (zodSchema) {
const parseResult = zodSchema.safeParse(value);
if (!parseResult.success) {
throw new BadRequestException();
}
return parseResult.data;
}
return value;
}
}

View file

@ -1,4 +1,3 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core'; import { NestFactory, Reflector } from '@nestjs/core';
import { import {
FastifyAdapter, FastifyAdapter,
@ -6,12 +5,12 @@ import {
} from '@nestjs/platform-fastify'; } from '@nestjs/platform-fastify';
import fastifyHelmet from 'fastify-helmet'; import fastifyHelmet from 'fastify-helmet';
import * as multipart from 'fastify-multipart'; import * as multipart from 'fastify-multipart';
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { UsersService } from './collections/userdb/userdb.service'; import { UsersService } from './collections/userdb/userdb.service';
import { HostConfigService } from './config/early/host.config.service'; import { HostConfigService } from './config/early/host.config.service';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter'; import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor'; import { SuccessInterceptor } from './layers/success/success.interceptor';
import { ZodValidationPipe } from './layers/validate/zod-validator.pipe';
import { PicsurLoggerService } from './logger/logger.service'; import { PicsurLoggerService } from './logger/logger.service';
import { MainAuthGuard } from './managers/auth/guards/main.guard'; import { MainAuthGuard } from './managers/auth/guards/main.guard';
import { HelmetOptions } from './security'; import { HelmetOptions } from './security';
@ -31,14 +30,11 @@ async function bootstrap() {
bufferLogs: true, bufferLogs: true,
}, },
); );
// app.enableCors({
// origin: 'self'
// });
// Configure nest app // Configure nest app
app.useGlobalFilters(new MainExceptionFilter()); app.useGlobalFilters(new MainExceptionFilter());
app.useGlobalInterceptors(new SuccessInterceptor()); app.useGlobalInterceptors(new SuccessInterceptor(app.get(Reflector)));
app.useGlobalPipes(new ValidationPipe(ValidateOptions)); app.useGlobalPipes(new ZodValidationPipe());
app.useGlobalGuards( app.useGlobalGuards(
new MainAuthGuard(app.get(Reflector), app.get(UsersService)), new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
); );

View file

@ -1,10 +1,8 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { instanceToPlain, plainToClass } from 'class-transformer'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable() @Injectable()
export class AuthManagerService { export class AuthManagerService {
@ -12,20 +10,20 @@ export class AuthManagerService {
constructor(private jwtService: JwtService) {} constructor(private jwtService: JwtService) {}
async createToken(user: EUserBackend): AsyncFailable<string> { async createToken(user: EUser): AsyncFailable<string> {
const jwtData: JwtDataDto = plainToClass(JwtDataDto, { const jwtData = {
user, user,
}); };
// Validate to be sure, this makes client experience better // Validate to be sure, this makes client experience better
// in case of any failures // in case of any failures
const errors = await strictValidate(jwtData); const result = JwtDataSchema.safeParse(jwtData);
if (errors.length > 0) { if (!result.success) {
return Fail('Invalid JWT: ' + errors); return Fail('Invalid JWT: ' + result.error);
} }
try { try {
return await this.jwtService.signAsync(instanceToPlain(jwtData)); return await this.jwtService.signAsync(result.data);
} catch (e) { } catch (e) {
return Fail("Couldn't create JWT: " + e); return Fail("Couldn't create JWT: " + e);
} }

View file

@ -1,14 +1,8 @@
import { import { Inject, Injectable, Logger } from '@nestjs/common';
Inject,
Injectable,
Logger
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
import { strictValidate } from 'picsur-shared/dist/util/validate'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
@ -23,17 +17,14 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
}); });
} }
async validate(payload: any): Promise<EUserBackend | false> { async validate(payload: any): Promise<EUser | false> {
const jwt = plainToClass(JwtDataDto, payload); const result = JwtDataSchema.safeParse(payload);
if (!result.success) {
// This then validates the data inside the jwt token this.logger.error('JWT could not be parsed: ' + result.error);
const errors = await strictValidate(jwt);
if (errors.length > 0) {
this.logger.warn(errors);
return false; return false;
} }
// And return the user // And return the user
return jwt.user; return result.data.user;
} }
} }

View file

@ -1,9 +1,10 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local'; import { Strategy } from 'passport-local';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { EUserBackend } from '../../../models/entities/user.entity'; import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
@Injectable() @Injectable()
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') { export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
@ -11,16 +12,13 @@ export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
super(); super();
} }
async validate( async validate(username: string, password: string): AsyncFailable<EUser> {
username: string,
password: string,
): AsyncFailable<EUserBackend> {
// All this does is call the usersservice authenticate for authentication // All this does is call the usersservice authenticate for authentication
const user = await this.usersService.authenticate(username, password); const user = await this.usersService.authenticate(username, password);
if (HasFailed(user)) { if (HasFailed(user)) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
return user; return EUserBackend2EUser(user);
} }
} }

View file

@ -7,12 +7,10 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer'; import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity';
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types'; import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { Permissions } from '../../../models/dto/permissions.dto'; import { Permissions } from '../../../models/dto/permissions.dto';
import { EUserBackend } from '../../../models/entities/user.entity';
import { isPermissionsArray } from '../../../models/validators/permissions.validator'; import { isPermissionsArray } from '../../../models/validators/permissions.validator';
// This guard extends both the jwt authenticator and the guest authenticator // This guard extends both the jwt authenticator and the guest authenticator
@ -84,16 +82,15 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
return permissions; return permissions;
} }
private async validateUser(user: EUserBackend): Promise<EUserBackend> { private async validateUser(user: EUser): Promise<EUser> {
const userClass = plainToClass(EUserBackend, user); const result = EUserSchema.safeParse(user);
const errors = await strictValidate(userClass); if (!result.success) {
this.logger.warn(
if (errors.length > 0) { `Invalid user object, where it should always be valid: ${result.error}`,
this.logger.error(
'Invalid user object, where it should always be valid: ' + errors,
); );
throw new InternalServerErrorException(); throw new InternalServerErrorException();
} }
return userClass;
return result.data;
} }
} }

View file

@ -1,23 +1,27 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { EImageSchema } 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';
import { z } from 'zod';
const OverriddenEImageSchema = EImageSchema.omit({ data: true }).merge(
z.object({
data: z.any(),
}),
);
type OverriddenEImage = z.infer<typeof OverriddenEImageSchema>;
@Entity() @Entity()
export class EImageBackend extends EImage { export class EImageBackend implements OverriddenEImage {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
override id?: string; id?: string;
@Index() @Index()
@Column({ unique: true, nullable: false }) @Column({ unique: true, nullable: false })
override hash: string; hash: string;
@Column({ nullable: false }) @Column({ nullable: false })
override mime: string; mime: string;
// Binary data // Binary data
@Column({ type: 'bytea', nullable: false, select: false }) @Column({ type: 'bytea', nullable: false, select: false })
@IsOptional() data?: Buffer;
@IsNotEmpty()
// @ts-ignore
override data?: Buffer;
} }

View file

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

View file

@ -2,14 +2,14 @@ import { ESysPreference } from 'picsur-shared/dist/entities/syspreference.entity
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class ESysPreferenceBackend extends ESysPreference { export class ESysPreferenceBackend implements ESysPreference {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn('uuid')
override id?: string; id?: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })
override key: string; key: string;
@Column({ nullable: false }) @Column({ nullable: false })
override value: string; value: string;
} }

View file

@ -1,25 +1,27 @@
import { IsOptional, IsString } from 'class-validator'; import { EUserSchema } 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';
import { z } from 'zod';
// Different data for public and private // Different data for public and private
const OverriddenEUserSchema = EUserSchema.omit({ hashedPassword: true }).merge(
z.object({
hashedPassword: z.string().optional(),
}),
);
type OverriddenEUser = z.infer<typeof OverriddenEUserSchema>;
@Entity() @Entity()
export class EUserBackend extends EUser { export class EUserBackend implements OverriddenEUser {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn('uuid', {})
override id?: string; id?: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })
override username: string; username: string;
@Column('text', { nullable: false, array: true }) @Column('text', { nullable: false, array: true })
override roles: string[]; roles: string[];
@Column({ nullable: false, select: false }) @Column({ nullable: false, select: false })
@IsOptional() hashedPassword?: string;
@IsString()
// @ts-ignore
override hashedPassword?: string;
} }

View file

@ -1,18 +1,18 @@
import { EUsrPreference } from 'picsur-shared/dist/entities/usrpreference'; import { EUsrPreference } from 'picsur-shared/dist/entities/usrpreference';
import { Column, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Index, PrimaryGeneratedColumn } from 'typeorm';
export class EUsrPreferenceBackend extends EUsrPreference { export class EUsrPreferenceBackend implements EUsrPreference {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn('uuid')
override id?: string; id?: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })
override key: string; key: string;
@Column({ nullable: false }) @Column({ nullable: false })
override value: string; value: string;
@Index() @Index()
@Column({ nullable: false }) @Column({ nullable: false })
override userId: number; userId: number;
} }

View file

@ -1,7 +1,7 @@
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { EUserBackend } from '../entities/user.entity'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
// Add typing to FastifyRequest to make using the user object easier // Add typing to FastifyRequest to make using the user object easier
export default interface AuthFasityRequest extends FastifyRequest { export default interface AuthFasityRequest extends FastifyRequest {
user: EUserBackend; user: EUser;
} }

View file

@ -1,8 +1,9 @@
import { IsMultiPartFile } from '../validators/multipart.validator'; import { createZodDto } from 'picsur-shared/dist/util/create-zod-dto';
import { MultiPartFileDto } from './multipart.dto'; import { z } from 'zod';
import { MultiPartFileDtoSchema } from './multipart.dto';
// A validation class for form based file upload of an image // A validation class for form based file upload of an image
export class ImageUploadDto { export const ImageUploadDtoSchema = z.object({
@IsMultiPartFile() image: MultiPartFileDtoSchema,
image: MultiPartFileDto });
} export class ImageUploadDto extends createZodDto(ImageUploadDtoSchema) {}

View file

@ -1,64 +1,48 @@
import { BusboyFileStream } from '@fastify/busboy';
import { HttpException } from '@nestjs/common'; import { HttpException } from '@nestjs/common';
import { IsDefined, IsNotEmpty, IsString } from 'class-validator';
import { MultipartFile } from 'fastify-multipart'; import { MultipartFile } from 'fastify-multipart';
import { z } from 'zod';
export class MultiPartFileDto { export const MultiPartFileDtoSchema = z.object({
@IsString() fieldname: z.string(),
@IsNotEmpty() encoding: z.string(),
fieldname: string; filename: z.string(),
mimetype: z.string(),
toBuffer: z.function(undefined, z.any()),
file: z.any(),
});
export type MultiPartFileDto = z.infer<typeof MultiPartFileDtoSchema>;
@IsString() export function CreateMultiPartFileDto(
@IsNotEmpty() file: MultipartFile,
encoding: string; exceptionOnFail: HttpException,
): MultiPartFileDto {
@IsString() return {
@IsNotEmpty() fieldname: file.fieldname,
filename: string; encoding: file.encoding,
filename: file.filename,
@IsString() mimetype: file.mimetype,
@IsNotEmpty() toBuffer: async () => {
mimetype: string;
@IsDefined()
toBuffer: () => Promise<Buffer>;
@IsDefined()
file: BusboyFileStream;
constructor(file: MultipartFile, exceptionOnFail: HttpException) {
this.fieldname = file.fieldname;
this.encoding = file.encoding;
this.filename = file.filename;
this.mimetype = file.mimetype;
this.toBuffer = async () => {
try { try {
return await file.toBuffer(); return await file.toBuffer();
} catch (e) { } catch (e) {
throw exceptionOnFail; throw exceptionOnFail;
} }
},
file: file.file,
}; };
this.file = file.file;
}
} }
export class MultiPartFieldDto { export const MultiPartFieldDtoSchema = z.object({
@IsString() fieldname: z.string(),
@IsNotEmpty() encoding: z.string(),
fieldname: string; value: z.string(),
});
export type MultiPartFieldDto = z.infer<typeof MultiPartFieldDtoSchema>;
@IsString() export function CreateMultiPartFieldDto(file: MultipartFile): MultiPartFieldDto {
@IsNotEmpty() return {
encoding: string; fieldname: file.fieldname,
encoding: file.encoding,
@IsString() value: (file as any).value,
@IsNotEmpty() };
value: string;
constructor(file: MultipartFile) {
this.fieldname = file.fieldname;
this.encoding = file.encoding;
this.value = (file as any).value;
}
} }

View file

@ -1,17 +0,0 @@
import { Type } from 'class-transformer';
import { IsDefined, ValidateNested } from 'class-validator';
import { CombinePDecorators } from 'picsur-shared/dist/util/decorator';
import { MultiPartFieldDto, MultiPartFileDto } from '../requests/multipart.dto';
export const IsMultiPartFile = CombinePDecorators(
IsDefined(),
ValidateNested(),
Type(() => MultiPartFileDto),
);
export const IsMultiPartField = CombinePDecorators(
IsDefined(),
ValidateNested(),
Type(() => MultiPartFieldDto),
);

View file

@ -1,11 +1,11 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { import {
AllPermissionsResponse, AllPermissionsResponse,
InfoResponse InfoResponse
} from 'picsur-shared/dist/dto/api/info.dto'; } from 'picsur-shared/dist/dto/api/info.dto';
import { HostConfigService } from '../../../config/early/host.config.service'; import { HostConfigService } from '../../../config/early/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator'; import { NoPermissions } from '../../../decorators/permissions.decorator';
import { Returns } from '../../../decorators/returns.decorator';
import { PermissionsList } from '../../../models/dto/permissions.dto'; import { PermissionsList } from '../../../models/dto/permissions.dto';
@Controller('api/info') @Controller('api/info')
@ -14,6 +14,7 @@ export class InfoController {
constructor(private hostConfig: HostConfigService) {} constructor(private hostConfig: HostConfigService) {}
@Get() @Get()
@Returns(InfoResponse)
async getInfo(): Promise<InfoResponse> { async getInfo(): Promise<InfoResponse> {
return { return {
demo: this.hostConfig.isDemo(), demo: this.hostConfig.isDemo(),
@ -24,11 +25,10 @@ export class InfoController {
// List all available permissions // List all available permissions
@Get('permissions') @Get('permissions')
@Returns(AllPermissionsResponse)
async getPermissions(): Promise<AllPermissionsResponse> { async getPermissions(): Promise<AllPermissionsResponse> {
const result: AllPermissionsResponse = { return {
permissions: PermissionsList, permissions: PermissionsList,
}; };
return plainToClass(AllPermissionsResponse, result);
} }
} }

View file

@ -8,13 +8,15 @@ import {
Post Post
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
GetSyspreferenceResponse, GetSysPreferenceResponse,
MultipleSysPreferencesResponse, UpdateSysPreferenceRequest, MultipleSysPreferencesResponse,
UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/syspref.dto'; } from 'picsur-shared/dist/dto/api/syspref.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service'; import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Returns } from '../../../decorators/returns.decorator';
import { Permission } from '../../../models/dto/permissions.dto'; import { Permission } from '../../../models/dto/permissions.dto';
@Controller('api/pref') @Controller('api/pref')
@ -25,6 +27,7 @@ export class PrefController {
constructor(private prefService: SysPreferenceService) {} constructor(private prefService: SysPreferenceService) {}
@Get('sys') @Get('sys')
@Returns(MultipleSysPreferencesResponse)
async getAllSysPrefs(): Promise<MultipleSysPreferencesResponse> { async getAllSysPrefs(): Promise<MultipleSysPreferencesResponse> {
const prefs = await this.prefService.getAllPreferences(); const prefs = await this.prefService.getAllPreferences();
if (HasFailed(prefs)) { if (HasFailed(prefs)) {
@ -39,9 +42,10 @@ export class PrefController {
} }
@Get('sys/:key') @Get('sys/:key')
@Returns(GetSysPreferenceResponse)
async getSysPref( async getSysPref(
@Param('key') key: string, @Param('key') key: string,
): Promise<GetSyspreferenceResponse> { ): Promise<GetSysPreferenceResponse> {
const pref = await this.prefService.getPreference(key); const pref = await this.prefService.getPreference(key);
if (HasFailed(pref)) { if (HasFailed(pref)) {
this.logger.warn(pref.getReason()); this.logger.warn(pref.getReason());
@ -52,6 +56,7 @@ export class PrefController {
} }
@Post('sys/:key') @Post('sys/:key')
@Returns(UpdateSysPreferenceResponse)
async setSysPref( async setSysPref(
@Param('key') key: string, @Param('key') key: string,
@Body() body: UpdateSysPreferenceRequest, @Body() body: UpdateSysPreferenceRequest,

View file

@ -22,6 +22,7 @@ import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service'; import { RolesService } from '../../../collections/roledb/roledb.service';
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 { Returns } from '../../../decorators/returns.decorator';
import { Permission } from '../../../models/dto/permissions.dto'; import { Permission } from '../../../models/dto/permissions.dto';
import { import {
DefaultRolesList, DefaultRolesList,
@ -42,6 +43,7 @@ export class RolesController {
) {} ) {}
@Get('list') @Get('list')
@Returns(RoleListResponse)
async getRoles(): Promise<RoleListResponse> { async getRoles(): Promise<RoleListResponse> {
const roles = await this.rolesService.findAll(); const roles = await this.rolesService.findAll();
if (HasFailed(roles)) { if (HasFailed(roles)) {
@ -56,6 +58,7 @@ export class RolesController {
} }
@Post('info') @Post('info')
@Returns(RoleInfoResponse)
async getRole(@Body() body: RoleInfoRequest): Promise<RoleInfoResponse> { async getRole(@Body() body: RoleInfoRequest): Promise<RoleInfoResponse> {
const role = await this.rolesService.findOne(body.name); const role = await this.rolesService.findOne(body.name);
if (HasFailed(role)) { if (HasFailed(role)) {
@ -67,6 +70,7 @@ export class RolesController {
} }
@Post('update') @Post('update')
@Returns(RoleUpdateResponse)
async updateRole( async updateRole(
@Body() body: RoleUpdateRequest, @Body() body: RoleUpdateRequest,
): Promise<RoleUpdateResponse> { ): Promise<RoleUpdateResponse> {
@ -88,6 +92,7 @@ export class RolesController {
} }
@Post('create') @Post('create')
@Returns(RoleCreateResponse)
async createRole( async createRole(
@Body() role: RoleCreateRequest, @Body() role: RoleCreateRequest,
): Promise<RoleCreateResponse> { ): Promise<RoleCreateResponse> {
@ -106,6 +111,7 @@ export class RolesController {
} }
@Post('delete') @Post('delete')
@Returns(RoleDeleteResponse)
async deleteRole( async deleteRole(
@Body() role: RoleDeleteRequest, @Body() role: RoleDeleteRequest,
): Promise<RoleDeleteResponse> { ): Promise<RoleDeleteResponse> {
@ -126,6 +132,7 @@ export class RolesController {
} }
@Get('special') @Get('special')
@Returns(SpecialRolesResponse)
async getSpecialRoles(): Promise<SpecialRolesResponse> { async getSpecialRoles(): Promise<SpecialRolesResponse> {
return { return {
SoulBoundRoles: SoulBoundRolesList, SoulBoundRoles: SoulBoundRolesList,

View file

@ -8,8 +8,7 @@ import {
Request Request
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
UserLoginResponse, UserLoginResponse, UserMePermissionsResponse,
UserMePermissionsResponse,
UserMeResponse, UserMeResponse,
UserRegisterRequest, UserRegisterRequest,
UserRegisterResponse UserRegisterResponse
@ -21,6 +20,7 @@ import {
RequiredPermissions, RequiredPermissions,
UseLocalAuth UseLocalAuth
} from '../../../decorators/permissions.decorator'; } from '../../../decorators/permissions.decorator';
import { Returns } from '../../../decorators/returns.decorator';
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';
@ -36,6 +36,7 @@ export class UserController {
) {} ) {}
@Post('login') @Post('login')
@Returns(UserLoginResponse)
@UseLocalAuth(Permission.UserLogin) @UseLocalAuth(Permission.UserLogin)
async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> { async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> {
const jwt_token = await this.authService.createToken(req.user); const jwt_token = await this.authService.createToken(req.user);
@ -48,6 +49,7 @@ export class UserController {
} }
@Post('register') @Post('register')
@Returns(UserRegisterResponse)
@RequiredPermissions(Permission.UserRegister) @RequiredPermissions(Permission.UserRegister)
async register( async register(
@Body() register: UserRegisterRequest, @Body() register: UserRegisterRequest,
@ -65,28 +67,32 @@ export class UserController {
} }
@Get('me') @Get('me')
@Returns(UserMeResponse)
@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'); if (!req.user.id) throw new InternalServerErrorException('User is corrupt');
const user = await this.usersService.findOne(req.user.id); const backenduser = await this.usersService.findOne(req.user.id);
if (HasFailed(user)) { if (HasFailed(backenduser)) {
this.logger.warn(user.getReason()); this.logger.warn(backenduser.getReason());
throw new InternalServerErrorException('Could not get user'); throw new InternalServerErrorException('Could not get user');
} }
const user = EUserBackend2EUser(backenduser);
const token = await this.authService.createToken(user); const token = await this.authService.createToken(user);
if (HasFailed(token)) { if (HasFailed(token)) {
this.logger.warn(token.getReason()); this.logger.warn(token.getReason());
throw new InternalServerErrorException('Could not get new token'); throw new InternalServerErrorException('Could not get new token');
} }
return { user: EUserBackend2EUser(user), token }; return { user, token };
} }
// You can always check your permissions // You can always check your permissions
@Get('me/permissions') @Get('me/permissions')
@Returns(UserMePermissionsResponse)
@NoPermissions() @NoPermissions()
async refresh( async refresh(
@Request() req: AuthFasityRequest, @Request() req: AuthFasityRequest,

View file

@ -6,7 +6,6 @@ import {
Logger, Logger,
Post Post
} from '@nestjs/common'; } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { import {
GetSpecialUsersResponse, GetSpecialUsersResponse,
UserCreateRequest, UserCreateRequest,
@ -23,6 +22,7 @@ import {
import { HasFailed } from 'picsur-shared/dist/types'; 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 { Returns } from '../../../decorators/returns.decorator';
import { Permission } from '../../../models/dto/permissions.dto'; import { Permission } from '../../../models/dto/permissions.dto';
import { import {
ImmutableUsersList, ImmutableUsersList,
@ -39,15 +39,16 @@ export class UserManageController {
constructor(private usersService: UsersService) {} constructor(private usersService: UsersService) {}
@Get('list') @Get('list')
@Returns(UserListResponse)
async listUsers(): Promise<UserListResponse> { async listUsers(): Promise<UserListResponse> {
const body = new UserListRequest(); return this.listUsersPaged({
body.count = 20; count: 20,
body.page = 0; page: 0,
});
return this.listUsersPaged(body);
} }
@Post('list') @Post('list')
@Returns(UserListResponse)
async listUsersPaged( async listUsersPaged(
@Body() body: UserListRequest, @Body() body: UserListRequest,
): Promise<UserListResponse> { ): Promise<UserListResponse> {
@ -65,6 +66,7 @@ export class UserManageController {
} }
@Post('create') @Post('create')
@Returns(UserCreateResponse)
async register( async register(
@Body() create: UserCreateRequest, @Body() create: UserCreateRequest,
): Promise<UserCreateResponse> { ): Promise<UserCreateResponse> {
@ -82,6 +84,7 @@ export class UserManageController {
} }
@Post('delete') @Post('delete')
@Returns(UserDeleteResponse)
async delete(@Body() body: UserDeleteRequest): Promise<UserDeleteResponse> { async delete(@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)) {
@ -93,6 +96,7 @@ export class UserManageController {
} }
@Post('info') @Post('info')
@Returns(UserInfoResponse)
async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> { async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> {
const user = await this.usersService.findOne(body.id); const user = await this.usersService.findOne(body.id);
if (HasFailed(user)) { if (HasFailed(user)) {
@ -104,6 +108,7 @@ export class UserManageController {
} }
@Post('update') @Post('update')
@Returns(UserUpdateResponse)
async setPermissions( async setPermissions(
@Body() body: UserUpdateRequest, @Body() body: UserUpdateRequest,
): Promise<UserUpdateResponse> { ): Promise<UserUpdateResponse> {
@ -133,13 +138,12 @@ export class UserManageController {
} }
@Get('special') @Get('special')
@Returns(GetSpecialUsersResponse)
async getSpecial(): Promise<GetSpecialUsersResponse> { async getSpecial(): Promise<GetSpecialUsersResponse> {
const result: GetSpecialUsersResponse = { return {
ImmutableUsersList, ImmutableUsersList,
LockedLoginUsersList, LockedLoginUsersList,
UndeletableUsersList, UndeletableUsersList,
}; };
return plainToClass(GetSpecialUsersResponse, result);
} }
} }

View file

@ -4,14 +4,13 @@ import {
Injectable, Injectable,
PipeTransform PipeTransform
} from '@nestjs/common'; } from '@nestjs/common';
import { isHash } from 'class-validator'; import { SHA256 } from 'picsur-shared/dist/util/common-regex';
@Injectable() @Injectable()
export class ImageIdValidator implements PipeTransform<string, string> { export class ImageIdValidator implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string { transform(value: string, metadata: ArgumentMetadata): string {
if (isHash(value, 'sha256')) { // Check regex for sha256
return value; if (SHA256.test(value)) return value;
}
throw new BadRequestException('Invalid image id'); throw new BadRequestException('Invalid image id');
} }
} }

View file

@ -13,9 +13,12 @@ import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator'; import { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { Returns } from '../../decorators/returns.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 { EImageBackend2EImage } from '../../models/transformers/image.transformer';
import { ImageIdValidator } from './imageid.validator'; import { ImageIdValidator } from './imageid.validator';
@ -45,6 +48,7 @@ export class ImageController {
} }
@Get('meta/:hash') @Get('meta/:hash')
@Returns(ImageMetaResponse)
async getImageMeta( async getImageMeta(
@Param('hash', ImageIdValidator) hash: string, @Param('hash', ImageIdValidator) hash: string,
): Promise<ImageMetaResponse> { ): Promise<ImageMetaResponse> {
@ -58,9 +62,10 @@ export class ImageController {
} }
@Post() @Post()
@Returns(ImageMetaResponse)
@RequiredPermissions(Permission.ImageUpload) @RequiredPermissions(Permission.ImageUpload)
async uploadImage( async uploadImage(
@MultiPart(ImageUploadDto) multipart: ImageUploadDto, @MultiPart() multipart: ImageUploadDto,
): Promise<ImageMetaResponse> { ): Promise<ImageMetaResponse> {
const fileBuffer = await multipart.image.toBuffer(); const fileBuffer = await multipart.image.toBuffer();
const image = await this.imagesService.upload(fileBuffer); const image = await this.imagesService.upload(fileBuffer);

View file

@ -24,8 +24,6 @@
"@angular/platform-browser-dynamic": "^14.0.0-next.10", "@angular/platform-browser-dynamic": "^14.0.0-next.10",
"@angular/router": "^14.0.0-next.10", "@angular/router": "^14.0.0-next.10",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"fuse.js": "^6.5.3", "fuse.js": "^6.5.3",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"ngx-auto-unsubscribe-decorator": "^0.1.0", "ngx-auto-unsubscribe-decorator": "^0.1.0",
@ -34,6 +32,7 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "~7.5.5", "rxjs": "~7.5.5",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"zod": "^3.14.3",
"zone.js": "~0.11.5" "zone.js": "~0.11.5"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { isHash } from 'class-validator';
import { ImageLinks } from 'picsur-shared/dist/dto/imagelinks.dto'; import { ImageLinks } from 'picsur-shared/dist/dto/imagelinks.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { SHA256 } from 'picsur-shared/dist/util/common-regex';
import { ImageService } from 'src/app/services/api/image.service'; import { ImageService } from 'src/app/services/api/image.service';
import { UtilService } from 'src/app/util/util.service'; import { UtilService } from 'src/app/util/util.service';
@ -24,7 +24,7 @@ export class ViewComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
const params = this.route.snapshot.paramMap; const params = this.route.snapshot.paramMap;
const hash = params.get('hash') ?? ''; const hash = params.get('hash') ?? '';
if (!isHash(hash, 'sha256')) { if (!SHA256.test(hash)) {
return this.utilService.quitError('Invalid image link'); return this.utilService.quitError('Invalid image link');
} }

View file

@ -1,13 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ClassConstructor, plainToClass } from 'class-transformer'; import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto';
import {
ApiResponse,
ApiSuccessResponse
} from 'picsur-shared/dist/dto/api/api.dto';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ApiError } from 'src/app/models/dto/api-error.dto'; import { ApiError } from 'src/app/models/dto/api-error.dto';
import { z } from 'zod';
import { MultiPartRequest } from '../../models/dto/multi-part-request.dto'; import { MultiPartRequest } from '../../models/dto/multi-part-request.dto';
import { Logger } from '../logger/logger.service'; import { Logger } from '../logger/logger.service';
import { KeyService } from '../storage/key.service'; import { KeyService } from '../storage/key.service';
@ -30,72 +27,64 @@ export class ApiService {
constructor(private keyService: KeyService) {} constructor(private keyService: KeyService) {}
public async get<T extends Object>( public async get<T extends z.AnyZodObject>(
type: ClassConstructor<T>, type: ZodDtoStatic<T>,
url: string url: string
): AsyncFailable<T> { ): AsyncFailable<z.infer<T>> {
return this.fetchSafeJson(type, url, { method: 'GET' }); return this.fetchSafeJson(type, url, { method: 'GET' });
} }
public async post<T extends Object, W extends Object>( public async post<T extends z.AnyZodObject, W extends z.AnyZodObject>(
sendType: ClassConstructor<T>, sendType: ZodDtoStatic<T>,
receiveType: ClassConstructor<W>, receiveType: ZodDtoStatic<W>,
url: string, url: string,
data: T data: z.infer<T>
): AsyncFailable<W> { ): AsyncFailable<z.infer<W>> {
const sendClass = plainToClass(sendType, data); const sendSchema = sendType.zodSchema;
const errors = await strictValidate(sendClass);
if (errors.length > 0) { const validateResult = sendSchema.safeParse(data);
this.logger.warn(errors); if (!validateResult.success) {
this.logger.warn(validateResult.error);
return Fail('Something went wrong'); return Fail('Something went wrong');
} }
return this.fetchSafeJson(receiveType, url, { return this.fetchSafeJson(receiveType, url, {
method: 'POST', method: 'POST',
body: JSON.stringify(sendClass), body: JSON.stringify(validateResult.data),
}); });
} }
public async postForm<T extends Object>( public async postForm<T extends z.AnyZodObject>(
receiveType: ClassConstructor<T>, receiveType: ZodDtoStatic<T>,
url: string, url: string,
data: MultiPartRequest data: MultiPartRequest
): AsyncFailable<T> { ): AsyncFailable<z.infer<T>> {
return this.fetchSafeJson(receiveType, url, { return this.fetchSafeJson(receiveType, url, {
method: 'POST', method: 'POST',
body: data.createFormData(), body: data.createFormData(),
}); });
} }
private async fetchSafeJson<T extends Object>( private async fetchSafeJson<T extends z.AnyZodObject>(
type: ClassConstructor<T>, type: ZodDtoStatic<T>,
url: RequestInfo, url: RequestInfo,
options: RequestInit options: RequestInit
): AsyncFailable<T> { ): AsyncFailable<z.infer<T>> {
let result = await this.fetchJsonAs<ApiResponse<T>>(url, options); const resultSchema = ApiResponseSchema(type.zodSchema as z.AnyZodObject);
type resultType = z.infer<typeof resultSchema>;
let result = await this.fetchJsonAs<resultType>(url, options);
if (HasFailed(result)) return result; if (HasFailed(result)) return result;
if (result.success === false) return Fail(result.data.message); const validateResult = resultSchema.safeParse(result);
if (!validateResult.success) {
const resultClass = plainToClass< this.logger.warn('result', validateResult.error);
ApiSuccessResponse<T>,
ApiSuccessResponse<T>
>(ApiSuccessResponse, result);
const resultErrors = await strictValidate(resultClass);
if (resultErrors.length > 0) {
this.logger.warn('result', resultErrors);
return Fail('Something went wrong'); return Fail('Something went wrong');
} }
const dataClass = plainToClass(type, result.data); if (validateResult.data.success === false) return Fail(result.data.message);
const dataErrors = await strictValidate(dataClass);
if (dataErrors.length > 0) {
this.logger.warn('data', dataErrors);
return Fail('Something went wrong');
}
return result.data; return validateResult.data.data;
} }
private async fetchJsonAs<T>( private async fetchJsonAs<T>(

View file

@ -1,11 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { plainToClass } from 'class-transformer';
import { isSemVer } from 'class-validator';
import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto';
import { import {
AsyncFailable, AsyncFailable,
Fail, HasFailed Fail, HasFailed
} from 'picsur-shared/dist/types'; } from 'picsur-shared/dist/types';
import { SemVer } from 'picsur-shared/dist/util/common-regex';
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';
import { UtilService } from 'src/app/util/util.service'; import { UtilService } from 'src/app/util/util.service';
@ -34,10 +33,8 @@ export class InfoService {
const response = await this.api.get(InfoResponse, '/api/info'); const response = await this.api.get(InfoResponse, '/api/info');
if (HasFailed(response)) return response; if (HasFailed(response)) return response;
const info = plainToClass(ServerInfo, response); this.infoSubject.next(response);
return response;
this.infoSubject.next(info);
return info;
} }
public getFrontendVersion(): string { public getFrontendVersion(): string {
@ -53,7 +50,7 @@ export class InfoService {
const serverVersion = info.version; const serverVersion = info.version;
const clientVersion = this.getFrontendVersion(); const clientVersion = this.getFrontendVersion();
if (!isSemVer(serverVersion) || !isSemVer(clientVersion)) { if (!SemVer.test(serverVersion) || !SemVer.test(clientVersion)) {
return Fail(`Not a valid semver: ${serverVersion} or ${clientVersion}`); return Fail(`Not a valid semver: ${serverVersion} or ${clientVersion}`);
} }

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { import {
GetSyspreferenceResponse, GetSysPreferenceResponse,
MultipleSysPreferencesResponse, MultipleSysPreferencesResponse,
UpdateSysPreferenceRequest, UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse UpdateSysPreferenceResponse
@ -73,12 +73,12 @@ export class SysprefService {
public async getPreference( public async getPreference(
key: string key: string
): AsyncFailable<GetSyspreferenceResponse> { ): AsyncFailable<GetSysPreferenceResponse> {
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');
const response = await this.api.get( const response = await this.api.get(
GetSyspreferenceResponse, GetSysPreferenceResponse,
`/api/pref/sys/${key}` `/api/pref/sys/${key}`
); );

View file

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { plainToClass } from 'class-transformer';
import jwt_decode from 'jwt-decode'; import jwt_decode from 'jwt-decode';
import { import {
UserLoginRequest, UserLoginRequest,
@ -8,10 +7,9 @@ import {
UserRegisterRequest, UserRegisterRequest,
UserRegisterResponse UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto'; } from 'picsur-shared/dist/dto/api/user.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { Logger } from '../logger/logger.service'; import { Logger } from '../logger/logger.service';
import { KeyService } from '../storage/key.service'; import { KeyService } from '../storage/key.service';
@ -122,14 +120,13 @@ export class UserService {
return Fail('Invalid token'); return Fail('Invalid token');
} }
const jwtData = plainToClass(JwtDataDto, decoded); const result = JwtDataSchema.safeParse(decoded);
const errors = await strictValidate(jwtData); if (!result.success) {
if (errors.length > 0) { this.logger.warn(result.error);
this.logger.warn(errors);
return Fail('Invalid token data'); return Fail('Invalid token data');
} }
return jwtData.user; return result.data.user;
} }
// This actually fetches up to date information from the server // This actually fetches up to date information from the server

View file

@ -9,9 +9,8 @@
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"dependencies": { "dependencies": {
"class-transformer": "^0.5.1", "tsc-watch": "^4.6.2",
"class-validator": "^0.13.2", "zod": "^3.14.3"
"tsc-watch": "^4.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.23", "@types/node": "^17.0.23",

View file

@ -1,36 +1,28 @@
import { import z from 'zod';
IsBoolean, IsInt,
IsNotEmpty,
IsString,
Max,
Min
} from 'class-validator';
class BaseApiResponse<T extends Object, W extends boolean> { const ApiResponseBase = z.object({
@IsBoolean() statusCode: z.number().min(0).max(600),
success: W; timestamp: z.string(),
});
@IsInt() const ApiSuccessResponse = <T extends z.AnyZodObject>(data: T) =>
@Min(0) ApiResponseBase.merge(
@Max(1000) z.object({
statusCode: number; success: z.literal(true),
data,
}),
);
@IsString() const ApiErrorResponse = ApiResponseBase.merge(
timestamp: string; z.object({
success: z.literal(false),
data: z.object({
message: z.string(),
}),
}),
);
@IsNotEmpty() export const ApiResponseSchema = <T extends z.AnyZodObject>(data: T) =>
data: T; ApiErrorResponse.or(ApiSuccessResponse(data));
}
export class ApiSuccessResponse<T extends Object> extends BaseApiResponse< export type ApiErrorResponse = z.infer<typeof ApiErrorResponse>;
T,
true
> {}
export class ApiErrorData {
@IsString()
message: string;
}
export class ApiErrorResponse extends BaseApiResponse<ApiErrorData, false> {}
export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;

View file

@ -1,3 +1,5 @@
import { EImage } from '../../entities/image.entity'; import { EImageSchema } from '../../entities/image.entity';
import { createZodDto } from '../../util/create-zod-dto';
export class ImageMetaResponse extends EImage {} export const ImageMetaResponseSchema = EImageSchema;
export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {}

View file

@ -1,19 +1,17 @@
import { IsBoolean, IsSemVer } from 'class-validator'; import { string, z } from 'zod';
import { SemVer } from '../../util/common-regex';
import { createZodDto } from '../../util/create-zod-dto';
import { IsStringList } from '../../validators/string-list.validator'; import { IsStringList } from '../../validators/string-list.validator';
export class InfoResponse { export const InfoResponseSchema = z.object({
@IsBoolean() production: z.boolean(),
production: boolean; demo: z.boolean(),
version: string().regex(SemVer),
});
export class InfoResponse extends createZodDto(InfoResponseSchema) {}
@IsBoolean() // Allpermissions
demo: boolean; export const AllPermissionsResponseSchema = z.object({
permissions: IsStringList(),
@IsSemVer() });
version: string; export class AllPermissionsResponse extends createZodDto(AllPermissionsResponseSchema) {}
}
// AllPermissions
export class AllPermissionsResponse {
@IsStringList()
permissions: string[];
}

View file

@ -1,53 +1,59 @@
import { IsArray } from 'class-validator'; import { z } from 'zod';
import { ERole, SimpleRole } from '../../entities/role.entity'; import {
import { IsNested } from '../../validators/nested.validator'; ERoleSchema, SimpleRoleSchema
} from '../../entities/role.entity';
import { createZodDto } from '../../util/create-zod-dto';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.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';
// RoleInfo // RoleInfo
export class RoleInfoRequest { export const RoleInfoRequestSchema = z.object({
@IsRoleName() name: IsRoleName(),
name: string; });
} export class RoleInfoRequest extends createZodDto(RoleInfoRequestSchema) {}
export class RoleInfoResponse extends ERole {}
export const RoleInfoResponseSchema = ERoleSchema;
export class RoleInfoResponse extends createZodDto(RoleInfoResponseSchema) {}
// RoleList // RoleList
export class RoleListResponse { export const RoleListResponseSchema = z.object({
@IsArray() roles: z.array(ERoleSchema),
@IsNested(ERole) total: IsPosInt(),
roles: ERole[]; });
export class RoleListResponse extends createZodDto(RoleListResponseSchema) {}
@IsPosInt()
total: number;
}
// RoleUpdate // RoleUpdate
export class RoleUpdateRequest extends SimpleRole {} export const RoleUpdateRequestSchema = SimpleRoleSchema.partial({
export class RoleUpdateResponse extends ERole {} permissions: true,
});
export class RoleUpdateRequest extends createZodDto(RoleUpdateRequestSchema) {}
export const RoleUpdateResponseSchema = ERoleSchema;
export class RoleUpdateResponse extends createZodDto(RoleUpdateResponseSchema) {}
// RoleCreate // RoleCreate
export class RoleCreateRequest extends SimpleRole {} export const RoleCreateRequestSchema = SimpleRoleSchema;
export class RoleCreateResponse extends ERole {} export class RoleCreateRequest extends createZodDto(RoleCreateRequestSchema) {}
export const RoleCreateResponseSchema = ERoleSchema;
export class RoleCreateResponse extends createZodDto(RoleCreateResponseSchema) {}
// RoleDelete // RoleDelete
export class RoleDeleteRequest { export const RoleDeleteRequestSchema = z.object({
@IsRoleName() name: IsRoleName(),
name: string; });
} export class RoleDeleteRequest extends createZodDto(RoleDeleteRequestSchema) {}
export class RoleDeleteResponse extends ERole {}
export const RoleDeleteResponseSchema = ERoleSchema;
export class RoleDeleteResponse extends createZodDto(RoleDeleteResponseSchema) {}
// SpecialRoles // SpecialRoles
export class SpecialRolesResponse {
@IsStringList()
SoulBoundRoles: string[];
@IsStringList() export const SpecialRolesResponseSchema = z.object({
ImmutableRoles: string[]; SoulBoundRoles: IsStringList(),
ImmutableRoles: IsStringList(),
@IsStringList() UndeletableRoles: IsStringList(),
UndeletableRoles: string[]; DefaultRoles: IsStringList(),
});
@IsStringList() export class SpecialRolesResponse extends createZodDto(SpecialRolesResponseSchema) {}
DefaultRoles: string[];
}

View file

@ -1,31 +1,29 @@
import { import { z } from 'zod';
IsArray import { createZodDto } from '../../util/create-zod-dto';
} from 'class-validator';
import { IsNested } from '../../validators/nested.validator';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.validator';
import { IsPrefValue } from '../../validators/pref-value.validator'; import { IsPrefValue } from '../../validators/pref-value.validator';
import { DecodedSysPref, PrefValueType } from '../preferences.dto'; import { DecodedSysPrefSchema } from '../preferences.dto';
// Get Syspreference // Get Syspreference
// Request is done via url parameters // Request is done via url parameters
export class GetSyspreferenceResponse extends DecodedSysPref {} export const GetSysPreferenceResponseSchema = DecodedSysPrefSchema;
export class GetSysPreferenceResponse extends createZodDto(GetSysPreferenceResponseSchema) {}
// Get syspreferences // Get syspreferences
export class MultipleSysPreferencesResponse { export const MultipleSysPreferencesResponseSchema = z.object({
@IsArray() preferences: z.array(DecodedSysPrefSchema),
@IsNested(DecodedSysPref) total: IsPosInt(),
preferences: DecodedSysPref[]; });
export class MultipleSysPreferencesResponse extends createZodDto(MultipleSysPreferencesResponseSchema) {}
@IsPosInt()
total: number;
}
// Update Syspreference // Update Syspreference
export class UpdateSysPreferenceRequest { export const UpdateSysPreferenceRequestSchema = z.object({
@IsPrefValue() value: IsPrefValue(),
value: PrefValueType; });
} export class UpdateSysPreferenceRequest extends createZodDto(UpdateSysPreferenceRequestSchema) {}
export class UpdateSysPreferenceResponse extends DecodedSysPref {}
export const UpdateSysPreferenceResponseSchema = DecodedSysPrefSchema;
export class UpdateSysPreferenceResponse extends createZodDto(UpdateSysPreferenceResponseSchema) {}

View file

@ -1,47 +1,40 @@
import { import { z } from 'zod';
IsJWT import { EUserSchema } from '../../entities/user.entity';
} from 'class-validator'; import { createZodDto } from '../../util/create-zod-dto';
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'; import { IsPlainTextPwd, IsUsername } from '../../validators/user.validators';
// Api // Api
const UserPassSchema = z.object({
username: IsUsername(),
password: IsPlainTextPwd(),
});
// UserLogin // UserLogin
export class UserLoginRequest { export const UserLoginRequestSchema = UserPassSchema;
@IsUsername() export class UserLoginRequest extends createZodDto(UserLoginRequestSchema) {}
username: string;
@IsPlainTextPwd() export const UserLoginResponseSchema = z.object({
password: string; jwt_token: z.string(),
} });
export class UserLoginResponse { export class UserLoginResponse extends createZodDto(UserLoginResponseSchema) {}
@IsJWT()
jwt_token: string;
}
// UserRegister // UserRegister
export class UserRegisterRequest { export const UserRegisterRequestSchema = UserPassSchema;
@IsUsername() export class UserRegisterRequest extends createZodDto(UserRegisterRequestSchema) {}
username: string;
@IsPlainTextPwd() export const UserRegisterResponseSchema = EUserSchema;
password: string; export class UserRegisterResponse extends createZodDto(UserRegisterResponseSchema) {}
}
export class UserRegisterResponse extends EUser {}
// UserMe // UserMe
export class UserMeResponse { export const UserMeResponseSchema = z.object({
@IsNested(EUser) user: EUserSchema,
user: EUser; token: z.string(),
});
@IsJWT() export class UserMeResponse extends createZodDto(UserMeResponseSchema) {}
token: string;
}
// UserMePermissions // UserMePermissions
export class UserMePermissionsResponse { export const UserMePermissionsResponseSchema = z.object({
@IsStringList() permissions: IsStringList(),
permissions: string[]; });
} export class UserMePermissionsResponse extends createZodDto(UserMePermissionsResponseSchema) {}

View file

@ -1,71 +1,60 @@
import { IsArray, IsOptional } from 'class-validator'; import { z } from 'zod';
import { EUser, SimpleUser } from '../../entities/user.entity'; import {
import { Newable } from '../../types'; EUserSchema, SimpleUserSchema
import { IsEntityID } from '../../validators/entity-id.validator'; } from '../../entities/user.entity';
import { IsNested } from '../../validators/nested.validator'; import { createZodDto } from '../../util/create-zod-dto';
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 { EntityIDObject } from '../idobject.dto'; import { EntityIDObjectSchema } from '../idobject.dto';
// UserList // UserList
export class UserListRequest { export const UserListRequestSchema = z.object({
@IsPosInt() count: IsPosInt(),
count: number; page: IsPosInt(),
});
export class UserListRequest extends createZodDto(UserListRequestSchema) {}
@IsPosInt() export const UserListResponseSchema = z.object({
page: number; users: z.array(EUserSchema),
} count: IsPosInt(),
page: IsPosInt(),
export class UserListResponse { });
@IsArray() export class UserListResponse extends createZodDto(UserListResponseSchema) {}
@IsNested(EUser)
users: EUser[];
@IsPosInt()
count: number;
@IsPosInt()
page: number;
}
// UserCreate // UserCreate
export class UserCreateRequest extends SimpleUser {} export const UserCreateRequestSchema = SimpleUserSchema;
export class UserCreateResponse extends EUser {} export class UserCreateRequest extends createZodDto(UserCreateRequestSchema) {}
export const UserCreateResponseSchema = EUserSchema;
export class UserCreateResponse extends createZodDto(UserCreateResponseSchema) {}
// UserDelete // UserDelete
export class UserDeleteRequest extends EntityIDObject {} export const UserDeleteRequestSchema = EntityIDObjectSchema;
export class UserDeleteResponse extends EUser {} export class UserDeleteRequest extends createZodDto(UserDeleteRequestSchema) {}
export const UserDeleteResponseSchema = EUserSchema;
export class UserDeleteResponse extends createZodDto(UserDeleteResponseSchema) {}
// UserInfo // UserInfo
export class UserInfoRequest extends EntityIDObject {} export const UserInfoRequestSchema = EntityIDObjectSchema;
export class UserInfoResponse extends EUser {} export class UserInfoRequest extends createZodDto(UserInfoRequestSchema) {}
export const UserInfoResponseSchema = EUserSchema;
export class UserInfoResponse extends createZodDto(UserInfoResponseSchema) {}
// UserUpdate // UserUpdate
export class UserUpdateRequest extends (SimpleUser as Newable< export const UserUpdateRequestSchema = EntityIDObjectSchema.merge(
Partial<SimpleUser> SimpleUserSchema.partial(),
>) { );
@IsEntityID() export class UserUpdateRequest extends createZodDto(UserUpdateRequestSchema) {}
id: string;
@IsOptional() export const UserUpdateResponseSchema = EUserSchema;
override username?: string; export class UserUpdateResponse extends createZodDto(UserUpdateResponseSchema) {}
@IsOptional()
override password?: string;
@IsOptional()
override roles?: string[];
}
export class UserUpdateResponse extends EUser {}
// GetSpecialUsers // GetSpecialUsers
export class GetSpecialUsersResponse { export const GetSpecialUsersResponseSchema = z.object({
@IsStringList() UndeletableUsersList: IsStringList(),
UndeletableUsersList: string[]; ImmutableUsersList: IsStringList(),
LockedLoginUsersList: IsStringList(),
@IsStringList() });
ImmutableUsersList: string[]; export class GetSpecialUsersResponse extends createZodDto(GetSpecialUsersResponseSchema) {}
@IsStringList()
LockedLoginUsersList: string[];
}

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export class EntityIDObject { export const EntityIDObjectSchema = z.object({
@IsEntityID() id: IsEntityID(),
id: string; });
}

View file

@ -1,16 +1,9 @@
import { IsInt, IsOptional } from 'class-validator'; import { z } from 'zod';
import { EUser } from '../entities/user.entity'; import { EUserSchema } from '../entities/user.entity';
import { IsNested } from '../validators/nested.validator';
export class JwtDataDto { export const JwtDataSchema = z.object({
@IsNested(EUser) user: EUserSchema.required(),
user: EUser; iat: z.number().int().optional(),
exp: z.number().int().optional(),
@IsOptional() });
@IsInt() export type JwtData = z.infer<typeof JwtDataSchema>;
iat?: number;
@IsOptional()
@IsInt()
exp?: number;
}

View file

@ -1,26 +1,24 @@
import { IsEnum, IsString } from 'class-validator'; import { z } from 'zod';
import tuple from '../types/tuple';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { IsPrefValue } from '../validators/pref-value.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 = tuple('string', 'number', 'boolean');
// Decoded Representations // Decoded Representations
export class DecodedSysPref { export const DecodedSysPrefSchema = z.object({
@IsString() key: z.string(),
key: string; value: IsPrefValue(),
type: z.enum(PrefValueTypes),
})
export type DecodedSysPref = z.infer<typeof DecodedSysPrefSchema>;
@IsPrefValue() export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge(z.object({
value: PrefValueType; user: IsEntityID(),
}))
export type DecodedUsrPref = z.infer<typeof DecodedUsrPrefSchema>;
@IsEnum(PrefValueTypes)
type: PrefValueTypeStrings;
}
export class DecodedUsrPref extends DecodedSysPref {
@IsEntityID()
user: string;
}

View file

@ -1,19 +1,11 @@
import { IsHash, IsOptional, IsString } from 'class-validator'; import { z } from 'zod';
import { SHA256 } from '../util/common-regex';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
import { IsNotDefined } from '../validators/not-defined.validator';
export class EImage { export const EImageSchema = z.object({
@IsOptional() id: IsEntityID().optional(),
@IsEntityID() hash: z.string().regex(SHA256),
id?: string; data: z.undefined(),
mime: z.string(),
@IsHash('sha256') });
hash: string; export type EImage = z.infer<typeof EImageSchema>;
// Because typescript does not support exact types, we have to do this stupidness
@IsNotDefined()
data: undefined;
@IsString()
mime: string;
}

View file

@ -1,24 +1,17 @@
import { IsOptional } from 'class-validator'; import { z } from 'zod';
import { IsEntityID } 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';
export class SimpleRole { export const SimpleRoleSchema = z.object({
@IsRoleName() name: IsRoleName(),
name: string; permissions: IsStringList(),
});
export type SimpleRole = z.infer<typeof SimpleRoleSchema>;
@IsStringList() export const ERoleSchema = z.object({
permissions: string[]; id: IsEntityID().optional(),
} name: IsRoleName(),
permissions: IsStringList(),
export class ERole { });
@IsOptional() export type ERole = z.infer<typeof ERoleSchema>;
@IsEntityID()
id?: string;
@IsRoleName()
name: string;
@IsStringList()
permissions: string[];
}

View file

@ -1,14 +1,9 @@
import { IsOptional, IsString } from 'class-validator'; import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export class ESysPreference { export const ESysPreferenceSchema = z.object({
@IsOptional() id: IsEntityID().optional(),
@IsEntityID() key: z.string(),
id?: string; value: z.string(),
});
@IsString() export type ESysPreference = z.infer<typeof ESysPreferenceSchema>;
key: string;
@IsString()
value: string;
}

View file

@ -1,32 +1,19 @@
import { IsOptional } from 'class-validator'; import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } 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';
export class SimpleUser { export const SimpleUserSchema = z.object({
@IsUsername() username: IsUsername(),
username: string; password: IsPlainTextPwd(),
roles: IsStringList(),
});
export type SimpleUser = z.infer<typeof SimpleUserSchema>;
@IsPlainTextPwd() export const EUserSchema = z.object({
password: string; id: IsEntityID().optional(),
username: IsUsername(),
@IsStringList() roles: IsStringList(),
roles: string[]; hashedPassword: z.undefined(),
} });
export type EUser = z.infer<typeof EUserSchema>;
export class EUser {
@IsOptional()
@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,18 +1,11 @@
import { IsOptional, IsString } from 'class-validator'; import { z } from 'zod';
import { IsEntityID } 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 const EUsrPreferenceSchema = z.object({
@IsOptional() id: IsEntityID().optional(),
@IsEntityID() key: z.string(),
id?: string; value: z.string(),
userId: IsPosInt(),
@IsString() })
key: string; export type EUsrPreference = z.infer<typeof EUsrPreferenceSchema>;
@IsString()
value: string;
@IsPosInt()
userId: number;
}

View file

@ -0,0 +1,3 @@
export const AlphaNumeric = /^[a-zA-Z0-9]+$/;
export const SHA256 = /^[a-f0-9A-F]{64}$/;
export const SemVer = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;

View file

@ -0,0 +1,29 @@
import * as z from 'zod';
/**
* This file was originally taken from:
* https://github.com/anatine/zod-plugins/blob/main/libs/zod-nestjs/src/lib/create-zod-dto.ts
*
*/
export type CompatibleZodType = z.ZodType<unknown>;
export type CompatibleZodInfer<T extends CompatibleZodType> = T['_output'];
export type ZodDtoStatic<T extends CompatibleZodType = CompatibleZodType> = {
new (): CompatibleZodInfer<T>;
zodSchema: T;
create(input: unknown): CompatibleZodInfer<T>;
};
export const createZodDto = <T extends CompatibleZodType>(
zodSchema: T,
): ZodDtoStatic<T> => {
class SchemaHolderClass {
public static zodSchema = zodSchema;
public static create(input: unknown): CompatibleZodInfer<T> {
return this.zodSchema.parse(input);
}
}
return SchemaHolderClass;
};

View file

@ -1,15 +0,0 @@
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: ValidatorOptions = {
forbidNonWhitelisted: true,
forbidUnknownValues: true,
stopAtFirstError: true,
whitelist: true,
strictGroups: true,
};
export const strictValidate = (object: object) =>
validate(object, ValidateOptions);

View file

@ -1,4 +1,3 @@
import { IsUUID } from 'class-validator'; import { z } from 'zod';
import { CombinePDecorators } from '../util/decorator';
export const IsEntityID = CombinePDecorators(IsUUID('4')); export const IsEntityID = () => z.string().uuid();

View file

@ -1,15 +0,0 @@
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

@ -1,26 +0,0 @@
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,12 +1,11 @@
import { isArray, isEnum, isString } from 'class-validator';
export function isPermissionsArray( export function isPermissionsArray(
value: any, value: any,
permissionsList: string[], permissionsList: string[],
): value is string[] { ): value is string[] {
if (!isArray(value)) return false; if (!Array.isArray(value)) return false;
if (!value.every((item: unknown) => isString(item))) return false; if (!value.every((item: unknown) => typeof item === 'string')) return false;
if (!value.every((item: string) => isEnum(item, permissionsList))) if (!value.every((item: string) => permissionsList.includes(item)))
return false; return false;
return true; return true;
} }

View file

@ -1,4 +1,3 @@
import { IsInt, Min } from 'class-validator'; import { z } from 'zod';
import { CombinePDecorators } from '../util/decorator';
export const IsPosInt = CombinePDecorators(IsInt(), Min(0)); export const IsPosInt = () => z.number().int().nonnegative();

View file

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

View file

@ -1,8 +1,4 @@
import { IsAlphanumeric, IsString, Length } from 'class-validator'; import { z } from 'zod';
import { CombinePDecorators } from '../util/decorator'; import { AlphaNumeric } from '../util/common-regex';
export const IsRoleName = CombinePDecorators( export const IsRoleName = () => z.string().min(4).max(32).regex(AlphaNumeric);
IsString(),
Length(4, 32),
IsAlphanumeric(),
);

View file

@ -1,9 +1,3 @@
import { import { z } from 'zod';
IsArray, IsString
} from 'class-validator';
import { CombinePDecorators } from '../util/decorator';
export const IsStringList = CombinePDecorators( export const IsStringList = () => z.array(z.string());
IsArray(),
IsString({ each: true }),
);

View file

@ -1,16 +1,9 @@
import { IsAlphanumeric, IsString, Length } from 'class-validator'; import { z } from 'zod';
import { CombinePDecorators } from '../util/decorator'; import { AlphaNumeric } from '../util/common-regex';
// 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 = () => z.string().min(4).max(32).regex(AlphaNumeric);
IsString(),
Length(4, 32),
IsAlphanumeric(),
);
export const IsPlainTextPwd = CombinePDecorators( export const IsPlainTextPwd = () => z.string().min(4).max(1024);
IsString(),
Length(4, 1024),
);

260
yarn.lock
View file

@ -95,10 +95,10 @@
"@angular-devkit/architect" "0.1400.0-next.7" "@angular-devkit/architect" "0.1400.0-next.7"
rxjs "6.6.7" rxjs "6.6.7"
"@angular-devkit/core@13.2.5": "@angular-devkit/core@13.3.0":
version "13.2.5" version "13.3.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.2.5.tgz#376f3d205a75cd381191ddabfe3c368e0e9a7d82" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.0.tgz#ad15f2a6b72599a3e22194ebf2decc35e91e592c"
integrity sha512-WuWp/1R0FtCHPBcJLF13lTLHETtDGFUX0ULfGPRaYB5OVCSQcovVp5UbZTTy/Ss3ub3EOEmJlU8kMJfBrWuq+A== integrity sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w==
dependencies: dependencies:
ajv "8.9.0" ajv "8.9.0"
ajv-formats "2.1.1" ajv-formats "2.1.1"
@ -107,10 +107,10 @@
rxjs "6.6.7" rxjs "6.6.7"
source-map "0.7.3" source-map "0.7.3"
"@angular-devkit/core@13.3.0": "@angular-devkit/core@13.3.1":
version "13.3.0" version "13.3.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.0.tgz#ad15f2a6b72599a3e22194ebf2decc35e91e592c" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.1.tgz#e79237d9cffedcc32c59702131e700eb52de76e2"
integrity sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w== integrity sha512-eXAcQaP1mn6rnQb+5bv5NsamY6b34UYM7G+S154Hnma6CTTSGBtcmoNAJs8cekuFqWlw7YgpB/e15jR5OLPkDA==
dependencies: dependencies:
ajv "8.9.0" ajv "8.9.0"
ajv-formats "2.1.1" ajv-formats "2.1.1"
@ -143,17 +143,6 @@
minimist "1.2.5" minimist "1.2.5"
symbol-observable "4.0.0" symbol-observable "4.0.0"
"@angular-devkit/schematics@13.2.5":
version "13.2.5"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.2.5.tgz#d1425ab7ce38c835e776329bea61925b333f53b8"
integrity sha512-kAye6VYiF9JQAoeO+BYhy8eT2QOmhB+WLziRjXoFCBxh5+yXTygTVfs9fD5jmIpHmeu4hd2ErSh69yT5xWcD9g==
dependencies:
"@angular-devkit/core" "13.2.5"
jsonc-parser "3.0.0"
magic-string "0.25.7"
ora "5.4.1"
rxjs "6.6.7"
"@angular-devkit/schematics@13.3.0": "@angular-devkit/schematics@13.3.0":
version "13.3.0" version "13.3.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.0.tgz#c79dba627d1ec4cef6d38bc282aeb7447446c94c" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.0.tgz#c79dba627d1ec4cef6d38bc282aeb7447446c94c"
@ -165,6 +154,17 @@
ora "5.4.1" ora "5.4.1"
rxjs "6.6.7" rxjs "6.6.7"
"@angular-devkit/schematics@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.1.tgz#808af593cd59c0de76361b72346a506cd1624a5a"
integrity sha512-DxXMjlq/sALcHuONZRMTBX5k30XPfN4b6Ue4k7Xl8JKZqyHhEzfXaZzgD9u2cwb7wybKEeF/BZ5eJd8JG525og==
dependencies:
"@angular-devkit/core" "13.3.1"
jsonc-parser "3.0.0"
magic-string "0.25.7"
ora "5.4.1"
rxjs "6.6.7"
"@angular-devkit/schematics@14.0.0-next.7": "@angular-devkit/schematics@14.0.0-next.7":
version "14.0.0-next.7" version "14.0.0-next.7"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.0.0-next.7.tgz#fce33ffbd4dcfb048f0969b0d49dc884ce6ecfc1" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.0.0-next.7.tgz#fce33ffbd4dcfb048f0969b0d49dc884ce6ecfc1"
@ -1211,9 +1211,9 @@
"@cspotcode/source-map-consumer" "0.8.0" "@cspotcode/source-map-consumer" "0.8.0"
"@csstools/postcss-color-function@^1.0.3": "@csstools/postcss-color-function@^1.0.3":
version "1.0.3" version "1.1.0"
resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.0.3.tgz#251c961a852c99e9aabdbbdbefd50e9a96e8a9ff" resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.0.tgz#229966327747f58fbe586de35daa139db3ce1e5d"
integrity sha512-J26I69pT2B3MYiLY/uzCGKVJyMYVg9TCpXkWsRlt+Yfq+nELUEm72QXIMYXs4xA9cJA4Oqs2EylrfokKl3mJEQ== integrity sha512-5D5ND/mZWcQoSfYnSPsXtuiFxhzmhxt6pcjrFLJyldj+p0ZN2vvRpYNX+lahFTtMhAYOa2WmkdGINr0yP0CvGA==
dependencies: dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0" "@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
@ -1241,11 +1241,11 @@
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
"@csstools/postcss-is-pseudo-class@^2.0.1": "@csstools/postcss-is-pseudo-class@^2.0.1":
version "2.0.1" version "2.0.2"
resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.1.tgz#472fff2cf434bdf832f7145b2a5491587e790c9e" resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.2.tgz#a834ca11a43d6ed9bc9e3ff53c80d490a4b1aaad"
integrity sha512-Og5RrTzwFhrKoA79c3MLkfrIBYmwuf/X83s+JQtz/Dkk/MpsaKtqHV1OOzYkogQ+tj3oYp5Mq39XotBXNqVc3Q== integrity sha512-L9h1yxXMj7KpgNzlMrw3isvHJYkikZgZE4ASwssTnGEH8tm50L6QsM9QQT5wR4/eO5mU0rN5axH7UzNxEYg5CA==
dependencies: dependencies:
postcss-selector-parser "^6.0.9" postcss-selector-parser "^6.0.10"
"@csstools/postcss-normalize-display-values@^1.0.0": "@csstools/postcss-normalize-display-values@^1.0.0":
version "1.0.0" version "1.0.0"
@ -1255,9 +1255,9 @@
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
"@csstools/postcss-oklab-function@^1.0.2": "@csstools/postcss-oklab-function@^1.0.2":
version "1.0.2" version "1.1.0"
resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.0.2.tgz#87cd646e9450347a5721e405b4f7cc35157b7866" resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.0.tgz#e9a269487a292e0930760948e923e1d46b638ee6"
integrity sha512-QwhWesEkMlp4narAwUi6pgc6kcooh8cC7zfxa9LSQNYXqzcdNUtNBzbGc5nuyAVreb7uf5Ox4qH1vYT3GA1wOg== integrity sha512-e/Q5HopQzmnQgqimG9v3w2IG4VRABsBq3itOcn4bnm+j4enTgQZ0nWsaH/m9GV2otWGQ0nwccYL5vmLKyvP1ww==
dependencies: dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0" "@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
@ -1474,12 +1474,12 @@
tslib "2.3.1" tslib "2.3.1"
"@nestjs/schematics@^8.0.3", "@nestjs/schematics@^8.0.8": "@nestjs/schematics@^8.0.3", "@nestjs/schematics@^8.0.8":
version "8.0.8" version "8.0.9"
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.8.tgz#b1612b535c1007d1503b7e624795708055460432" resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.9.tgz#796e06c14ba43930abb12cde0e0144b16925a6a3"
integrity sha512-xIIb5YnMQN/OJQ68+MCapy2bXvTxSWgINoqQbyZWkLL/yTIuROvZCdtV850NPGyr7f7l93VBP0ZPitbFIexy3Q== integrity sha512-2YNRFWyQCxKaK7XTKBRKecERKQj2cg9FLxsHwAf9beryXxVmciTmHV0gHCW7Pfatj38R50QFGPN71wuyyZIC5g==
dependencies: dependencies:
"@angular-devkit/core" "13.2.5" "@angular-devkit/core" "13.3.1"
"@angular-devkit/schematics" "13.2.5" "@angular-devkit/schematics" "13.3.1"
fs-extra "10.0.1" fs-extra "10.0.1"
jsonc-parser "3.0.0" jsonc-parser "3.0.0"
pluralize "8.0.0" pluralize "8.0.0"
@ -1756,9 +1756,9 @@
"@types/node" "*" "@types/node" "*"
"@types/jasmine@~4.0.1": "@types/jasmine@~4.0.1":
version "4.0.1" version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-4.0.1.tgz#be74714a651bc76e11fbc66c55dcfacae872361f" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-4.0.2.tgz#9a03589f6aa00ef42a90a8d37f09c893efced14c"
integrity sha512-6KtN9P42PfhUSxybWY0iG6wpMJEoMMXyd9qi06EiOf5p6fOwAj9t/BwBEkx0Ys47oxAbMKv9sKqTV9igHYeUsQ== integrity sha512-mSPIWhDyQ4nzYdR6Ixy15VhVKMVw93mSUlQxxpVb4S9Hj90lBvg+7kkBw23uYcv8CESPPXit+u3cARYcPeC8Jg==
"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.11" version "7.0.11"
@ -2510,7 +2510,7 @@ bl@^4.1.0:
inherits "^2.0.4" inherits "^2.0.4"
readable-stream "^3.4.0" readable-stream "^3.4.0"
body-parser@1.19.2, body-parser@^1.19.0: body-parser@1.19.2:
version "1.19.2" version "1.19.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e"
integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==
@ -2526,6 +2526,24 @@ body-parser@1.19.2, body-parser@^1.19.0:
raw-body "2.4.3" raw-body "2.4.3"
type-is "~1.6.18" type-is "~1.6.18"
body-parser@^1.19.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==
dependencies:
bytes "3.1.2"
content-type "~1.0.4"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.10.3"
raw-body "2.5.1"
type-is "~1.6.18"
unpipe "1.0.0"
bonjour@^3.5.0: bonjour@^3.5.0:
version "3.5.0" version "3.5.0"
resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@ -2677,9 +2695,9 @@ camelcase@^5.3.1:
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caniuse-lite@^1.0.30001317: caniuse-lite@^1.0.30001317:
version "1.0.30001323" version "1.0.30001325"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz#a451ff80dec7033016843f532efda18f02eec011" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz#2b4ad19b77aa36f61f2eaf72e636d7481d55e606"
integrity sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA== integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
chalk@3.0.0: chalk@3.0.0:
version "3.0.0" version "3.0.0"
@ -2736,19 +2754,6 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
class-transformer@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
class-validator@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.2.tgz#64b031e9f3f81a1e1dcd04a5d604734608b24143"
integrity sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==
dependencies:
libphonenumber-js "^1.9.43"
validator "^13.7.0"
clean-stack@^2.0.0: clean-stack@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@ -3206,6 +3211,11 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
depd@^1.1.2, depd@~1.1.2: depd@^1.1.2, depd@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -3216,6 +3226,11 @@ dependency-graph@^0.11.0:
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
destroy@~1.0.4: destroy@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@ -4424,6 +4439,17 @@ http-errors@1.8.1:
statuses ">= 1.5.0 < 2" statuses ">= 1.5.0 < 2"
toidentifier "1.0.1" toidentifier "1.0.1"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
http-errors@~1.6.2: http-errors@~1.6.2:
version "1.6.3" version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
@ -5107,11 +5133,6 @@ levn@^0.4.1:
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
type-check "~0.4.0" type-check "~0.4.0"
libphonenumber-js@^1.9.43:
version "1.9.50"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.50.tgz#f5028a2c4cc47a69d69a0de3629afad97a613712"
integrity sha512-cCzQPChw2XbordcO2LKiw5Htx5leHVfFk/EXkxNHqJfFo7Fndcb1kF5wPJpc316vCJhhikedYnVysMh3Sc7Ocw==
license-webpack-plugin@4.0.2: license-webpack-plugin@4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6"
@ -5621,9 +5642,9 @@ node-forge@^1:
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-gyp-build@^4.2.2: node-gyp-build@^4.2.2:
version "4.3.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==
node-gyp@^9.0.0: node-gyp@^9.0.0:
version "9.0.0" version "9.0.0"
@ -5778,6 +5799,11 @@ object-hash@3.0.0:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
object-is@^1.0.1: object-is@^1.0.1:
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
@ -5806,6 +5832,13 @@ obuf@^1.0.0, obuf@^1.1.2:
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
on-finished@~2.3.0: on-finished@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -6351,9 +6384,9 @@ postcss-initial@^4.0.1:
integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==
postcss-lab-function@^4.1.2: postcss-lab-function@^4.1.2:
version "4.1.2" version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz#b75afe43ba9c1f16bfe9bb12c8109cabd55b5fc2" resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.0.tgz#e054e662c6480202f5760887ec1ae0d153357123"
integrity sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q== integrity sha512-Zb1EO9DGYfa3CP8LhINHCcTTCTLI+R3t7AX2mKsDzdgVQ/GkCpHOTgOr6HBHslP7XDdVbqgHW5vvRPMdVANQ8w==
dependencies: dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0" "@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
@ -6406,11 +6439,11 @@ postcss-modules-values@^4.0.0:
icss-utils "^5.0.0" icss-utils "^5.0.0"
postcss-nesting@^10.1.3: postcss-nesting@^10.1.3:
version "10.1.3" version "10.1.4"
resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.3.tgz#f0b1cd7ae675c697ab6a5a5ca1feea4784a2ef77" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.4.tgz#80de9d1c2717bc44df918dd7f118929300192a7a"
integrity sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw== integrity sha512-2ixdQ59ik/Gt1+oPHiI1kHdwEI8lLKEmui9B1nl6163ANLC+GewQn7fXMxJF2JSb4i2MKL96GU8fIiQztK4TTA==
dependencies: dependencies:
postcss-selector-parser "^6.0.9" postcss-selector-parser "^6.0.10"
postcss-opacity-percentage@^1.1.2: postcss-opacity-percentage@^1.1.2:
version "1.1.2" version "1.1.2"
@ -6484,11 +6517,11 @@ postcss-preset-env@7.4.3:
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
postcss-pseudo-class-any-link@^7.1.1: postcss-pseudo-class-any-link@^7.1.1:
version "7.1.1" version "7.1.2"
resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz#534eb1dadd9945eb07830dbcc06fb4d5d865b8e0" resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.2.tgz#81ec491aa43f97f9015e998b7a14263b4630bdf0"
integrity sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg== integrity sha512-76XzEQv3g+Vgnz3tmqh3pqQyRojkcJ+pjaePsyhcyf164p9aZsu3t+NWxkZYbcHLK1ju5Qmalti2jPI5IWCe5w==
dependencies: dependencies:
postcss-selector-parser "^6.0.9" postcss-selector-parser "^6.0.10"
postcss-replace-overflow-wrap@^4.0.0: postcss-replace-overflow-wrap@^4.0.0:
version "4.0.0" version "4.0.0"
@ -6502,7 +6535,7 @@ postcss-selector-not@^5.0.0:
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.9: postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.9:
version "6.0.10" version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
@ -6559,9 +6592,9 @@ prettier-linter-helpers@^1.0.0:
fast-diff "^1.1.2" fast-diff "^1.1.2"
prettier@^2.6.1: prettier@^2.6.1:
version "2.6.1" version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
pretty-bytes@^5.3.0: pretty-bytes@^5.3.0:
version "5.6.0" version "5.6.0"
@ -6634,6 +6667,13 @@ qjobs@^1.2.0:
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
qs@6.10.3:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
dependencies:
side-channel "^1.0.4"
qs@6.9.7: qs@6.9.7:
version "6.9.7" version "6.9.7"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
@ -6671,6 +6711,16 @@ raw-body@2.4.3:
iconv-lite "0.4.24" iconv-lite "0.4.24"
unpipe "1.0.0" unpipe "1.0.0"
raw-body@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
read-cache@^1.0.0: read-cache@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@ -7142,6 +7192,15 @@ shelljs@0.8.5:
interpret "^1.0.0" interpret "^1.0.0"
rechoir "^0.6.2" rechoir "^0.6.2"
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7" version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@ -7345,6 +7404,11 @@ ssri@^8.0.1:
dependencies: dependencies:
minipass "^3.1.1" minipass "^3.1.1"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@ -7876,11 +7940,6 @@ validate-npm-package-name@^4.0.0:
dependencies: dependencies:
builtins "^5.0.0" builtins "^5.0.0"
validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
vary@^1, vary@^1.1.2, vary@~1.1.2: vary@^1, vary@^1.1.2, vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -7990,7 +8049,7 @@ webpack-subresource-integrity@5.1.0:
dependencies: dependencies:
typed-assert "^1.0.8" typed-assert "^1.0.8"
webpack@5.70.0, webpack@^5.70.0: webpack@5.70.0:
version "5.70.0" version "5.70.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d"
integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==
@ -8020,6 +8079,36 @@ webpack@5.70.0, webpack@^5.70.0:
watchpack "^2.3.1" watchpack "^2.3.1"
webpack-sources "^3.2.3" webpack-sources "^3.2.3"
webpack@^5.70.0:
version "5.71.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.71.0.tgz#b01fcf379570b8c5ee06ca06c829ca168c951884"
integrity sha512-g4dFT7CFG8LY0iU5G8nBL6VlkT21Z7dcYDpJAEJV5Q1WLb9UwnFbrem1k7K52ILqEmomN7pnzWFxxE6SlDY56A==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
acorn "^8.4.1"
acorn-import-assertions "^1.7.6"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.9.2"
es-module-lexer "^0.9.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9"
json-parse-better-errors "^1.0.2"
loader-runner "^4.2.0"
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^3.1.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.1.3"
watchpack "^2.3.1"
webpack-sources "^3.2.3"
websocket-driver@>=0.5.1, websocket-driver@^0.7.4: websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
version "0.7.4" version "0.7.4"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
@ -8183,6 +8272,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.14.3:
version "3.14.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.3.tgz#60e86341c05883c281fe96a0e79acea48a09f123"
integrity sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ==
zone.js@~0.11.5: zone.js@~0.11.5:
version "0.11.5" version "0.11.5"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.5.tgz#ab0b449e91fadb5ebb2db189ffe1b7b6048dc8b1" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.5.tgz#ab0b449e91fadb5ebb2db189ffe1b7b6048dc8b1"