From e15009c91cb4b25aca2d0a50bd6e0e42336e7456 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:29:37 +0000 Subject: [PATCH 1/9] Start draft PR From dc948b86f3dd1bc1378f76740ca1985824e5f2c8 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:29:52 +0000 Subject: [PATCH 2/9] Add email validation utility functions --- lib/validation.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/validation.ts diff --git a/lib/validation.ts b/lib/validation.ts new file mode 100644 index 0000000..fdaaede --- /dev/null +++ b/lib/validation.ts @@ -0,0 +1,29 @@ +/** + * Validates email format using a comprehensive regex pattern + * @param email - Email address to validate + * @returns boolean indicating if the email is valid + */ +export const isValidEmail = (email: string): boolean => { + // Comprehensive email regex that follows RFC 5322 standard + const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + // Check if email is defined, not empty, and matches the regex pattern + return !!email && email.trim().length > 0 && emailRegex.test(email.trim()); +}; + +/** + * Provides a descriptive error message for invalid email formats + * @param email - Email address to validate + * @returns Error message or null if email is valid + */ +export const getEmailValidationError = (email: string): string | null => { + if (!email || email.trim().length === 0) { + return 'Email address is required'; + } + + if (!isValidEmail(email)) { + return 'Please enter a valid email address (e.g., example@domain.com)'; + } + + return null; +}; \ No newline at end of file From 24e33e8c381e232eb4e93ac4eb27d9ef6c7cf924 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:30:03 +0000 Subject: [PATCH 3/9] Add comprehensive email validation tests --- __tests__/validation.test.ts | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 __tests__/validation.test.ts diff --git a/__tests__/validation.test.ts b/__tests__/validation.test.ts new file mode 100644 index 0000000..135da9a --- /dev/null +++ b/__tests__/validation.test.ts @@ -0,0 +1,60 @@ +import { isValidEmail, getEmailValidationError } from '../lib/validation'; + +describe('Email Validation', () => { + // Valid email test cases + const validEmails = [ + 'user@example.com', + 'firstname.lastname@example.com', + 'user+tag@example.com', + 'user123@example.co.uk', + 'user@subdomain.example.com', + ]; + + // Invalid email test cases + const invalidEmails = [ + '', + ' ', + 'invalid-email', + 'user@', + '@example.com', + 'user@example', + 'user@.com', + 'user@example.', + 'user@example..com', + ]; + + // Test valid email scenarios + describe('isValidEmail - Valid Emails', () => { + validEmails.forEach(email => { + it(`should return true for valid email: ${email}`, () => { + expect(isValidEmail(email)).toBe(true); + }); + }); + }); + + // Test invalid email scenarios + describe('isValidEmail - Invalid Emails', () => { + invalidEmails.forEach(email => { + it(`should return false for invalid email: ${email}`, () => { + expect(isValidEmail(email)).toBe(false); + }); + }); + }); + + // Test error message generation + describe('getEmailValidationError', () => { + it('should return error for empty email', () => { + expect(getEmailValidationError('')).toBe('Email address is required'); + expect(getEmailValidationError(' ')).toBe('Email address is required'); + }); + + it('should return error for invalid email formats', () => { + expect(getEmailValidationError('invalid-email')).toBe('Please enter a valid email address (e.g., example@domain.com)'); + expect(getEmailValidationError('user@')).toBe('Please enter a valid email address (e.g., example@domain.com)'); + }); + + it('should return null for valid email', () => { + expect(getEmailValidationError('user@example.com')).toBeNull(); + }); + }); +}); \ No newline at end of file From 02feeec0e5b85fd002b573fc3b9cde0cc73bea85 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:31:09 +0000 Subject: [PATCH 4/9] Enhance email validation with RFC 5322 standard and unique constraint utility --- lib/validation.ts | 49 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/validation.ts b/lib/validation.ts index fdaaede..fc0e183 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -1,14 +1,34 @@ +import mongoose from 'mongoose'; + /** - * Validates email format using a comprehensive regex pattern + * Implements RFC 5322 standard email validation with additional safety checks * @param email - Email address to validate * @returns boolean indicating if the email is valid */ export const isValidEmail = (email: string): boolean => { - // Comprehensive email regex that follows RFC 5322 standard - const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + // Comprehensive RFC 5322 email validation regex + const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + // Validate email format, length, and complexity + if (!email) return false; + + // Trim and convert to lowercase for consistent validation + const trimmedEmail = email.trim().toLowerCase(); + + // Length checks + if (trimmedEmail.length < 5 || trimmedEmail.length > 254) return false; + + // Additional checks + const parts = trimmedEmail.split('@'); + if (parts.length !== 2) return false; + + const [local, domain] = parts; - // Check if email is defined, not empty, and matches the regex pattern - return !!email && email.trim().length > 0 && emailRegex.test(email.trim()); + // Local part and domain length checks + if (local.length > 64 || domain.length > 253) return false; + + // Final regex test + return emailRegex.test(trimmedEmail); }; /** @@ -26,4 +46,23 @@ export const getEmailValidationError = (email: string): string | null => { } return null; +}; + +/** + * Creates a case-insensitive unique email validation for Mongoose + * @returns Mongoose validation function + */ +export const createUniqueEmailValidator = (model: mongoose.Model) => { + return async function(email: string): Promise { + if (!isValidEmail(email)) { + throw new Error('Invalid email format'); + } + + // Case-insensitive unique check + const existingUser = await model.findOne({ + email: { $regex: new RegExp(`^${email}$`, 'i') } + }); + + return !existingUser; + }; }; \ No newline at end of file From 3148c05066d6b7e589f3af0bd6e12c09658a61b1 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:31:32 +0000 Subject: [PATCH 5/9] Update email validation tests with comprehensive scenarios --- __tests__/validation.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/__tests__/validation.test.ts b/__tests__/validation.test.ts index 135da9a..ea53cb5 100644 --- a/__tests__/validation.test.ts +++ b/__tests__/validation.test.ts @@ -1,16 +1,20 @@ import { isValidEmail, getEmailValidationError } from '../lib/validation'; describe('Email Validation', () => { - // Valid email test cases + // Comprehensive test cases covering various scenarios const validEmails = [ 'user@example.com', 'firstname.lastname@example.com', 'user+tag@example.com', 'user123@example.co.uk', 'user@subdomain.example.com', + 'very.common@example.com', + 'disposable.style.email.with+symbol@example.com', + 'other.email-with-hyphen@example.com', + 'fully-qualified-domain@example.com', + 'user.name+tag@example.org', ]; - // Invalid email test cases const invalidEmails = [ '', ' ', @@ -21,6 +25,14 @@ describe('Email Validation', () => { 'user@.com', 'user@example.', 'user@example..com', + 'a@b.c', // Too short + 'a' .repeat(65) + '@example.com', // Local part too long + 'user@' + 'a'.repeat(254) + '.com', // Domain too long + 'user name@example.com', // Space in local part + 'user@domain', // Missing TLD + '.user@example.com', // Starts with dot + 'user.@example.com', // Ends with dot + 'user..name@example.com', // Consecutive dots ]; // Test valid email scenarios From 33e75f34cdaf33b63bd37b42348083cf52f48f83 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:31:38 +0000 Subject: [PATCH 6/9] Add type definitions for email validation --- types.d.ts | 123 ++++++++--------------------------------------------- 1 file changed, 18 insertions(+), 105 deletions(-) diff --git a/types.d.ts b/types.d.ts index a2748eb..c26df11 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,109 +1,22 @@ -/* eslint-disable no-unused-vars */ -type Job = { - data: [ - { - employer_name: string; - employer_logo: string; - employer_website: string; - employer_company_type: string; - job_publisher: string; - job_id: string; - job_employment_type: string; - job_title: string; - job_apply_link: string; - job_apply_is_direct: boolean; - job_apply_quality_score: number; - job_description: string; - job_is_remote: boolean; - job_posted_at_timestamp: number; - job_posted_at_datetime_utc: string; - job_city: string; - job_state: string; - job_country: string; - job_latitude: number; - job_longitude: number; - job_benefits: null; - job_google_link: string; - job_offer_expiration_datetime_utc: string; - job_offer_expiration_timestamp: number; - job_required_experience: { - no_experience_required: boolean; - required_experience_in_months: null; - experience_mentioned: boolean; - experience_preferred: boolean; - }; - job_required_skills: null; - job_required_education: { - postgraduate_degree: boolean; - professional_certification: boolean; - high_school: boolean; - associates_degree: boolean; - bachelors_degree: boolean; - degree_mentioned: boolean; - degree_preferred: boolean; - professional_certification_mentioned: boolean; - }; - job_experience_in_place_of_education: boolean; - job_min_salary: null; - job_max_salary: null; - job_salary_currency: null; - job_salary_period: null; - job_highlights: { - Qualifications: [string]; - Responsibilities: [string, string]; - Benefits: [string, string, string]; - }; - job_job_title: null; - job_posting_language: string; - job_onet_soc: string; - job_onet_job_zone: string; - job_naics_code: string; - job_naics_name: string; - }, - ]; -}; +import mongoose from 'mongoose'; -type JobResult = Job["data"][0]; +declare module 'mongoose' { + interface EmailValidationOptions { + message?: string; + } -type JobSalaryEntryType = { - location?: string; - job_title?: string; - publisher_name?: string; - publisher_link?: string; - min_salary?: number; - max_salary?: number; - median_salary?: number; - salary_period?: "YEAR"; - salary_currency?: "CAD"; -}; -interface JobRequiredExperienceType { - no_experience_required?: boolean; - required_experience_in_months: number | null; - experience_mentioned?: boolean; - experience_preferred?: boolean; + interface SchemaTypeOptions { + /** + * Custom email validation for mongoose schemas + */ + email?: boolean | EmailValidationOptions; + } } -interface SearchParams { - query?: string; - searchQuery?: string; - employmentType?: string; - pageNumber?: number; - recommended?: boolean; -} - -interface CommonFilterDataShapeType { - name: string; - value: string; -} - -type CompanyDetailCardProps = { - logo: string; - employerName: string; - companyType: string; - city: string; - state: string; - companyLink: string | null; - jobId: string; - companyJobs: JobResult[]; - country: string; -}; +/** + * Email validation interface for user registration + */ +export interface IUserEmail { + email: string; + isValidEmail: (email: string) => boolean; +} \ No newline at end of file From a2e59f5dcf51b6b61de375853f43cd3c132ebe81 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:32:34 +0000 Subject: [PATCH 7/9] Create User model with unique, case-insensitive email constraint --- models/User.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 models/User.ts diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..900e0f7 --- /dev/null +++ b/models/User.ts @@ -0,0 +1,70 @@ +import mongoose from 'mongoose'; +import { isValidEmail } from '../lib/validation'; + +// Define the User schema interface +interface IUser extends mongoose.Document { + email: string; + createdAt: Date; + updatedAt: Date; +} + +// Create the User schema with unique, case-insensitive email validation +const UserSchema = new mongoose.Schema({ + email: { + type: String, + required: [true, 'Email is required'], + unique: true, + lowercase: true, // Store emails in lowercase + trim: true, + validate: { + validator: function(value: string) { + return isValidEmail(value); + }, + message: 'Invalid email format' + } + } +}, { + timestamps: true, // Automatically add createdAt and updatedAt + // Ensure case-insensitive unique index + autoIndex: true +}); + +// Create a pre-save middleware to ensure email uniqueness +UserSchema.pre('save', async function(next) { + // Only run this check if email is modified + if (!this.isModified('email')) return next(); + + try { + // Check for existing user with the same email (case-insensitive) + const existingUser = await this.constructor.findOne({ + email: this.email + }); + + if (existingUser && existingUser._id.toString() !== this._id.toString()) { + next(new Error('Email already in use')); + } else { + next(); + } + } catch (error) { + next(error as mongoose.CallbackError); + } +}); + +// Create a compound unique index for case-insensitive email +UserSchema.index({ email: 1 }, { + unique: true, + collation: { locale: 'en', strength: 2 } +}); + +// Ensure email is lowercase before saving +UserSchema.pre('save', function(next) { + if (this.isModified('email')) { + this.email = this.email.toLowerCase(); + } + next(); +}); + +// Create and export the User model +const User = mongoose.models.User || mongoose.model('User', UserSchema); + +export default User; \ No newline at end of file From 47d89e86a0df16421c9805ec557a39bed9ce0b1c Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:32:49 +0000 Subject: [PATCH 8/9] Update email validation with enhanced RFC 5322 regex and unique validation --- lib/validation.ts | 49 +++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/validation.ts b/lib/validation.ts index fc0e183..4a42fe0 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -1,38 +1,38 @@ import mongoose from 'mongoose'; /** - * Implements RFC 5322 standard email validation with additional safety checks + * Validates email format according to RFC 5322 standard * @param email - Email address to validate * @returns boolean indicating if the email is valid */ export const isValidEmail = (email: string): boolean => { - // Comprehensive RFC 5322 email validation regex - const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - - // Validate email format, length, and complexity - if (!email) return false; - // Trim and convert to lowercase for consistent validation + if (!email) return false; const trimmedEmail = email.trim().toLowerCase(); - // Length checks - if (trimmedEmail.length < 5 || trimmedEmail.length > 254) return false; - - // Additional checks - const parts = trimmedEmail.split('@'); - if (parts.length !== 2) return false; - - const [local, domain] = parts; + // Comprehensive RFC 5322 email validation regex + const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - // Local part and domain length checks - if (local.length > 64 || domain.length > 253) return false; + // Validate email format and length + if ( + trimmedEmail.length < 5 || + trimmedEmail.length > 254 || + !emailRegex.test(trimmedEmail) + ) { + return false; + } - // Final regex test - return emailRegex.test(trimmedEmail); + // Additional checks for email parts + const [local, domain] = trimmedEmail.split('@'); + return ( + local.length <= 64 && + domain.length <= 253 && + domain.split('.').every(part => part.length <= 63) + ); }; /** - * Provides a descriptive error message for invalid email formats + * Generates a descriptive error message for email validation * @param email - Email address to validate * @returns Error message or null if email is valid */ @@ -49,20 +49,23 @@ export const getEmailValidationError = (email: string): string | null => { }; /** - * Creates a case-insensitive unique email validation for Mongoose - * @returns Mongoose validation function + * Creates a unique email validator for Mongoose + * @param model - Mongoose model to check against + * @returns Async validation function */ export const createUniqueEmailValidator = (model: mongoose.Model) => { return async function(email: string): Promise { + // Validate email format first if (!isValidEmail(email)) { throw new Error('Invalid email format'); } - // Case-insensitive unique check + // Perform case-insensitive unique check const existingUser = await model.findOne({ email: { $regex: new RegExp(`^${email}$`, 'i') } }); + // Return true if no existing user found return !existingUser; }; }; \ No newline at end of file From 269c9380bd8cd48d5fdd4c8285aab43f3d27f251 Mon Sep 17 00:00:00 2001 From: Santix12 Date: Wed, 14 May 2025 03:33:06 +0000 Subject: [PATCH 9/9] Update validation tests with unique constraint scenarios --- __tests__/validation.test.ts | 61 ++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/__tests__/validation.test.ts b/__tests__/validation.test.ts index ea53cb5..06c989c 100644 --- a/__tests__/validation.test.ts +++ b/__tests__/validation.test.ts @@ -1,17 +1,32 @@ -import { isValidEmail, getEmailValidationError } from '../lib/validation'; +import mongoose from 'mongoose'; +import { isValidEmail, getEmailValidationError, createUniqueEmailValidator } from '../lib/validation'; +import User from '../models/User'; describe('Email Validation', () => { - // Comprehensive test cases covering various scenarios + // Setup MongoDB connection for unique constraint tests + beforeAll(async () => { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/testdb'); + }); + + // Cleanup after tests + afterAll(async () => { + await mongoose.connection.close(); + }); + + // Clear database before each test + beforeEach(async () => { + await User.deleteMany({}); + }); + + // Comprehensive test cases for email formats const validEmails = [ 'user@example.com', 'firstname.lastname@example.com', 'user+tag@example.com', 'user123@example.co.uk', - 'user@subdomain.example.com', 'very.common@example.com', 'disposable.style.email.with+symbol@example.com', 'other.email-with-hyphen@example.com', - 'fully-qualified-domain@example.com', 'user.name+tag@example.org', ]; @@ -28,24 +43,16 @@ describe('Email Validation', () => { 'a@b.c', // Too short 'a' .repeat(65) + '@example.com', // Local part too long 'user@' + 'a'.repeat(254) + '.com', // Domain too long - 'user name@example.com', // Space in local part - 'user@domain', // Missing TLD - '.user@example.com', // Starts with dot - 'user.@example.com', // Ends with dot - 'user..name@example.com', // Consecutive dots ]; - // Test valid email scenarios - describe('isValidEmail - Valid Emails', () => { + // Validate email format tests + describe('isValidEmail - Format Validation', () => { validEmails.forEach(email => { it(`should return true for valid email: ${email}`, () => { expect(isValidEmail(email)).toBe(true); }); }); - }); - // Test invalid email scenarios - describe('isValidEmail - Invalid Emails', () => { invalidEmails.forEach(email => { it(`should return false for invalid email: ${email}`, () => { expect(isValidEmail(email)).toBe(false); @@ -53,7 +60,7 @@ describe('Email Validation', () => { }); }); - // Test error message generation + // Error message generation tests describe('getEmailValidationError', () => { it('should return error for empty email', () => { expect(getEmailValidationError('')).toBe('Email address is required'); @@ -62,11 +69,33 @@ describe('Email Validation', () => { it('should return error for invalid email formats', () => { expect(getEmailValidationError('invalid-email')).toBe('Please enter a valid email address (e.g., example@domain.com)'); - expect(getEmailValidationError('user@')).toBe('Please enter a valid email address (e.g., example@domain.com)'); }); it('should return null for valid email', () => { expect(getEmailValidationError('user@example.com')).toBeNull(); }); }); + + // Unique constraint tests + describe('Unique Email Constraint', () => { + it('should prevent duplicate email registration (case-insensitive)', async () => { + // Create first user + const user1 = new User({ email: 'test@example.com' }); + await user1.save(); + + // Attempt to create user with same email (different case) + const user2 = new User({ email: 'TEST@EXAMPLE.COM' }); + + // Expect an error about duplicate email + await expect(user2.save()).rejects.toThrow('Email already in use'); + }); + + it('should allow unique email registration', async () => { + const user1 = new User({ email: 'unique1@example.com' }); + const user2 = new User({ email: 'unique2@example.com' }); + + await expect(user1.save()).resolves.toBeTruthy(); + await expect(user2.save()).resolves.toBeTruthy(); + }); + }); }); \ No newline at end of file