diff --git a/package-lock.json b/package-lock.json index 61c4749e6..cbeeba990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6008,6 +6008,12 @@ "@types/estree": "*" } }, + "node_modules/@types/bad-words": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/bad-words/-/bad-words-3.0.3.tgz", + "integrity": "sha512-jYdpTxDOJ+EENnsCwt8cOZhV/+4+qcwhks1igrOSg4zwwA17rsPqLsZpTo1l+OwViNu+5SPus0v5g7iGx+ofzA==", + "dev": true + }, "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", @@ -6906,6 +6912,22 @@ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" }, + "node_modules/bad-words": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bad-words/-/bad-words-3.0.4.tgz", + "integrity": "sha512-v/Q9uRPH4+yzDVLL4vR1+S9KoFgOEUl5s4axd6NIAq8SV2mradgi4E8lma/Y0cw1ltVdvyegCQQKffCPRCp8fg==", + "dependencies": { + "badwords-list": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/badwords-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/badwords-list/-/badwords-list-1.0.0.tgz", + "integrity": "sha512-oWhaSG67e+HQj3OGHQt2ucP+vAPm1wTbdp2aDHeuh4xlGXBdWwzZ//pfu6swf5gZ8iX0b7JgmSo8BhgybbqszA==" + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -19475,12 +19497,15 @@ "@trpc/next": "^10.36.0", "@trpc/react-query": "^10.36.0", "@trpc/server": "^10.36.0", + "bad-words": "^3.0.4", "luxon": "^3.4.0", "superjson": "^1.13.1", "ts-pattern": "^5.0.5", "zod": "^3.22.4" }, - "devDependencies": {} + "devDependencies": { + "@types/bad-words": "^3.0.3" + } }, "packages/tsconfig": { "name": "@documenso/tsconfig", diff --git a/packages/lib/utils/generate-url-slug.ts b/packages/lib/utils/generate-url-slug.ts new file mode 100644 index 000000000..9ef329ee8 --- /dev/null +++ b/packages/lib/utils/generate-url-slug.ts @@ -0,0 +1,25 @@ +const diacriticRegex = /\p{Diacritic}/gu; +const nonAlphanumericRegex = /[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu; +const whitespaceUnderscoreRegex = /[\s_#]+/g; +const dashStartRegex = /^-+/; +const multipleDotsRegex = /\.{2,}/g; + +export const generateURLSlug = (str: string, forDisplayingInput?: boolean) => { + if (!str) { + return ''; + } + + const slug = str + .toLowerCase() + .trim() + .normalize('NFD') + .replace(diacriticRegex, '') + .replace(nonAlphanumericRegex, '-') + .replace(whitespaceUnderscoreRegex, '-') + .replace(dashStartRegex, '') // Remove dashes from start + .replace(multipleDotsRegex, '.'); // Replace consecutive periods with a single period + + return forDisplayingInput ? slug : slug.replace(/-*$/g, ''); // Remove dashes from end +}; + +export default generateURLSlug; diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 54c1d5917..73c0783e6 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -17,10 +17,13 @@ "@trpc/next": "^10.36.0", "@trpc/react-query": "^10.36.0", "@trpc/server": "^10.36.0", + "bad-words": "^3.0.4", "luxon": "^3.4.0", "superjson": "^1.13.1", "ts-pattern": "^5.0.5", "zod": "^3.22.4" }, - "devDependencies": {} + "devDependencies": { + "@types/bad-words": "^3.0.3" + } } diff --git a/packages/trpc/server/team-router/schema.ts b/packages/trpc/server/team-router/schema.ts index 8072b9c73..dfd45e074 100644 --- a/packages/trpc/server/team-router/schema.ts +++ b/packages/trpc/server/team-router/schema.ts @@ -2,6 +2,12 @@ import { z } from 'zod'; import { TeamMemberRole } from '@documenso/prisma/client'; +import { generateURLSlug } from '@documenso/lib/utils/generate-url-slug'; + +import badWords from 'bad-words'; + +const filter = new badWords(); + const GenericFindQuerySchema = z.object({ term: z.string().optional(), page: z.number().optional(), @@ -20,7 +26,12 @@ export const ZAddTeamEmailVerificationMutationSchema = z.object({ export const ZCreateTeamMutationSchema = z.object({ name: z.string().min(1), - url: z.string().min(1), // Todo: Teams - Apply lowercase, disallow certain symbols, disallow profanity. + url: z.string().min(1).refine((value) => { + const generatedSlug = generateURLSlug(value); + return !filter.isProfane(value.toLowerCase()) && generatedSlug === value.toLowerCase(); + }, { + message: 'URL contains inappropriate language or unsupported characters', + }), }); export const ZCreateTeamMemberInvitesMutationSchema = z.object({ @@ -100,7 +111,12 @@ export const ZUpdateTeamMutationSchema = z.object({ data: z.object({ // Todo: Teams name: z.string().min(1), - url: z.string().min(1), // Todo: Apply regex. Todo: lowercase, etc + url: z.string().min(1).refine((value) => { + const generatedSlug = generateURLSlug(value); + return !filter.isProfane(value.toLowerCase()) && generatedSlug === value.toLowerCase(); + }, { + message: 'URL contains inappropriate language or unsupported characters', + }), }), });