From cfb2a1e3126605ea938080c1fa0e21ab98289124 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 8 Oct 2025 11:47:54 +0000 Subject: [PATCH] feat: Add invite code validation to user registration flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added InviteCode and InviteCodeUse entity imports to auth.service.ts and auth.module.ts - Added inviteCode field to RegisterDto as optional string - Implemented registration mode check (invite_only vs public) - Added invite code validation logic at registration: * Check if code exists and is active * Validate expiration date * Verify usage limit not exceeded - Record invite code usage after successful registration: * Increment usage counter * Create invite_code_uses record with user details * Add audit logging for invite code usage - Registration now enforces REGISTRATION_MODE environment variable - Users get clear error messages for invalid/expired/used-up invite codes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/modules/auth/auth.module.ts | 3 + .../src/modules/auth/auth.service.ts | 60 +++++++++++++++++++ .../src/modules/auth/dto/register.dto.ts | 4 ++ 3 files changed, 67 insertions(+) diff --git a/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts b/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts index b68a8b2..f5345a6 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts @@ -22,6 +22,7 @@ import { Family, FamilyMember, } from '../../database/entities'; +import { InviteCode, InviteCodeUse } from '../invite-codes/invite-codes.entity'; @Module({ imports: [ @@ -33,6 +34,8 @@ import { Family, FamilyMember, WebAuthnCredential, + InviteCode, + InviteCodeUse, ]), PassportModule, CommonModule, diff --git a/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts b/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts index de7a14d..de480da 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts @@ -20,6 +20,7 @@ import { AuditAction, EntityType, } from '../../database/entities'; +import { InviteCode, InviteCodeUse } from '../invite-codes/invite-codes.entity'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; import { RefreshTokenDto } from './dto/refresh-token.dto'; @@ -43,12 +44,49 @@ export class AuthService { private familyRepository: Repository, @InjectRepository(FamilyMember) private familyMemberRepository: Repository, + @InjectRepository(InviteCode) + private inviteCodeRepository: Repository, + @InjectRepository(InviteCodeUse) + private inviteCodeUseRepository: Repository, private jwtService: JwtService, private configService: ConfigService, private auditService: AuditService, ) {} async register(registerDto: RegisterDto): Promise { + // Check registration mode and validate invite code if required + const registrationMode = this.configService.get('REGISTRATION_MODE', 'invite_only'); + const requireInviteCode = registrationMode === 'invite_only'; + + let validatedInviteCode: InviteCode | null = null; + + if (requireInviteCode) { + if (!registerDto.inviteCode) { + throw new BadRequestException( + 'An invite code is required to register. Please contact an administrator for an invite code.', + ); + } + + // Validate the invite code + validatedInviteCode = await this.inviteCodeRepository.findOne({ + where: { code: registerDto.inviteCode, isActive: true }, + }); + + if (!validatedInviteCode) { + throw new BadRequestException('Invalid or inactive invite code'); + } + + // Check if the invite code has expired + if (validatedInviteCode.expiresAt && new Date(validatedInviteCode.expiresAt) < new Date()) { + throw new BadRequestException('This invite code has expired'); + } + + // Check if the invite code has reached its maximum number of uses + if (validatedInviteCode.maxUses && validatedInviteCode.uses >= validatedInviteCode.maxUses) { + throw new BadRequestException('This invite code has reached its maximum number of uses'); + } + } + // Check if user already exists const existingUser = await this.userRepository.findOne({ where: { email: registerDto.email }, @@ -99,6 +137,28 @@ export class AuthService { const savedUser = await this.userRepository.save(user); + // Record invite code usage if applicable + if (validatedInviteCode) { + // Increment the usage counter + validatedInviteCode.uses += 1; + await this.inviteCodeRepository.save(validatedInviteCode); + + // Record the invite code usage + const inviteCodeUse = this.inviteCodeUseRepository.create({ + inviteCodeId: validatedInviteCode.id, + usedBy: savedUser.id, + userEmail: savedUser.email, + userIp: null, // Could be passed from the request if needed + userAgent: null, // Could be passed from the request if needed + }); + + await this.inviteCodeUseRepository.save(inviteCodeUse); + + this.logger.log( + `User ${savedUser.email} registered using invite code ${validatedInviteCode.code}`, + ); + } + // Create default family for user const family = this.familyRepository.create({ name: `${registerDto.name}'s Family`, diff --git a/maternal-app/maternal-app-backend/src/modules/auth/dto/register.dto.ts b/maternal-app/maternal-app-backend/src/modules/auth/dto/register.dto.ts index 0b35cad..dae2e5e 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/dto/register.dto.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/dto/register.dto.ts @@ -57,6 +57,10 @@ export class RegisterDto { @IsEmail() parentalEmail?: string; // Parent/guardian email for users 13-17 + @IsOptional() + @IsString() + inviteCode?: string; // Invite code (required if REGISTRATION_MODE=invite_only) + @IsObject() deviceInfo: DeviceInfoDto; }