feat: Add invite code validation to user registration flow
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- 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 <noreply@anthropic.com>
This commit is contained in:
Andrei
2025-10-08 11:47:54 +00:00
parent 2ed1c09657
commit cfb2a1e312
3 changed files with 67 additions and 0 deletions

View File

@@ -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,

View File

@@ -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<Family>,
@InjectRepository(FamilyMember)
private familyMemberRepository: Repository<FamilyMember>,
@InjectRepository(InviteCode)
private inviteCodeRepository: Repository<InviteCode>,
@InjectRepository(InviteCodeUse)
private inviteCodeUseRepository: Repository<InviteCodeUse>,
private jwtService: JwtService,
private configService: ConfigService,
private auditService: AuditService,
) {}
async register(registerDto: RegisterDto): Promise<AuthResponse> {
// Check registration mode and validate invite code if required
const registrationMode = this.configService.get<string>('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`,

View File

@@ -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;
}