Skip to content

Commit

Permalink
feat(workspace): expand forbidden subdomain validation (#9082)
Browse files Browse the repository at this point in the history
Added new forbidden words and regex patterns to subdomain validation in
`update-workspace-input`. Enhanced the `ForbiddenWords` validator to
support both strings and regex matching. Updated tests to verify
regex-based forbidden subdomain validation.

Fix #9064

---------

Co-authored-by: Weiko <corentin@twenty.com>
  • Loading branch information
AMoreaux and Weiko authored Dec 18, 2024
1 parent 550756c commit 2bcce44
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export const SettingsDomain = () => {
} catch (error) {
if (
error instanceof Error &&
error.message === 'Subdomain already taken'
(error.message === 'Subdomain already taken' ||
error.message.endsWith('not allowed'))
) {
control.setError('subdomain', {
type: 'manual',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Field, InputType } from '@nestjs/graphql';

import { IsBoolean, IsOptional, IsString, Matches } from 'class-validator';

import { ForbiddenWords } from 'src/engine/utils/custom-class-validator/ForbiddenWords';
import {
IsBoolean,
IsOptional,
IsString,
Matches,
IsNotIn,
} from 'class-validator';

@InputType()
export class UpdateWorkspaceInput {
Expand All @@ -14,8 +18,91 @@ export class UpdateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
@Matches(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/)
@ForbiddenWords(['demo'])
@Matches(/^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/)
@IsNotIn([
'demo',
'api',
't',
'companies',
'telemetry',
'logs',
'metrics',
'next',
'main',
'admin',
'dashboard',
'dash',
'billing',
'db',
'favicon',
'www',
'mail',
'docs',
'dev',
'app',
'staging',
'production',
'developer',
'files',
'cdn',
'storage',
'about',
'help',
'support',
'contact',
'privacy',
'terms',
'careers',
'jobs',
'blog',
'news',
'events',
'community',
'forum',
'chat',
'test',
'testing',
'feedback',
'config',
'settings',
'media',
'image',
'audio',
'video',
'images',
'partners',
'partnership',
'partnerships',
'assets',
'login',
'signin',
'signup',
'legal',
'shop',
'merch',
'store',
'auth',
'register',
'payment',
'fr',
'de',
'it',
'es',
'pt',
'nl',
'be',
'ch',
'us',
'ca',
'au',
'nz',
'za',
'uk',
'eu',
'asia',
'africa',
'america',
])
subdomain?: string;

@Field({ nullable: true })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UseGuards } from '@nestjs/common';
import { UseFilters, UseGuards } from '@nestjs/common';
import {
Args,
Mutation,
Expand Down Expand Up @@ -41,6 +41,7 @@ import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { GraphqlValidationExceptionFilter } from 'src/filters/validation-exception.filter';
import { assert } from 'src/utils/assert';
import { isDefined } from 'src/utils/is-defined';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
Expand All @@ -50,6 +51,7 @@ import { Workspace } from './workspace.entity';
import { WorkspaceService } from './services/workspace.service';

@Resolver(() => Workspace)
@UseFilters(GraphqlValidationExceptionFilter)
export class WorkspaceResolver {
constructor(
private readonly workspaceService: WorkspaceService,
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

import { ValidationError } from 'class-validator';

import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';

@Catch(ValidationError)
export class GraphqlValidationExceptionFilter implements ExceptionFilter {
catch(exception: ValidationError, _host: ArgumentsHost) {
const errors = Object.values(exception.constraints || {}).map((error) => ({
message: error,
path: exception.property,
}));

return new UserInputError(errors.map((error) => error.message).join(', '));
}
}
14 changes: 12 additions & 2 deletions packages/twenty-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import fs from 'fs';

import bytes from 'bytes';
import { useContainer } from 'class-validator';
import { useContainer, ValidationError } from 'class-validator';
import session from 'express-session';
import { graphqlUploadExpress } from 'graphql-upload';

import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
import { getSessionStorageOptions } from 'src/engine/core-modules/session-storage/session-storage.module-factory';
import { UnhandledExceptionFilter } from 'src/utils/apply-cors-to-exceptions';
import { UnhandledExceptionFilter } from 'src/filters/unhandled-exception.filter';

import { AppModule } from './app.module';
import './instrument';
Expand Down Expand Up @@ -54,6 +54,16 @@ const bootstrap = async () => {
app.useGlobalPipes(
new ValidationPipe({
transform: true,
exceptionFactory: (errors) => {
const error = new ValidationError();

error.constraints = Object.assign(
{},
...errors.map((error) => error.constraints),
);

return error;
},
}),
);
app.useBodyParser('json', { limit: settings.storage.maxFileSize });
Expand Down

0 comments on commit 2bcce44

Please sign in to comment.