mirror of https://github.com/sylv/micro.git
use zod for config validation
This commit is contained in:
parent
805ebcd4b4
commit
b0352724b2
|
@ -13,7 +13,7 @@
|
|||
"build": "tsc --noEmit && tsup",
|
||||
"lint": "eslint src --fix --cache",
|
||||
"test": "vitest run",
|
||||
"watch": "tsup --watch --onSuccess \"node dist/main.js --inspect --inspect-brk\""
|
||||
"watch": "tsup --watch --onSuccess \"node dist/main.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^9.2.0",
|
||||
|
@ -65,6 +65,7 @@
|
|||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"bytes": "^3.1.2",
|
||||
"chalk": "^5.3.0",
|
||||
"content-range": "^2.0.2",
|
||||
"dedent": "^1.5.1",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
|
@ -80,7 +81,9 @@
|
|||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.1.3"
|
||||
"vitest": "^1.1.3",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
},
|
||||
"mikro-orm": {
|
||||
"useTsNode": true,
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsEmail,
|
||||
IsMimeType,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Max,
|
||||
NotEquals,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import path from 'path';
|
||||
import { expandMime } from '../helpers/expand-mime.js';
|
||||
import { MicroConversion } from './MicroConversion.js';
|
||||
import { MicroEmail } from './MicroEmail.js';
|
||||
import { MicroHost } from './MicroHost.js';
|
||||
import { MicroPurge } from './MicroPurge.js';
|
||||
|
||||
export class MicroConfig {
|
||||
@IsUrl({ require_tld: false, require_protocol: true, protocols: ['postgresql', 'postgres'] })
|
||||
databaseUrl: string;
|
||||
|
||||
@IsString()
|
||||
@NotEquals('YOU_SHALL_NOT_PASS')
|
||||
secret: string;
|
||||
|
||||
@IsEmail()
|
||||
inquiries: string;
|
||||
|
||||
@IsNumber()
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
uploadLimit = bytes.parse('50MB');
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Max(500000)
|
||||
maxPasteLength = 500000;
|
||||
|
||||
@IsMimeType({ each: true })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (!value) return value;
|
||||
const clean = expandMime(value);
|
||||
return new Set(clean);
|
||||
})
|
||||
allowTypes?: Set<string>;
|
||||
|
||||
@IsString()
|
||||
@Transform(({ value }) => path.resolve(value))
|
||||
storagePath: string;
|
||||
|
||||
@IsBoolean()
|
||||
restrictFilesToHost: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => MicroPurge)
|
||||
purge?: MicroPurge;
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => MicroEmail)
|
||||
email: MicroEmail;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsOptional()
|
||||
@Type(() => MicroConversion)
|
||||
conversions?: MicroConversion[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsDefined()
|
||||
@Type(() => MicroHost)
|
||||
hosts: MicroHost[];
|
||||
|
||||
get rootHost() {
|
||||
return this.hosts[0]!;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsMimeType, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { expandMime } from '../helpers/expand-mime.js';
|
||||
|
||||
export class MicroConversion {
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) => {
|
||||
const clean = expandMime(value);
|
||||
return new Set(clean);
|
||||
})
|
||||
from: Set<string>;
|
||||
|
||||
@IsMimeType()
|
||||
to: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
minSize?: number;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { IsEmail, IsObject } from 'class-validator';
|
||||
|
||||
export class MicroEmail {
|
||||
@IsEmail()
|
||||
from: string;
|
||||
|
||||
@IsObject()
|
||||
smtp: Record<string, any>;
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { IsOptional, IsString, IsUrl, Matches } from 'class-validator';
|
||||
import escapeString from 'escape-string-regexp';
|
||||
import { HostService } from '../modules/host/host.service.js';
|
||||
|
||||
export class MicroHost {
|
||||
constructor(url: string, tags?: string[], redirect?: string) {
|
||||
this.url = url;
|
||||
this.tags = tags;
|
||||
this.redirect = redirect;
|
||||
}
|
||||
|
||||
// https://regex101.com/r/ZR9rpp/1
|
||||
@Matches(/^https?:\/\/[\d.:A-z{}-]+$/u)
|
||||
url: string;
|
||||
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
tags?: string[];
|
||||
|
||||
@IsUrl({ require_protocol: true })
|
||||
@IsOptional()
|
||||
redirect?: string;
|
||||
|
||||
get normalised() {
|
||||
return HostService.normaliseHostUrl(this.url);
|
||||
}
|
||||
|
||||
get isWildcard() {
|
||||
return this.url.includes('{{username}}');
|
||||
}
|
||||
|
||||
private _pattern?: RegExp;
|
||||
get pattern() {
|
||||
if (this._pattern) return this._pattern;
|
||||
this._pattern = MicroHost.getWildcardPattern(this.url);
|
||||
return this._pattern;
|
||||
}
|
||||
|
||||
static getWildcardPattern(url: string) {
|
||||
const normalised = HostService.normaliseHostUrl(url);
|
||||
const escaped = escapeString(normalised);
|
||||
const pattern = escaped.replace('\\{\\{username\\}\\}', '(?<username>[a-z0-9-{}]+?)');
|
||||
return new RegExp(`^(https?:\\/\\/)?${pattern}\\/?`, 'u');
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import ms from 'ms';
|
||||
|
||||
export class MicroPurge {
|
||||
@IsNumber()
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
overLimit: number;
|
||||
|
||||
@IsNumber()
|
||||
@Transform(({ value }) => ms(value))
|
||||
afterTime: number;
|
||||
}
|
|
@ -1,20 +1,100 @@
|
|||
import { loadConfig } from '@ryanke/venera';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { MicroConfig } from './classes/MicroConfig.js';
|
||||
import bytes from 'bytes';
|
||||
import c from 'chalk';
|
||||
import { randomBytes } from 'crypto';
|
||||
import dedent from 'dedent';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
import ms from 'ms';
|
||||
import z, { any, array, boolean, number, record, strictObject, string, union } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
import { expandMime } from './helpers/expand-mime.js';
|
||||
import { HostService } from './modules/host/host.service.js';
|
||||
|
||||
export type MicroHost = ReturnType<typeof enhanceHost>;
|
||||
|
||||
const schema = strictObject({
|
||||
databaseUrl: string().startsWith('postgresql://'),
|
||||
secret: string().min(6),
|
||||
inquiries: string().email(),
|
||||
uploadLimit: string().transform(bytes.parse),
|
||||
maxPasteLength: number().default(500000),
|
||||
allowTypes: z
|
||||
.union([array(string()), string()])
|
||||
.optional()
|
||||
.transform((value) => new Set(value ? expandMime(value) : [])),
|
||||
storagePath: string(),
|
||||
restrictFilesToHost: boolean().default(true),
|
||||
purge: strictObject({
|
||||
overLimit: string().transform(bytes.parse),
|
||||
afterTime: string().transform(ms),
|
||||
}).optional(),
|
||||
email: strictObject({
|
||||
from: string().email(),
|
||||
smtp: record(string(), any()),
|
||||
}).optional(),
|
||||
conversions: array(
|
||||
strictObject({
|
||||
from: union([array(string()), string()]).transform((value) => new Set(expandMime(value))),
|
||||
to: string(),
|
||||
minSize: string().transform(bytes.parse).optional(),
|
||||
}),
|
||||
).optional(),
|
||||
hosts: array(
|
||||
strictObject({
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((value) => value.replace(/\/$/, '')),
|
||||
tags: array(string()).optional(),
|
||||
redirect: string().url().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const data = loadConfig('micro');
|
||||
const config = plainToClass(MicroConfig, data, { exposeDefaultValues: true });
|
||||
const errors = validateSync(config, { forbidUnknownValues: true });
|
||||
if (errors.length > 0) {
|
||||
const clean = errors.map((error) => error.toString()).join('\n');
|
||||
console.dir(config, { depth: null });
|
||||
console.error(clean);
|
||||
process.exit(1);
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) {
|
||||
console.dir({ data, error: result.error }, { depth: null });
|
||||
const pretty = fromZodError(result.error);
|
||||
throw new Error(pretty.toString());
|
||||
}
|
||||
|
||||
if (config.rootHost.isWildcard) {
|
||||
const getWildcardPattern = (url: string) => {
|
||||
const normalised = HostService.normaliseHostUrl(url);
|
||||
const escaped = escapeStringRegexp(normalised);
|
||||
const pattern = escaped.replace('\\{\\{username\\}\\}', '(?<username>[a-z0-9-{}]+?)');
|
||||
return new RegExp(`^(https?:\\/\\/)?${pattern}\\/?`, 'u');
|
||||
};
|
||||
|
||||
const enhanceHost = (host: z.infer<typeof schema>['hosts'][0]) => {
|
||||
const isWildcard = host.url.includes('{{username}}');
|
||||
const normalised = HostService.normaliseHostUrl(host.url);
|
||||
const pattern = getWildcardPattern(host.url);
|
||||
|
||||
return {
|
||||
...host,
|
||||
isWildcard,
|
||||
normalised,
|
||||
pattern,
|
||||
};
|
||||
};
|
||||
|
||||
export const config = result.data as Omit<z.infer<typeof schema>, 'hosts'>;
|
||||
export const hosts = result.data.hosts.map(enhanceHost);
|
||||
export const rootHost = hosts[0];
|
||||
|
||||
if (rootHost.isWildcard) {
|
||||
throw new Error(`Root host cannot be a wildcard domain.`);
|
||||
}
|
||||
|
||||
export { config };
|
||||
const disallowed = new Set(['youshallnotpass', 'you_shall_not_pass', 'secret', 'test']);
|
||||
if (disallowed.has(config.secret.toLowerCase())) {
|
||||
const token = randomBytes(24).toString('hex');
|
||||
throw new Error(
|
||||
dedent`
|
||||
${c.redBright.bold('Do not use the default secret.')}
|
||||
Please generate a random, secure secret or you risk anyone being able to impersonate you.
|
||||
If you're lazy, here is a random secret: ${c.underline(token)}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import type { IdentifiedReference } from '@mikro-orm/core';
|
||||
import { BeforeCreate, Entity, type EventArgs, Property } from '@mikro-orm/core';
|
||||
import { BeforeCreate, Entity, Property, type EventArgs } from '@mikro-orm/core';
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { config } from '../config.js';
|
||||
import type { ResourceLocations } from '../types/resource-locations.type.js';
|
||||
import { config, hosts, rootHost } from '../config.js';
|
||||
import type { User } from '../modules/user/user.entity.js';
|
||||
import type { ResourceLocations } from '../types/resource-locations.type.js';
|
||||
import { getHostFromRequest } from './get-host-from-request.js';
|
||||
|
||||
@Entity({ abstract: true })
|
||||
|
@ -31,10 +31,10 @@ export abstract class Resource {
|
|||
}
|
||||
|
||||
getHost() {
|
||||
if (!this.hostname) return config.rootHost;
|
||||
const match = config.hosts.find((host) => host.normalised === this.hostname || host.pattern.test(this.hostname!));
|
||||
if (!this.hostname) return rootHost;
|
||||
const match = hosts.find((host) => host.normalised === this.hostname || host.pattern.test(this.hostname!));
|
||||
if (match) return match;
|
||||
return config.rootHost;
|
||||
return rootHost;
|
||||
}
|
||||
|
||||
getBaseUrl() {
|
||||
|
@ -42,7 +42,7 @@ export abstract class Resource {
|
|||
const host = this.getHost();
|
||||
const hasPlaceholder = host.url.includes('{{username}}');
|
||||
if (hasPlaceholder) {
|
||||
if (!owner) return config.rootHost.url;
|
||||
if (!owner) return rootHost.url;
|
||||
return host.url.replace('{{username}}', owner.username);
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ export abstract class Resource {
|
|||
if (!config.restrictFilesToHost) return true;
|
||||
|
||||
// root host can send all files
|
||||
if (hostname === config.rootHost.normalised) return true;
|
||||
if (hostname === rootHost.normalised) return true;
|
||||
if (this.hostname === hostname) return true;
|
||||
if (this.hostname?.includes('{{username}}')) {
|
||||
// old files have {{username}} in the persisted hostname, migrating them
|
||||
|
|
|
@ -4,7 +4,7 @@ import { config } from '../config.js';
|
|||
const transport = config.email && nodemailer.createTransport(config.email.smtp);
|
||||
|
||||
export const sendMail = (options: Omit<nodemailer.SendMailOptions, 'from'>) => {
|
||||
if (!transport) {
|
||||
if (!transport || !config.email) {
|
||||
throw new Error('No SMTP configuration found');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { config } from '../config.js';
|
||||
import { config, hosts, rootHost } from '../config.js';
|
||||
import { UserId } from './auth/auth.decorators.js';
|
||||
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard.js';
|
||||
import { UserService } from './user/user.service.js';
|
||||
|
@ -26,10 +26,10 @@ export class AppController {
|
|||
allowTypes: config.allowTypes ? [...config.allowTypes?.values()] : undefined,
|
||||
email: !!config.email,
|
||||
rootHost: {
|
||||
url: config.rootHost.url,
|
||||
normalised: config.rootHost.normalised,
|
||||
url: rootHost.url,
|
||||
normalised: rootHost.normalised,
|
||||
},
|
||||
hosts: config.hosts
|
||||
hosts: hosts
|
||||
.filter((host) => {
|
||||
if (!host.tags || !host.tags[0]) return true;
|
||||
return host.tags.every((tag) => tags.includes(tag));
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
import { MicroHost } from '../classes/MicroHost.js';
|
||||
import { config } from '../config.js';
|
||||
import { config, hosts, rootHost, type MicroHost } from '../config.js';
|
||||
import type { ConfigHost } from '../types/config.type.js';
|
||||
import { Config } from '../types/config.type.js';
|
||||
import { CurrentHost, UserId } from './auth/auth.decorators.js';
|
||||
|
@ -28,9 +27,9 @@ export class AppResolver {
|
|||
uploadLimit: config.uploadLimit,
|
||||
allowTypes: config.allowTypes ? [...config.allowTypes?.values()] : [],
|
||||
requireEmails: !!config.email,
|
||||
rootHost: this.filterHost(config.rootHost),
|
||||
rootHost: this.filterHost(rootHost),
|
||||
currentHost: this.filterHost(currentHost),
|
||||
hosts: config.hosts
|
||||
hosts: hosts
|
||||
.filter((host) => {
|
||||
if (!host.tags || !host.tags[0]) return true;
|
||||
return host.tags.every((tag) => tags.includes(tag));
|
||||
|
|
|
@ -4,7 +4,7 @@ import { UseGuards } from '@nestjs/common';
|
|||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import ms from 'ms';
|
||||
import { config } from '../../config.js';
|
||||
import { rootHost } from '../../config.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { UserId } from './auth.decorators.js';
|
||||
import { AuthService, TokenType } from './auth.service.js';
|
||||
|
@ -18,13 +18,13 @@ export class AuthResolver {
|
|||
private static readonly COOKIE_OPTIONS = {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
domain: config.rootHost.normalised.split(':').shift(),
|
||||
secure: config.rootHost.url.startsWith('https'),
|
||||
domain: rootHost.normalised.split(':').shift(),
|
||||
secure: rootHost.url.startsWith('https'),
|
||||
};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User) private readonly userRepo: EntityRepository<User>,
|
||||
private readonly authService: AuthService
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => User)
|
||||
|
@ -32,7 +32,7 @@ export class AuthResolver {
|
|||
@Context() ctx: any,
|
||||
@Args('username') username: string,
|
||||
@Args('password') password: string,
|
||||
@Args('otpCode', { nullable: true }) otpCode?: string
|
||||
@Args('otpCode', { nullable: true }) otpCode?: string,
|
||||
) {
|
||||
const reply = ctx.reply as FastifyReply;
|
||||
const user = await this.authService.authenticateUser(username, password, otpCode);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { config } from '../../config.js';
|
||||
import { rootHost } from '../../config.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
|
@ -31,14 +31,14 @@ export class FileController {
|
|||
private readonly fileService: FileService,
|
||||
private readonly userService: UserService,
|
||||
private readonly hostService: HostService,
|
||||
private readonly linkService: LinkService
|
||||
private readonly linkService: LinkService,
|
||||
) {}
|
||||
|
||||
@Get('file/:fileId')
|
||||
async getFileContent(
|
||||
@Res() reply: FastifyReply,
|
||||
@Param('fileId') fileId: string,
|
||||
@Request() request: FastifyRequest
|
||||
@Request() request: FastifyRequest,
|
||||
) {
|
||||
return this.fileService.sendFile(fileId, request, reply);
|
||||
}
|
||||
|
@ -49,8 +49,8 @@ export class FileController {
|
|||
@UserId() userId: string,
|
||||
@Req() request: FastifyRequest,
|
||||
@Headers('X-Micro-Paste-Shortcut') shortcut: string,
|
||||
@Headers('x-micro-host') hosts = config.rootHost.url,
|
||||
@Query('input') input?: string
|
||||
@Headers('x-micro-host') hosts = rootHost.url,
|
||||
@Query('input') input?: string,
|
||||
) {
|
||||
const user = await this.userService.getUser(userId, true);
|
||||
const host = this.hostService.resolveUploadHost(hosts, user);
|
||||
|
|
|
@ -13,8 +13,7 @@ import { DateTime } from 'luxon';
|
|||
import mime from 'mime-types';
|
||||
import sharp from 'sharp';
|
||||
import { PassThrough } from 'stream';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { config } from '../../config.js';
|
||||
import { config, type MicroHost } from '../../config.js';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { getStreamType } from '../../helpers/get-stream-type.helper.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
|
@ -29,7 +28,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
@InjectRepository('File') private readonly fileRepo: EntityRepository<File>,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly hostService: HostService,
|
||||
protected readonly orm: MikroORM
|
||||
protected readonly orm: MikroORM,
|
||||
) {}
|
||||
|
||||
async getFile(id: string, request: FastifyRequest) {
|
||||
|
@ -45,7 +44,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
multipart: MultipartFile,
|
||||
request: FastifyRequest,
|
||||
owner: User,
|
||||
host: MicroHost | undefined
|
||||
host: MicroHost | undefined,
|
||||
): Promise<File> {
|
||||
if (host) this.hostService.checkUserCanUploadTo(host, owner);
|
||||
if (!request.headers['content-length']) throw new BadRequestException('Missing "Content-Length" header.');
|
||||
|
@ -53,7 +52,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
if (Number.isNaN(contentLength) || contentLength >= config.uploadLimit) {
|
||||
const size = bytes.parse(Number(request.headers['content-length']));
|
||||
this.logger.warn(
|
||||
`User ${owner.id} tried uploading a ${size} file, which is over the configured upload size limit.`
|
||||
`User ${owner.id} tried uploading a ${size} file, which is over the configured upload size limit.`,
|
||||
);
|
||||
|
||||
throw new PayloadTooLargeException();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { config } from '../../config.js';
|
||||
import { hosts } from '../../config.js';
|
||||
import { getRequest } from '../../helpers/get-request.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -9,11 +9,11 @@ export class HostGuard implements CanActivate {
|
|||
const request = getRequest(context);
|
||||
const referer = request.headers.referer;
|
||||
if (!referer) {
|
||||
request.host = config.hosts[0];
|
||||
request.host = hosts[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
const host = config.hosts.find((host) => host.pattern.test(referer));
|
||||
const host = hosts.find((host) => host.pattern.test(referer));
|
||||
if (!host) throw new BadRequestException('Invalid host.');
|
||||
request.host = host;
|
||||
return true;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import normalizeUrl from 'normalize-url';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { config } from '../../config.js';
|
||||
import { hosts, rootHost, type MicroHost } from '../../config.js';
|
||||
import { randomItem } from '../../helpers/random-item.helper.js';
|
||||
import type { User } from '../user/user.entity.js';
|
||||
|
||||
|
@ -19,9 +18,9 @@ export class HostService {
|
|||
* @throws if the host could not be resolved.
|
||||
*/
|
||||
getHostFrom(url: string | undefined, tags: string[] | null): MicroHost {
|
||||
if (!url) return config.rootHost;
|
||||
if (!url) return rootHost;
|
||||
const normalised = HostService.normaliseHostUrl(url);
|
||||
for (const host of config.hosts) {
|
||||
for (const host of hosts) {
|
||||
if (!host.pattern.test(normalised)) continue;
|
||||
if (tags && host.tags) {
|
||||
const hasTags = host.tags.every((tag) => tags.includes(tag));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Entity, ManyToOne, OneToOne, OptionalProps, PrimaryKey, Property, type Ref } from '@mikro-orm/core';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { config } from '../../config.js';
|
||||
import { rootHost } from '../../config.js';
|
||||
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class Invite {
|
|||
@Property({ persist: false })
|
||||
@Field(() => String)
|
||||
get url() {
|
||||
const url = new URL(config.rootHost.url);
|
||||
const url = new URL(rootHost.url);
|
||||
url.pathname = this.path;
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { Link } from './link.entity.js';
|
||||
import type { MicroHost } from '../../config.js';
|
||||
|
||||
@Injectable()
|
||||
export class LinkService {
|
||||
|
|
|
@ -7,7 +7,7 @@ import dedent from 'dedent';
|
|||
import handlebars from 'handlebars';
|
||||
import ms from 'ms';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../../config.js';
|
||||
import { config, rootHost } from '../../config.js';
|
||||
import type { Permission } from '../../constants.js';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { sendMail } from '../../helpers/send-mail.helper.js';
|
||||
|
@ -38,7 +38,7 @@ export class UserService {
|
|||
@InjectRepository(UserVerification) private readonly verificationRepo: EntityRepository<UserVerification>,
|
||||
@InjectRepository(File) private readonly fileRepo: EntityRepository<File>,
|
||||
@InjectRepository(Paste) private readonly pasteRepo: EntityRepository<Paste>,
|
||||
private readonly inviteService: InviteService
|
||||
private readonly inviteService: InviteService,
|
||||
) {}
|
||||
|
||||
async getUser(id: string, verified: boolean) {
|
||||
|
@ -61,7 +61,7 @@ export class UserService {
|
|||
orderBy: {
|
||||
createdAt: QueryOrder.DESC,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ export class UserService {
|
|||
orderBy: {
|
||||
createdAt: QueryOrder.DESC,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,7 @@ export class UserService {
|
|||
});
|
||||
|
||||
user.verifications.add(verification);
|
||||
const verifyUrl = `${config.rootHost.url}/api/user/${verification.user.id}/verify/${verification.id}`;
|
||||
const verifyUrl = `${rootHost.url}/api/user/${verification.user.id}/verify/${verification.id}`;
|
||||
const html = UserService.EMAIL_TEMPLATE({ verifyUrl });
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
|
@ -164,7 +164,7 @@ export class UserService {
|
|||
$gt: new Date(),
|
||||
},
|
||||
},
|
||||
{ populate: ['user'] }
|
||||
{ populate: ['user'] },
|
||||
);
|
||||
|
||||
if (!verification) {
|
||||
|
|
|
@ -159,6 +159,9 @@ importers:
|
|||
bytes:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
chalk:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
content-range:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
|
@ -207,6 +210,12 @@ importers:
|
|||
vitest:
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/node@20.10.6)
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
zod-validation-error:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(zod@3.22.4)
|
||||
|
||||
packages/web:
|
||||
dependencies:
|
||||
|
@ -4458,6 +4467,11 @@ packages:
|
|||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
/chalk@5.3.0:
|
||||
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/change-case-all@1.0.14:
|
||||
resolution: {integrity: sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA==}
|
||||
dependencies:
|
||||
|
@ -10835,6 +10849,19 @@ packages:
|
|||
resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==}
|
||||
dev: false
|
||||
|
||||
/zod-validation-error@2.1.0(zod@3.22.4):
|
||||
resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.18.0
|
||||
dependencies:
|
||||
zod: 3.22.4
|
||||
dev: true
|
||||
|
||||
/zod@3.22.4:
|
||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||
dev: true
|
||||
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
|
Loading…
Reference in New Issue