217 lines
7.7 KiB
TypeScript
217 lines
7.7 KiB
TypeScript
import { CreateUserRequest, CreateCategoryRequest, CreatePostRequest, CreateCommentRequest, UpdatePostRequest } from './BlogModels';
|
|
|
|
export class ValidationError extends Error {
|
|
constructor(message: string, public field?: string) {
|
|
super(message);
|
|
this.name = 'ValidationError';
|
|
}
|
|
}
|
|
|
|
export class BlogValidation {
|
|
static validateUser(data: CreateUserRequest): void {
|
|
if (!data.username || data.username.length < 3 || data.username.length > 30) {
|
|
throw new ValidationError('Username must be between 3 and 30 characters', 'username');
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_]+$/.test(data.username)) {
|
|
throw new ValidationError('Username can only contain letters, numbers, and underscores', 'username');
|
|
}
|
|
|
|
if (!data.email || !this.isValidEmail(data.email)) {
|
|
throw new ValidationError('Valid email is required', 'email');
|
|
}
|
|
|
|
if (data.displayName && data.displayName.length > 100) {
|
|
throw new ValidationError('Display name cannot exceed 100 characters', 'displayName');
|
|
}
|
|
|
|
if (data.avatar && !this.isValidUrl(data.avatar)) {
|
|
throw new ValidationError('Avatar must be a valid URL', 'avatar');
|
|
}
|
|
|
|
if (data.roles && !Array.isArray(data.roles)) {
|
|
throw new ValidationError('Roles must be an array', 'roles');
|
|
}
|
|
}
|
|
|
|
static validateCategory(data: CreateCategoryRequest): void {
|
|
if (!data.name || data.name.length < 2 || data.name.length > 50) {
|
|
throw new ValidationError('Category name must be between 2 and 50 characters', 'name');
|
|
}
|
|
|
|
if (data.description && data.description.length > 500) {
|
|
throw new ValidationError('Description cannot exceed 500 characters', 'description');
|
|
}
|
|
|
|
if (data.color && !/^#[0-9A-Fa-f]{6}$/.test(data.color)) {
|
|
throw new ValidationError('Color must be a valid hex color code', 'color');
|
|
}
|
|
}
|
|
|
|
static validatePost(data: CreatePostRequest): void {
|
|
if (!data.title || data.title.length < 3 || data.title.length > 200) {
|
|
throw new ValidationError('Title must be between 3 and 200 characters', 'title');
|
|
}
|
|
|
|
if (!data.content || data.content.length < 10) {
|
|
throw new ValidationError('Content must be at least 10 characters long', 'content');
|
|
}
|
|
|
|
if (data.content.length > 50000) {
|
|
throw new ValidationError('Content cannot exceed 50,000 characters', 'content');
|
|
}
|
|
|
|
if (!data.authorId) {
|
|
throw new ValidationError('Author ID is required', 'authorId');
|
|
}
|
|
|
|
if (data.excerpt && data.excerpt.length > 300) {
|
|
throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt');
|
|
}
|
|
|
|
if (data.tags && !Array.isArray(data.tags)) {
|
|
throw new ValidationError('Tags must be an array', 'tags');
|
|
}
|
|
|
|
if (data.tags && data.tags.length > 10) {
|
|
throw new ValidationError('Cannot have more than 10 tags', 'tags');
|
|
}
|
|
|
|
if (data.tags) {
|
|
for (const tag of data.tags) {
|
|
if (typeof tag !== 'string' || tag.length > 30) {
|
|
throw new ValidationError('Each tag must be a string with max 30 characters', 'tags');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.featuredImage && !this.isValidUrl(data.featuredImage)) {
|
|
throw new ValidationError('Featured image must be a valid URL', 'featuredImage');
|
|
}
|
|
|
|
if (data.status && !['draft', 'published'].includes(data.status)) {
|
|
throw new ValidationError('Status must be either "draft" or "published"', 'status');
|
|
}
|
|
}
|
|
|
|
static validatePostUpdate(data: UpdatePostRequest): void {
|
|
if (data.title && (data.title.length < 3 || data.title.length > 200)) {
|
|
throw new ValidationError('Title must be between 3 and 200 characters', 'title');
|
|
}
|
|
|
|
if (data.content && (data.content.length < 10 || data.content.length > 50000)) {
|
|
throw new ValidationError('Content must be between 10 and 50,000 characters', 'content');
|
|
}
|
|
|
|
if (data.excerpt && data.excerpt.length > 300) {
|
|
throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt');
|
|
}
|
|
|
|
if (data.tags && !Array.isArray(data.tags)) {
|
|
throw new ValidationError('Tags must be an array', 'tags');
|
|
}
|
|
|
|
if (data.tags && data.tags.length > 10) {
|
|
throw new ValidationError('Cannot have more than 10 tags', 'tags');
|
|
}
|
|
|
|
if (data.tags) {
|
|
for (const tag of data.tags) {
|
|
if (typeof tag !== 'string' || tag.length > 30) {
|
|
throw new ValidationError('Each tag must be a string with max 30 characters', 'tags');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.featuredImage && !this.isValidUrl(data.featuredImage)) {
|
|
throw new ValidationError('Featured image must be a valid URL', 'featuredImage');
|
|
}
|
|
|
|
if (data.isFeatured !== undefined && typeof data.isFeatured !== 'boolean') {
|
|
throw new ValidationError('isFeatured must be a boolean', 'isFeatured');
|
|
}
|
|
}
|
|
|
|
static validateComment(data: CreateCommentRequest): void {
|
|
if (!data.content || data.content.length < 1 || data.content.length > 2000) {
|
|
throw new ValidationError('Comment must be between 1 and 2000 characters', 'content');
|
|
}
|
|
|
|
if (!data.postId) {
|
|
throw new ValidationError('Post ID is required', 'postId');
|
|
}
|
|
|
|
if (!data.authorId) {
|
|
throw new ValidationError('Author ID is required', 'authorId');
|
|
}
|
|
|
|
// parentId is optional, but if provided should be a string
|
|
if (data.parentId !== undefined && typeof data.parentId !== 'string') {
|
|
throw new ValidationError('Parent ID must be a string', 'parentId');
|
|
}
|
|
}
|
|
|
|
private static isValidEmail(email: string): boolean {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
private static isValidUrl(url: string): boolean {
|
|
try {
|
|
new URL(url);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Sanitization helpers
|
|
static sanitizeString(input: string): string {
|
|
return input.trim().replace(/[<>]/g, '');
|
|
}
|
|
|
|
static sanitizeArray(input: string[]): string[] {
|
|
return input.map(item => this.sanitizeString(item)).filter(item => item.length > 0);
|
|
}
|
|
|
|
static sanitizeUserInput(data: CreateUserRequest): CreateUserRequest {
|
|
return {
|
|
username: data.username ? this.sanitizeString(data.username) : '',
|
|
email: data.email ? this.sanitizeString(data.email.toLowerCase()) : '',
|
|
displayName: data.displayName ? this.sanitizeString(data.displayName) : undefined,
|
|
avatar: data.avatar ? this.sanitizeString(data.avatar) : undefined,
|
|
roles: data.roles ? this.sanitizeArray(data.roles) : undefined
|
|
};
|
|
}
|
|
|
|
static sanitizeCategoryInput(data: CreateCategoryRequest): CreateCategoryRequest {
|
|
return {
|
|
name: data.name ? this.sanitizeString(data.name) : '',
|
|
slug: data.slug ? this.sanitizeString(data.slug) : undefined,
|
|
description: data.description ? this.sanitizeString(data.description) : undefined,
|
|
color: data.color ? this.sanitizeString(data.color) : undefined
|
|
};
|
|
}
|
|
|
|
static sanitizePostInput(data: CreatePostRequest): CreatePostRequest {
|
|
return {
|
|
title: this.sanitizeString(data.title),
|
|
content: data.content.trim(), // Don't sanitize content too aggressively
|
|
excerpt: data.excerpt ? this.sanitizeString(data.excerpt) : undefined,
|
|
authorId: this.sanitizeString(data.authorId),
|
|
categoryId: data.categoryId ? this.sanitizeString(data.categoryId) : undefined,
|
|
tags: data.tags ? this.sanitizeArray(data.tags) : undefined,
|
|
featuredImage: data.featuredImage ? this.sanitizeString(data.featuredImage) : undefined,
|
|
status: data.status
|
|
};
|
|
}
|
|
|
|
static sanitizeCommentInput(data: CreateCommentRequest): CreateCommentRequest {
|
|
return {
|
|
content: data.content.trim(),
|
|
postId: this.sanitizeString(data.postId),
|
|
authorId: this.sanitizeString(data.authorId),
|
|
parentId: data.parentId ? this.sanitizeString(data.parentId) : undefined
|
|
};
|
|
}
|
|
} |