feat: enhance slug generation and validation

- Add robust error handling for `generateSlug`, throwing `BadRequestException` on invalid input.
- Improve error messages to provide clearer feedback in case of failures.
This commit is contained in:
oscargonzalezmoreno@gmail.com 2024-12-27 12:57:14 +01:00
parent 6609122f8a
commit 2b8909c17f
5 changed files with 81 additions and 12 deletions

View file

@ -44,6 +44,7 @@
"@supabase/supabase-js": "^2.47.10", "@supabase/supabase-js": "^2.47.10",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/leo-profanity": "^1.5.4",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
@ -53,6 +54,7 @@
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"leo-profanity": "^1.7.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",

View file

@ -35,6 +35,13 @@ export class RicesService {
throw new BadRequestException('The request body must be a string.'); throw new BadRequestException('The request body must be a string.');
} }
try {
this.validateJsonStructure(content);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
throw new BadRequestException('Invalid json request');
}
// Validate lengths // Validate lengths
if (name.length > 75) { if (name.length > 75) {
throw new BadRequestException( throw new BadRequestException(
@ -49,13 +56,11 @@ export class RicesService {
} }
// Parse version and OS from User-Agent // Parse version and OS from User-Agent
const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d+) \((.+)\)/; const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d.\d+) \((.+)\)/;
const match = userAgent.match(userAgentRegex); const match = userAgent.match(userAgentRegex);
if (!match) { if (!match) {
throw new BadRequestException( throw new BadRequestException('Invalid request');
'Invalid User-Agent format. Expected format: ZenBrowser/<version> (<OS>).',
);
} }
const [, version, os] = match; const [, version, os] = match;
@ -80,9 +85,16 @@ export class RicesService {
); );
} }
const slug = `${generateSlug(name)}-${uuidv4()}`; let slug: string;
const token = uuidv4(); try {
slug = `${generateSlug(name)}-${uuidv4()}`;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// If generateSlug throws an error, rethrow as a BadRequestException
throw new BadRequestException(`Invalid name provided`);
}
const token = uuidv4();
const encodedContent = Buffer.from(content).toString('base64'); const encodedContent = Buffer.from(content).toString('base64');
const metadata = { const metadata = {
@ -154,13 +166,18 @@ export class RicesService {
} }
// Parse version and OS from User-Agent // Parse version and OS from User-Agent
const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d+) \((.+)\)/; const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d.\d+) \((.+)\)/;
const match = userAgent.match(userAgentRegex); const match = userAgent.match(userAgentRegex);
if (!match) { if (!match) {
throw new BadRequestException( throw new BadRequestException('Invalid request');
'Invalid User-Agent format. Expected format: ZenBrowser/<version> (<OS>).', }
);
try {
this.validateJsonStructure(content);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
throw new BadRequestException('Invalid json request');
} }
const [, version, os] = match; const [, version, os] = match;
@ -280,4 +297,39 @@ export class RicesService {
throw new Error('Failed to remove rice by moderation'); throw new Error('Failed to remove rice by moderation');
} }
} }
validateJsonStructure(jsonString: string): boolean {
const requiredKeys: string[] = [
'userChrome',
'userContent',
'enabledMods',
'preferences',
'workspaceThemes',
];
let json: Record<string, unknown>;
// Validate JSON string
try {
json = JSON.parse(jsonString);
} catch {
throw new BadRequestException('Invalid JSON string.');
}
// Ensure the parsed JSON is an object
if (typeof json !== 'object' || json === null) {
throw new BadRequestException('The parsed JSON is not a valid object.');
}
// Check for missing keys
const missingKeys = requiredKeys.filter((key) => !(key in json));
if (missingKeys.length > 0) {
throw new BadRequestException(
`The JSON is missing the following required keys: ${missingKeys.join(', ')}`,
);
}
return true;
}
} }

View file

@ -1,3 +1,5 @@
import * as leoProfanity from 'leo-profanity';
export function generateSlug(name: string): string { export function generateSlug(name: string): string {
// Ensure the input is a string and trim whitespace // Ensure the input is a string and trim whitespace
if (typeof name !== 'string') { if (typeof name !== 'string') {
@ -5,6 +7,9 @@ export function generateSlug(name: string): string {
} }
const sanitizedInput = name.trim(); const sanitizedInput = name.trim();
// Configure the profanity filter
leoProfanity.loadDictionary('en'); // Ensure the dictionary is loaded
// Replace accented characters with their unaccented counterparts // Replace accented characters with their unaccented counterparts
const normalized = sanitizedInput const normalized = sanitizedInput
.normalize('NFD') .normalize('NFD')
@ -21,5 +26,15 @@ export function generateSlug(name: string): string {
throw new Error('Generated slug is empty'); throw new Error('Generated slug is empty');
} }
// Split the slug into individual words
const words = slug.split('-');
// Check each word for inappropriate content
words.forEach((word) => {
if (leoProfanity.check(word)) {
throw new Error(`The word "${word}" is inappropriate.`);
}
});
return slug; return slug;
} }

View file

@ -4,7 +4,7 @@ POST {{baseUrl}}/rices
Content-Type: application/json Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-aurora2 X-Zen-Rice-Name: cool-zenrice-aurora2
X-Zen-Rice-Author: jhon@doe.com X-Zen-Rice-Author: jhon@doe.com
User-Agent: ZenBrowser/1.0.0 (EndeavourOS x86_64) User-Agent: ZenBrowser/1.0.0.0 (EndeavourOS x86_64)
{ {
"userChrome": "", "userChrome": "",

View file

@ -7,7 +7,7 @@ Content-Type: application/json
x-zen-rices-token: {{previous_token}} x-zen-rices-token: {{previous_token}}
X-Zen-Rice-Name: cool-zenrice-aurora2 X-Zen-Rice-Name: cool-zenrice-aurora2
X-Zen-Rice-Author: jhon@doe.com X-Zen-Rice-Author: jhon@doe.com
User-Agent: ZenBrowser/1.0.0 (EndeavourOS x86_64) User-Agent: ZenBrowser/1.0.0.0 (EndeavourOS x86_64)
{ {
"userChrome": "", "userChrome": "",