From 8637002491d122da2adaf3a5e99274a8bd1c2b44 Mon Sep 17 00:00:00 2001 From: Tusharmahajan12 Date: Fri, 22 May 2026 11:16:10 +0530 Subject: [PATCH 1/3] Create Cohortmember added validation if Admin role is there donot check Application endDate --- src/adapters/postgres/cohortMembers-adapter.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/adapters/postgres/cohortMembers-adapter.ts b/src/adapters/postgres/cohortMembers-adapter.ts index b89c50b8..5130e37f 100644 --- a/src/adapters/postgres/cohortMembers-adapter.ts +++ b/src/adapters/postgres/cohortMembers-adapter.ts @@ -779,10 +779,11 @@ export class PostgresCohortMembersService { ); } - // Validate Application End Date before creating cohort member (only if configured) + // Validate Application End Date before creating cohort member (only if configured and not admin) const applicationEndDateFieldId = process.env.APPLICATION_END_FIELD_ID; + const callerIsAdmin = await this.isUserAdmin(loginUser, tenantId); - if (applicationEndDateFieldId) { + if (applicationEndDateFieldId && !callerIsAdmin) { const cohortId = cohortMembers.cohortId; // Query FieldValues table for the Application End Date field @@ -6881,4 +6882,15 @@ export class PostgresCohortMembersService { return { condition: '', params: [] }; } } + + private async isUserAdmin(userId: string, tenantId: string): Promise { + const result = await this.usersRepository.query( + `SELECT 1 FROM "UserRolesMapping" URM + JOIN "Roles" R ON R."roleId" = URM."roleId" + WHERE URM."userId" = $1 AND URM."tenantId" = $2 AND R."code" = 'admin' + LIMIT 1`, + [userId, tenantId] + ); + return result.length > 0; + } } \ No newline at end of file From c39a8021d63aaba213c1666ba4667dbd1f10186a Mon Sep 17 00:00:00 2001 From: Tusharmahajan12 Date: Tue, 26 May 2026 15:50:38 +0530 Subject: [PATCH 2/3] Added validation and limited character allowed in referral tracking --- src/referrals/referrals.service.ts | 29 +++++++++----------- src/referrals/utils/referral-slug.util.ts | 33 ++++++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/referrals/referrals.service.ts b/src/referrals/referrals.service.ts index 3f98e2fd..6985945b 100644 --- a/src/referrals/referrals.service.ts +++ b/src/referrals/referrals.service.ts @@ -15,7 +15,7 @@ import { ReferralEntitySubType, ReferralEntityType } from './referrals.types'; import { buildReferLink, generateReferralSlug, - isValidStandardSlug, + isValidUserProvidedSlug, standardizeSlugInput, } from './utils/referral-slug.util'; @@ -66,15 +66,15 @@ export class ReferralsService { }); if (dto.slug) { - const normalizedSlug = standardizeSlugInput(dto.slug); - if (!normalizedSlug) { - throw new BadRequestException('Provided slug is invalid after normalization'); + const trimmedSlug = dto.slug.trim(); + if (!trimmedSlug || !isValidUserProvidedSlug(trimmedSlug)) { + throw new BadRequestException('Slug may only contain letters, digits, hyphens (-), underscores (_), dots (.) and tildes (~)'); } - const slugExists = await this.slugExistsAnywhere(normalizedSlug); + const slugExists = await this.slugExistsAnywhere(trimmedSlug); if (slugExists) { - throw new ConflictException(`Slug '${normalizedSlug}' already exists`); + throw new ConflictException(`Slug '${trimmedSlug}' already exists`); } - entity.slug = normalizedSlug; + entity.slug = trimmedSlug; } else { entity.slug = await this.generateUniqueSlug({ type: dto.type, @@ -247,16 +247,13 @@ export class ReferralsService { // ── Slug: normalize any format, check uniqueness, preserve history ─────── let pendingSlug: string | null = null; if (dto.slug !== undefined) { - const newSlug = standardizeSlugInput(dto.slug); - if (!newSlug) { - throw new BadRequestException('Provided slug is invalid after normalization'); + const trimmedSlug = dto.slug.trim(); + if (!trimmedSlug || !isValidUserProvidedSlug(trimmedSlug)) { + throw new BadRequestException('Slug may only contain letters, digits, hyphens (-), underscores (_), dots (.) and tildes (~)'); } - if (!isValidStandardSlug(newSlug)) { - throw new BadRequestException('Slug must contain only lowercase a-z, 0-9, and _'); - } - if (newSlug !== entity.slug) { - await this.assertSlugUnique(newSlug); - pendingSlug = newSlug; + if (trimmedSlug !== entity.slug) { + await this.assertSlugUnique(trimmedSlug); + pendingSlug = trimmedSlug; } } diff --git a/src/referrals/utils/referral-slug.util.ts b/src/referrals/utils/referral-slug.util.ts index 61336274..599e400b 100644 --- a/src/referrals/utils/referral-slug.util.ts +++ b/src/referrals/utils/referral-slug.util.ts @@ -22,22 +22,32 @@ function randomBase36(length: number): string { return asBase36.padStart(length, '0').slice(0, length); } -export function standardizeSlugInput(input: string): string { - // Lowercase, remove accents, keep [a-z0-9_], collapse spaces to nothing. - // Use NFKD so accents become combining marks; then strip them. - const s = String(input ?? '') +// Allowed chars in a user-provided slug: letters, digits, -, _, ., ~ +const USER_SLUG_PATTERN = /^[a-zA-Z0-9\-_\.~]+$/; + +export function isValidUserProvidedSlug(slug: string): boolean { + return USER_SLUG_PATTERN.test(slug); +} + +// Used only for name-based auto-generation (strips to safe base36-compatible chars) +function normalizeNamePart(input: string): string { + return String(input ?? '') .trim() .toLowerCase() .normalize('NFKD') - .replace(/[\u0300-\u036f]/g, '') // diacritics - .replace(/\s+/g, '') // remove spaces - .replace(/[^a-z0-9_]/g, ''); // remove special chars - return s; + .replace(/[\u0300-\u036f]/g, '') + .replace(/\s+/g, '') + .replace(/[^a-z0-9_]/g, ''); +} + +// Keep for resolveSlug backward compat \u2014 only trims, no stripping +export function standardizeSlugInput(input: string): string { + return String(input ?? '').trim(); } export function standardizeNameForSlug(firstName: string, lastName?: string) { - const a = standardizeSlugInput(firstName); - const b = lastName ? standardizeSlugInput(lastName) : ''; + const a = normalizeNamePart(firstName); + const b = lastName ? normalizeNamePart(lastName) : ''; if (a && b) return `${a}_${b}`; return a || b; } @@ -51,11 +61,9 @@ export function generateReferralSlug(params: { const { type, subType, firstName, lastName } = params; if (type === ReferralEntityType.INTERNAL && subType === ReferralEntitySubType.ALUMNI) { - // Internal alumni: random, non-identifiable return randomBase36(8); } - // External: human-readable name + random suffix const base = standardizeNameForSlug(firstName, lastName ?? undefined); const suffix = randomBase36(6); return `${base}_${suffix}`; @@ -64,7 +72,6 @@ export function generateReferralSlug(params: { export function isValidStandardSlug(slug: string): boolean { const s = String(slug || '').trim(); if (!s) return false; - if (s !== s.toLowerCase()) return false; return /^[a-z0-9_]+$/.test(s); } From 8b5c68bdb49cea5e9f725bf923a4edd527f1e484 Mon Sep 17 00:00:00 2001 From: Tusharmahajan12 Date: Tue, 26 May 2026 16:28:25 +0530 Subject: [PATCH 3/3] Added validation and limited character allowed in referral tracking --- .../dto/create-referral-entity.dto.ts | 2 +- src/referrals/dto/update-referral-slug.dto.ts | 2 +- src/referrals/referrals.service.ts | 21 ++++++++++--------- src/referrals/utils/referral-slug.util.ts | 15 +++++++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/referrals/dto/create-referral-entity.dto.ts b/src/referrals/dto/create-referral-entity.dto.ts index e1d431bc..92f19df4 100644 --- a/src/referrals/dto/create-referral-entity.dto.ts +++ b/src/referrals/dto/create-referral-entity.dto.ts @@ -7,7 +7,7 @@ import { } from '../referrals.types'; export class CreateReferralEntityDto { - @ApiPropertyOptional({ description: 'Custom slug (any format accepted; will be normalized to lowercase a-z0-9_). If omitted, auto-generated.' }) + @ApiPropertyOptional({ description: 'Custom slug. Allowed characters: letters (A-Z, a-z), digits, hyphens (-), underscores (_), dots (.) and tildes (~). Input is lowercased before storage. If omitted, auto-generated.' }) @IsOptional() @IsString() @MaxLength(100) diff --git a/src/referrals/dto/update-referral-slug.dto.ts b/src/referrals/dto/update-referral-slug.dto.ts index e98f9232..2ed18dbc 100644 --- a/src/referrals/dto/update-referral-slug.dto.ts +++ b/src/referrals/dto/update-referral-slug.dto.ts @@ -3,7 +3,7 @@ import { IsArray, IsEmail, IsEnum, IsOptional, IsString, MaxLength } from 'class import { ReferralEntityStatus, ReferralEntitySubType, ReferralEntityType } from '../referrals.types'; export class UpdateReferralSlugDto { - @ApiPropertyOptional({ description: 'New slug (any format accepted; will be normalized to lowercase a-z0-9_)' }) + @ApiPropertyOptional({ description: 'New slug. Allowed characters: letters (A-Z, a-z), digits, hyphens (-), underscores (_), dots (.) and tildes (~). Input is lowercased before storage.' }) @IsOptional() @IsString() @MaxLength(100) diff --git a/src/referrals/referrals.service.ts b/src/referrals/referrals.service.ts index 6985945b..34771ba6 100644 --- a/src/referrals/referrals.service.ts +++ b/src/referrals/referrals.service.ts @@ -16,6 +16,7 @@ import { buildReferLink, generateReferralSlug, isValidUserProvidedSlug, + normalizeUserProvidedSlug, standardizeSlugInput, } from './utils/referral-slug.util'; @@ -66,15 +67,15 @@ export class ReferralsService { }); if (dto.slug) { - const trimmedSlug = dto.slug.trim(); - if (!trimmedSlug || !isValidUserProvidedSlug(trimmedSlug)) { + const normalizedSlug = normalizeUserProvidedSlug(dto.slug); + if (!normalizedSlug || !isValidUserProvidedSlug(normalizedSlug)) { throw new BadRequestException('Slug may only contain letters, digits, hyphens (-), underscores (_), dots (.) and tildes (~)'); } - const slugExists = await this.slugExistsAnywhere(trimmedSlug); + const slugExists = await this.slugExistsAnywhere(normalizedSlug); if (slugExists) { - throw new ConflictException(`Slug '${trimmedSlug}' already exists`); + throw new ConflictException(`Slug '${normalizedSlug}' already exists`); } - entity.slug = trimmedSlug; + entity.slug = normalizedSlug; } else { entity.slug = await this.generateUniqueSlug({ type: dto.type, @@ -247,13 +248,13 @@ export class ReferralsService { // ── Slug: normalize any format, check uniqueness, preserve history ─────── let pendingSlug: string | null = null; if (dto.slug !== undefined) { - const trimmedSlug = dto.slug.trim(); - if (!trimmedSlug || !isValidUserProvidedSlug(trimmedSlug)) { + const normalizedSlug = normalizeUserProvidedSlug(dto.slug); + if (!normalizedSlug || !isValidUserProvidedSlug(normalizedSlug)) { throw new BadRequestException('Slug may only contain letters, digits, hyphens (-), underscores (_), dots (.) and tildes (~)'); } - if (trimmedSlug !== entity.slug) { - await this.assertSlugUnique(trimmedSlug); - pendingSlug = trimmedSlug; + if (normalizedSlug !== entity.slug) { + await this.assertSlugUnique(normalizedSlug); + pendingSlug = normalizedSlug; } } diff --git a/src/referrals/utils/referral-slug.util.ts b/src/referrals/utils/referral-slug.util.ts index 599e400b..8af21214 100644 --- a/src/referrals/utils/referral-slug.util.ts +++ b/src/referrals/utils/referral-slug.util.ts @@ -22,9 +22,16 @@ function randomBase36(length: number): string { return asBase36.padStart(length, '0').slice(0, length); } -// Allowed chars in a user-provided slug: letters, digits, -, _, ., ~ -const USER_SLUG_PATTERN = /^[a-zA-Z0-9\-_\.~]+$/; +// Allowed chars after lowercasing: digits, a-z, -, _, ., ~ +// Input may contain uppercase letters \u2014 they are lowercased before validation and storage. +const USER_SLUG_PATTERN = /^[a-z0-9\-_\.~]+$/; +// Normalize a user-provided slug: trim whitespace and lowercase. +export function normalizeUserProvidedSlug(slug: string): string { + return String(slug ?? '').trim().toLowerCase(); +} + +// Returns true when the slug (already normalized) contains only allowed chars. export function isValidUserProvidedSlug(slug: string): boolean { return USER_SLUG_PATTERN.test(slug); } @@ -40,9 +47,9 @@ function normalizeNamePart(input: string): string { .replace(/[^a-z0-9_]/g, ''); } -// Keep for resolveSlug backward compat \u2014 only trims, no stripping +// Trim + lowercase: used by resolveSlug for consistent lookup against stored slugs. export function standardizeSlugInput(input: string): string { - return String(input ?? '').trim(); + return String(input ?? '').trim().toLowerCase(); } export function standardizeNameForSlug(firstName: string, lastName?: string) {