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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,8 @@
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
export const InjectRequest = createParamDecorator(
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => {
return {
req: ctx.switchToHttp().getRequest(),
data,
};
async (data: any, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest();
},
);

View file

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

View file

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

View file

@ -2,25 +2,82 @@ import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor
InternalServerErrorException,
Logger,
NestInterceptor,
Optional
} 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';
// This interceptor will neatly wrap any json response made within nest
export interface ZodValidationInterceptorOptions {
strict?: boolean;
}
@Injectable()
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> {
return next.handle().pipe(
map((data) => {
if (data instanceof Buffer) {
return data;
} 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 response: ApiResponse<any> = {
const response = {
success: true,
statusCode: status,
timestamp: new Date().toISOString(),
@ -29,10 +86,5 @@ export class SuccessInterceptor<T> implements NestInterceptor {
};
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 {
FastifyAdapter,
@ -6,12 +5,12 @@ import {
} from '@nestjs/platform-fastify';
import fastifyHelmet from 'fastify-helmet';
import * as multipart from 'fastify-multipart';
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
import { AppModule } from './app.module';
import { UsersService } from './collections/userdb/userdb.service';
import { HostConfigService } from './config/early/host.config.service';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor';
import { ZodValidationPipe } from './layers/validate/zod-validator.pipe';
import { PicsurLoggerService } from './logger/logger.service';
import { MainAuthGuard } from './managers/auth/guards/main.guard';
import { HelmetOptions } from './security';
@ -31,14 +30,11 @@ async function bootstrap() {
bufferLogs: true,
},
);
// app.enableCors({
// origin: 'self'
// });
// Configure nest app
app.useGlobalFilters(new MainExceptionFilter());
app.useGlobalInterceptors(new SuccessInterceptor());
app.useGlobalPipes(new ValidationPipe(ValidateOptions));
app.useGlobalInterceptors(new SuccessInterceptor(app.get(Reflector)));
app.useGlobalPipes(new ZodValidationPipe());
app.useGlobalGuards(
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,27 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { EImageSchema } from 'picsur-shared/dist/entities/image.entity';
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()
export class EImageBackend extends EImage {
export class EImageBackend implements OverriddenEImage {
@PrimaryGeneratedColumn('uuid')
override id?: string;
id?: string;
@Index()
@Column({ unique: true, nullable: false })
override hash: string;
hash: string;
@Column({ nullable: false })
override mime: string;
mime: string;
// Binary data
@Column({ type: 'bytea', nullable: false, select: false })
@IsOptional()
@IsNotEmpty()
// @ts-ignore
override data?: Buffer;
data?: Buffer;
}

View file

@ -3,14 +3,14 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { Permissions } from '../dto/permissions.dto';
@Entity()
export class ERoleBackend extends ERole {
@PrimaryGeneratedColumn("uuid")
override id?: string;
export class ERoleBackend implements ERole {
@PrimaryGeneratedColumn('uuid')
id?: string;
@Index()
@Column({ nullable: false, unique: true })
override name: string;
name: string;
@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';
@Entity()
export class ESysPreferenceBackend extends ESysPreference {
@PrimaryGeneratedColumn("uuid")
override id?: string;
export class ESysPreferenceBackend implements ESysPreference {
@PrimaryGeneratedColumn('uuid')
id?: string;
@Index()
@Column({ nullable: false, unique: true })
override key: string;
key: string;
@Column({ nullable: false })
override value: string;
value: string;
}

View file

@ -1,25 +1,27 @@
import { IsOptional, IsString } from 'class-validator';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { EUserSchema } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { z } from 'zod';
// 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()
export class EUserBackend extends EUser {
@PrimaryGeneratedColumn("uuid")
override id?: string;
export class EUserBackend implements OverriddenEUser {
@PrimaryGeneratedColumn('uuid', {})
id?: string;
@Index()
@Column({ nullable: false, unique: true })
override username: string;
username: string;
@Column('text', { nullable: false, array: true })
override roles: string[];
roles: string[];
@Column({ nullable: false, select: false })
@IsOptional()
@IsString()
// @ts-ignore
override hashedPassword?: string;
hashedPassword?: string;
}

View file

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

View file

@ -1,7 +1,7 @@
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
export default interface AuthFasityRequest extends FastifyRequest {
user: EUserBackend;
user: EUser;
}

View file

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

View file

@ -1,64 +1,48 @@
import { BusboyFileStream } from '@fastify/busboy';
import { HttpException } from '@nestjs/common';
import { IsDefined, IsNotEmpty, IsString } from 'class-validator';
import { MultipartFile } from 'fastify-multipart';
import { z } from 'zod';
export class MultiPartFileDto {
@IsString()
@IsNotEmpty()
fieldname: string;
export const MultiPartFileDtoSchema = z.object({
fieldname: z.string(),
encoding: z.string(),
filename: z.string(),
mimetype: z.string(),
toBuffer: z.function(undefined, z.any()),
file: z.any(),
});
export type MultiPartFileDto = z.infer<typeof MultiPartFileDtoSchema>;
@IsString()
@IsNotEmpty()
encoding: string;
@IsString()
@IsNotEmpty()
filename: string;
@IsString()
@IsNotEmpty()
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 () => {
export function CreateMultiPartFileDto(
file: MultipartFile,
exceptionOnFail: HttpException,
): MultiPartFileDto {
return {
fieldname: file.fieldname,
encoding: file.encoding,
filename: file.filename,
mimetype: file.mimetype,
toBuffer: async () => {
try {
return await file.toBuffer();
} catch (e) {
throw exceptionOnFail;
}
},
file: file.file,
};
this.file = file.file;
}
}
export class MultiPartFieldDto {
@IsString()
@IsNotEmpty()
fieldname: string;
export const MultiPartFieldDtoSchema = z.object({
fieldname: z.string(),
encoding: z.string(),
value: z.string(),
});
export type MultiPartFieldDto = z.infer<typeof MultiPartFieldDtoSchema>;
@IsString()
@IsNotEmpty()
encoding: string;
@IsString()
@IsNotEmpty()
value: string;
constructor(file: MultipartFile) {
this.fieldname = file.fieldname;
this.encoding = file.encoding;
this.value = (file as any).value;
}
export function CreateMultiPartFieldDto(file: MultipartFile): MultiPartFieldDto {
return {
fieldname: file.fieldname,
encoding: file.encoding,
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 { plainToClass } from 'class-transformer';
import {
AllPermissionsResponse,
InfoResponse
} from 'picsur-shared/dist/dto/api/info.dto';
import { HostConfigService } from '../../../config/early/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator';
import { Returns } from '../../../decorators/returns.decorator';
import { PermissionsList } from '../../../models/dto/permissions.dto';
@Controller('api/info')
@ -14,6 +14,7 @@ export class InfoController {
constructor(private hostConfig: HostConfigService) {}
@Get()
@Returns(InfoResponse)
async getInfo(): Promise<InfoResponse> {
return {
demo: this.hostConfig.isDemo(),
@ -24,11 +25,10 @@ export class InfoController {
// List all available permissions
@Get('permissions')
@Returns(AllPermissionsResponse)
async getPermissions(): Promise<AllPermissionsResponse> {
const result: AllPermissionsResponse = {
return {
permissions: PermissionsList,
};
return plainToClass(AllPermissionsResponse, result);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,13 @@ import {
Injectable,
PipeTransform
} from '@nestjs/common';
import { isHash } from 'class-validator';
import { SHA256 } from 'picsur-shared/dist/util/common-regex';
@Injectable()
export class ImageIdValidator implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
if (isHash(value, 'sha256')) {
return value;
}
// Check regex for sha256
if (SHA256.test(value)) return value;
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 { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { Returns } from '../../decorators/returns.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto';
import {
ImageUploadDto
} from '../../models/requests/imageroute.dto';
import { EImageBackend2EImage } from '../../models/transformers/image.transformer';
import { ImageIdValidator } from './imageid.validator';
@ -45,6 +48,7 @@ export class ImageController {
}
@Get('meta/:hash')
@Returns(ImageMetaResponse)
async getImageMeta(
@Param('hash', ImageIdValidator) hash: string,
): Promise<ImageMetaResponse> {
@ -58,9 +62,10 @@ export class ImageController {
}
@Post()
@Returns(ImageMetaResponse)
@RequiredPermissions(Permission.ImageUpload)
async uploadImage(
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
@MultiPart() multipart: ImageUploadDto,
): Promise<ImageMetaResponse> {
const fileBuffer = await multipart.image.toBuffer();
const image = await this.imagesService.upload(fileBuffer);

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
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 {
AsyncFailable,
Fail, HasFailed
} from 'picsur-shared/dist/types';
import { SemVer } from 'picsur-shared/dist/util/common-regex';
import { BehaviorSubject } from 'rxjs';
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
import { UtilService } from 'src/app/util/util.service';
@ -34,10 +33,8 @@ export class InfoService {
const response = await this.api.get(InfoResponse, '/api/info');
if (HasFailed(response)) return response;
const info = plainToClass(ServerInfo, response);
this.infoSubject.next(info);
return info;
this.infoSubject.next(response);
return response;
}
public getFrontendVersion(): string {
@ -53,7 +50,7 @@ export class InfoService {
const serverVersion = info.version;
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}`);
}

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import {
GetSyspreferenceResponse,
GetSysPreferenceResponse,
MultipleSysPreferencesResponse,
UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse
@ -73,12 +73,12 @@ export class SysprefService {
public async getPreference(
key: string
): AsyncFailable<GetSyspreferenceResponse> {
): AsyncFailable<GetSysPreferenceResponse> {
if (!this.hasPermission)
return Fail('You do not have permission to edit system preferences');
const response = await this.api.get(
GetSyspreferenceResponse,
GetSysPreferenceResponse,
`/api/pref/sys/${key}`
);

View file

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

View file

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

View file

@ -1,36 +1,28 @@
import {
IsBoolean, IsInt,
IsNotEmpty,
IsString,
Max,
Min
} from 'class-validator';
import z from 'zod';
class BaseApiResponse<T extends Object, W extends boolean> {
@IsBoolean()
success: W;
const ApiResponseBase = z.object({
statusCode: z.number().min(0).max(600),
timestamp: z.string(),
});
@IsInt()
@Min(0)
@Max(1000)
statusCode: number;
const ApiSuccessResponse = <T extends z.AnyZodObject>(data: T) =>
ApiResponseBase.merge(
z.object({
success: z.literal(true),
data,
}),
);
@IsString()
timestamp: string;
const ApiErrorResponse = ApiResponseBase.merge(
z.object({
success: z.literal(false),
data: z.object({
message: z.string(),
}),
}),
);
@IsNotEmpty()
data: T;
}
export const ApiResponseSchema = <T extends z.AnyZodObject>(data: T) =>
ApiErrorResponse.or(ApiSuccessResponse(data));
export class ApiSuccessResponse<T extends Object> extends BaseApiResponse<
T,
true
> {}
export class ApiErrorData {
@IsString()
message: string;
}
export class ApiErrorResponse extends BaseApiResponse<ApiErrorData, false> {}
export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
export type ApiErrorResponse = z.infer<typeof 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';
export class InfoResponse {
@IsBoolean()
production: boolean;
export const InfoResponseSchema = z.object({
production: z.boolean(),
demo: z.boolean(),
version: string().regex(SemVer),
});
export class InfoResponse extends createZodDto(InfoResponseSchema) {}
@IsBoolean()
demo: boolean;
@IsSemVer()
version: string;
}
// AllPermissions
export class AllPermissionsResponse {
@IsStringList()
permissions: string[];
}
// Allpermissions
export const AllPermissionsResponseSchema = z.object({
permissions: IsStringList(),
});
export class AllPermissionsResponse extends createZodDto(AllPermissionsResponseSchema) {}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { IsPrefValue } from '../validators/pref-value.validator';
// Variable value type
export type PrefValueType = string | number | boolean;
export type PrefValueTypeStrings = 'string' | 'number' | 'boolean';
export const PrefValueTypes = ['string', 'number', 'boolean'];
export const PrefValueTypes = tuple('string', 'number', 'boolean');
// Decoded Representations
export class DecodedSysPref {
@IsString()
key: string;
export const DecodedSysPrefSchema = z.object({
key: z.string(),
value: IsPrefValue(),
type: z.enum(PrefValueTypes),
})
export type DecodedSysPref = z.infer<typeof DecodedSysPrefSchema>;
@IsPrefValue()
value: PrefValueType;
export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge(z.object({
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 { IsNotDefined } from '../validators/not-defined.validator';
export class EImage {
@IsOptional()
@IsEntityID()
id?: string;
@IsHash('sha256')
hash: string;
// Because typescript does not support exact types, we have to do this stupidness
@IsNotDefined()
data: undefined;
@IsString()
mime: string;
}
export const EImageSchema = z.object({
id: IsEntityID().optional(),
hash: z.string().regex(SHA256),
data: z.undefined(),
mime: z.string(),
});
export type EImage = z.infer<typeof EImageSchema>;

View file

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

View file

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

View file

@ -1,32 +1,19 @@
import { IsOptional } from 'class-validator';
import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator';
import { IsNotDefined } from '../validators/not-defined.validator';
import { IsStringList } from '../validators/string-list.validator';
import { IsPlainTextPwd, IsUsername } from '../validators/user.validators';
export class SimpleUser {
@IsUsername()
username: string;
export const SimpleUserSchema = z.object({
username: IsUsername(),
password: IsPlainTextPwd(),
roles: IsStringList(),
});
export type SimpleUser = z.infer<typeof SimpleUserSchema>;
@IsPlainTextPwd()
password: string;
@IsStringList()
roles: string[];
}
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;
}
export const EUserSchema = z.object({
id: IsEntityID().optional(),
username: IsUsername(),
roles: IsStringList(),
hashedPassword: z.undefined(),
});
export type EUser = z.infer<typeof EUserSchema>;

View file

@ -1,18 +1,11 @@
import { IsOptional, IsString } from 'class-validator';
import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator';
import { IsPosInt } from '../validators/positive-int.validator';
export class EUsrPreference {
@IsOptional()
@IsEntityID()
id?: string;
@IsString()
key: string;
@IsString()
value: string;
@IsPosInt()
userId: number;
}
export const EUsrPreferenceSchema = z.object({
id: IsEntityID().optional(),
key: z.string(),
value: z.string(),
userId: IsPosInt(),
})
export type EUsrPreference = z.infer<typeof EUsrPreferenceSchema>;

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 { CombinePDecorators } from '../util/decorator';
import { z } from 'zod';
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(
value: any,
permissionsList: string[],
): value is string[] {
if (!isArray(value)) return false;
if (!value.every((item: unknown) => isString(item))) return false;
if (!value.every((item: string) => isEnum(item, permissionsList)))
if (!Array.isArray(value)) return false;
if (!value.every((item: unknown) => typeof item === 'string')) return false;
if (!value.every((item: string) => permissionsList.includes(item)))
return false;
return true;
}

View file

@ -1,4 +1,3 @@
import { IsInt, Min } from 'class-validator';
import { CombinePDecorators } from '../util/decorator';
import { z } from 'zod';
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 { PrefValueTypes } from '../dto/preferences.dto';
import { z } from 'zod';
export function isPrefValue(value: any, args: ValidationArguments) {
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,
},
});
};
}
export const IsPrefValue = () => z.string().or(z.number().or(z.boolean()));

View file

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

View file

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

View file

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

260
yarn.lock
View file

@ -95,10 +95,10 @@
"@angular-devkit/architect" "0.1400.0-next.7"
rxjs "6.6.7"
"@angular-devkit/core@13.2.5":
version "13.2.5"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.2.5.tgz#376f3d205a75cd381191ddabfe3c368e0e9a7d82"
integrity sha512-WuWp/1R0FtCHPBcJLF13lTLHETtDGFUX0ULfGPRaYB5OVCSQcovVp5UbZTTy/Ss3ub3EOEmJlU8kMJfBrWuq+A==
"@angular-devkit/core@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.0.tgz#ad15f2a6b72599a3e22194ebf2decc35e91e592c"
integrity sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w==
dependencies:
ajv "8.9.0"
ajv-formats "2.1.1"
@ -107,10 +107,10 @@
rxjs "6.6.7"
source-map "0.7.3"
"@angular-devkit/core@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.0.tgz#ad15f2a6b72599a3e22194ebf2decc35e91e592c"
integrity sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w==
"@angular-devkit/core@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.1.tgz#e79237d9cffedcc32c59702131e700eb52de76e2"
integrity sha512-eXAcQaP1mn6rnQb+5bv5NsamY6b34UYM7G+S154Hnma6CTTSGBtcmoNAJs8cekuFqWlw7YgpB/e15jR5OLPkDA==
dependencies:
ajv "8.9.0"
ajv-formats "2.1.1"
@ -143,17 +143,6 @@
minimist "1.2.5"
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":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.0.tgz#c79dba627d1ec4cef6d38bc282aeb7447446c94c"
@ -165,6 +154,17 @@
ora "5.4.1"
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":
version "14.0.0-next.7"
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"
"@csstools/postcss-color-function@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.0.3.tgz#251c961a852c99e9aabdbbdbefd50e9a96e8a9ff"
integrity sha512-J26I69pT2B3MYiLY/uzCGKVJyMYVg9TCpXkWsRlt+Yfq+nELUEm72QXIMYXs4xA9cJA4Oqs2EylrfokKl3mJEQ==
version "1.1.0"
resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.0.tgz#229966327747f58fbe586de35daa139db3ce1e5d"
integrity sha512-5D5ND/mZWcQoSfYnSPsXtuiFxhzmhxt6pcjrFLJyldj+p0ZN2vvRpYNX+lahFTtMhAYOa2WmkdGINr0yP0CvGA==
dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0"
@ -1241,11 +1241,11 @@
postcss-value-parser "^4.2.0"
"@csstools/postcss-is-pseudo-class@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.1.tgz#472fff2cf434bdf832f7145b2a5491587e790c9e"
integrity sha512-Og5RrTzwFhrKoA79c3MLkfrIBYmwuf/X83s+JQtz/Dkk/MpsaKtqHV1OOzYkogQ+tj3oYp5Mq39XotBXNqVc3Q==
version "2.0.2"
resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.2.tgz#a834ca11a43d6ed9bc9e3ff53c80d490a4b1aaad"
integrity sha512-L9h1yxXMj7KpgNzlMrw3isvHJYkikZgZE4ASwssTnGEH8tm50L6QsM9QQT5wR4/eO5mU0rN5axH7UzNxEYg5CA==
dependencies:
postcss-selector-parser "^6.0.9"
postcss-selector-parser "^6.0.10"
"@csstools/postcss-normalize-display-values@^1.0.0":
version "1.0.0"
@ -1255,9 +1255,9 @@
postcss-value-parser "^4.2.0"
"@csstools/postcss-oklab-function@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.0.2.tgz#87cd646e9450347a5721e405b4f7cc35157b7866"
integrity sha512-QwhWesEkMlp4narAwUi6pgc6kcooh8cC7zfxa9LSQNYXqzcdNUtNBzbGc5nuyAVreb7uf5Ox4qH1vYT3GA1wOg==
version "1.1.0"
resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.0.tgz#e9a269487a292e0930760948e923e1d46b638ee6"
integrity sha512-e/Q5HopQzmnQgqimG9v3w2IG4VRABsBq3itOcn4bnm+j4enTgQZ0nWsaH/m9GV2otWGQ0nwccYL5vmLKyvP1ww==
dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0"
@ -1474,12 +1474,12 @@
tslib "2.3.1"
"@nestjs/schematics@^8.0.3", "@nestjs/schematics@^8.0.8":
version "8.0.8"
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.8.tgz#b1612b535c1007d1503b7e624795708055460432"
integrity sha512-xIIb5YnMQN/OJQ68+MCapy2bXvTxSWgINoqQbyZWkLL/yTIuROvZCdtV850NPGyr7f7l93VBP0ZPitbFIexy3Q==
version "8.0.9"
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.9.tgz#796e06c14ba43930abb12cde0e0144b16925a6a3"
integrity sha512-2YNRFWyQCxKaK7XTKBRKecERKQj2cg9FLxsHwAf9beryXxVmciTmHV0gHCW7Pfatj38R50QFGPN71wuyyZIC5g==
dependencies:
"@angular-devkit/core" "13.2.5"
"@angular-devkit/schematics" "13.2.5"
"@angular-devkit/core" "13.3.1"
"@angular-devkit/schematics" "13.3.1"
fs-extra "10.0.1"
jsonc-parser "3.0.0"
pluralize "8.0.0"
@ -1756,9 +1756,9 @@
"@types/node" "*"
"@types/jasmine@~4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-4.0.1.tgz#be74714a651bc76e11fbc66c55dcfacae872361f"
integrity sha512-6KtN9P42PfhUSxybWY0iG6wpMJEoMMXyd9qi06EiOf5p6fOwAj9t/BwBEkx0Ys47oxAbMKv9sKqTV9igHYeUsQ==
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-4.0.2.tgz#9a03589f6aa00ef42a90a8d37f09c893efced14c"
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":
version "7.0.11"
@ -2510,7 +2510,7 @@ bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
body-parser@1.19.2, body-parser@^1.19.0:
body-parser@1.19.2:
version "1.19.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e"
integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==
@ -2526,6 +2526,24 @@ body-parser@1.19.2, body-parser@^1.19.0:
raw-body "2.4.3"
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:
version "3.5.0"
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==
caniuse-lite@^1.0.30001317:
version "1.0.30001323"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz#a451ff80dec7033016843f532efda18f02eec011"
integrity sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==
version "1.0.30001325"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz#2b4ad19b77aa36f61f2eaf72e636d7481d55e606"
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
chalk@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"
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:
version "2.2.0"
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"
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:
version "1.1.2"
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"
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:
version "1.0.4"
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"
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:
version "1.6.3"
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"
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:
version "4.0.2"
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==
node-gyp-build@^4.2.2:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
version "4.4.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4"
integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==
node-gyp@^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"
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:
version "1.1.5"
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"
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:
version "2.3.0"
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==
postcss-lab-function@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz#b75afe43ba9c1f16bfe9bb12c8109cabd55b5fc2"
integrity sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q==
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.0.tgz#e054e662c6480202f5760887ec1ae0d153357123"
integrity sha512-Zb1EO9DGYfa3CP8LhINHCcTTCTLI+R3t7AX2mKsDzdgVQ/GkCpHOTgOr6HBHslP7XDdVbqgHW5vvRPMdVANQ8w==
dependencies:
"@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0"
@ -6406,11 +6439,11 @@ postcss-modules-values@^4.0.0:
icss-utils "^5.0.0"
postcss-nesting@^10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.3.tgz#f0b1cd7ae675c697ab6a5a5ca1feea4784a2ef77"
integrity sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw==
version "10.1.4"
resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.4.tgz#80de9d1c2717bc44df918dd7f118929300192a7a"
integrity sha512-2ixdQ59ik/Gt1+oPHiI1kHdwEI8lLKEmui9B1nl6163ANLC+GewQn7fXMxJF2JSb4i2MKL96GU8fIiQztK4TTA==
dependencies:
postcss-selector-parser "^6.0.9"
postcss-selector-parser "^6.0.10"
postcss-opacity-percentage@^1.1.2:
version "1.1.2"
@ -6484,11 +6517,11 @@ postcss-preset-env@7.4.3:
postcss-value-parser "^4.2.0"
postcss-pseudo-class-any-link@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz#534eb1dadd9945eb07830dbcc06fb4d5d865b8e0"
integrity sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg==
version "7.1.2"
resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.2.tgz#81ec491aa43f97f9015e998b7a14263b4630bdf0"
integrity sha512-76XzEQv3g+Vgnz3tmqh3pqQyRojkcJ+pjaePsyhcyf164p9aZsu3t+NWxkZYbcHLK1ju5Qmalti2jPI5IWCe5w==
dependencies:
postcss-selector-parser "^6.0.9"
postcss-selector-parser "^6.0.10"
postcss-replace-overflow-wrap@^4.0.0:
version "4.0.0"
@ -6502,7 +6535,7 @@ postcss-selector-not@^5.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
@ -6559,9 +6592,9 @@ prettier-linter-helpers@^1.0.0:
fast-diff "^1.1.2"
prettier@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17"
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==
version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
pretty-bytes@^5.3.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"
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:
version "6.9.7"
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"
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:
version "1.0.0"
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"
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:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@ -7345,6 +7404,11 @@ ssri@^8.0.1:
dependencies:
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:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@ -7876,11 +7940,6 @@ validate-npm-package-name@^4.0.0:
dependencies:
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:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -7990,7 +8049,7 @@ webpack-subresource-integrity@5.1.0:
dependencies:
typed-assert "^1.0.8"
webpack@5.70.0, webpack@^5.70.0:
webpack@5.70.0:
version "5.70.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d"
integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==
@ -8020,6 +8079,36 @@ webpack@5.70.0, webpack@^5.70.0:
watchpack "^2.3.1"
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:
version "0.7.4"
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"
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:
version "0.11.5"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.5.tgz#ab0b449e91fadb5ebb2db189ffe1b7b6048dc8b1"