From f2a7bf5502fc7ca4e687d986850aacf7ca64d007 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Wed, 22 Apr 2026 17:14:50 +0100 Subject: [PATCH 1/6] feat: implement user verification documents (Issue #323) - Add VerificationDocument model and VerificationStatus enum to schema - Create DTOs for verification document operations - Implement VerificationDocuments service with full CRUD - Add user endpoints for document upload and management - Add admin endpoints for document review and approval - Support for document upload, status tracking, admin review - Include admin notes and review timestamp - Pagination support for admin document listing - Access control: users can only access their own documents - Update UsersModule to include verification feature --- src/users/dto/verification-document.dto.ts | 42 ++++++ .../verification-documents.controller.ts | 77 +++++++++++ src/users/verification-documents.service.ts | 127 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 src/users/dto/verification-document.dto.ts create mode 100644 src/users/verification-documents.controller.ts create mode 100644 src/users/verification-documents.service.ts diff --git a/src/users/dto/verification-document.dto.ts b/src/users/dto/verification-document.dto.ts new file mode 100644 index 00000000..19632912 --- /dev/null +++ b/src/users/dto/verification-document.dto.ts @@ -0,0 +1,42 @@ +import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; +import { VerificationStatus } from '@prisma/client'; + +export class CreateVerificationDocumentDto { + @IsString() + documentType: string; + + @IsString() + fileName: string; + + @IsString() + fileUrl: string; + + @IsString() + fileSize: string; + + @IsString() + mimeType: string; +} + +export class ReviewVerificationDocumentDto { + @IsEnum(VerificationStatus) + status: VerificationStatus; + + @IsOptional() + @IsString() + adminNotes?: string; +} + +export class UpdateVerificationDocumentDto { + @IsOptional() + @IsString() + documentType?: string; + + @IsOptional() + @IsString() + fileName?: string; + + @IsOptional() + @IsString() + fileUrl?: string; +} diff --git a/src/users/verification-documents.controller.ts b/src/users/verification-documents.controller.ts new file mode 100644 index 00000000..82c7aa0a --- /dev/null +++ b/src/users/verification-documents.controller.ts @@ -0,0 +1,77 @@ +import { Controller, Get, Post, Body, Param, Put, Delete, UseGuards, Query } from '@nestjs/common'; +import { VerificationDocumentsService } from './verification-documents.service'; +import { CreateVerificationDocumentDto, ReviewVerificationDocumentDto, UpdateVerificationDocumentDto } from './dto/verification-document.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; + +@UseGuards(JwtAuthGuard) +@Controller('users/verification-documents') +export class VerificationDocumentsController { + constructor(private readonly verificationService: VerificationDocumentsService) {} + + @Post() + createDocument( + @CurrentUser() user: any, + @Body() createDto: CreateVerificationDocumentDto, + ) { + return this.verificationService.create(user.id, createDto); + } + + @Get() + getAllDocuments(@CurrentUser() user: any) { + return this.verificationService.findAllByUserId(user.id); + } + + @Get(':id') + getDocument(@CurrentUser() user: any, @Param('id') id: string) { + return this.verificationService.findOne(id, user.id); + } + + @Put(':id') + updateDocument( + @CurrentUser() user: any, + @Param('id') id: string, + @Body() updateDto: UpdateVerificationDocumentDto, + ) { + return this.verificationService.update(id, user.id, updateDto); + } + + @Delete(':id') + removeDocument(@CurrentUser() user: any, @Param('id') id: string) { + return this.verificationService.remove(id, user.id); + } +} + +// Admin controller for reviewing verification documents +@UseGuards(JwtAuthGuard) +@Controller('admin/verification-documents') +export class AdminVerificationDocumentsController { + constructor(private readonly verificationService: VerificationDocumentsService) {} + + @Get() + getAllDocumentsForAdmin( + @Query('page') page?: string, + @Query('limit') limit?: string, + @Query('status') status?: string, + ) { + return this.verificationService.findAllForAdmin( + page ? parseInt(page) : 1, + limit ? parseInt(limit) : 20, + status, + ); + } + + @Get(':id') + getDocumentForAdmin(@Param('id') id: string) { + return this.verificationService.findOne(id, ''); + } + + @Put(':id/review') + reviewDocument( + @CurrentUser() user: any, + @Param('id') id: string, + @Body() reviewDto: ReviewVerificationDocumentDto, + ) { + return this.verificationService.review(id, user.id, reviewDto); + } +} diff --git a/src/users/verification-documents.service.ts b/src/users/verification-documents.service.ts new file mode 100644 index 00000000..f64dfdcb --- /dev/null +++ b/src/users/verification-documents.service.ts @@ -0,0 +1,127 @@ +import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { PrismaService } from '../database/prisma.service'; +import { CreateVerificationDocumentDto, ReviewVerificationDocumentDto, UpdateVerificationDocumentDto } from './dto/verification-document.dto'; + +@Injectable() +export class VerificationDocumentsService { + constructor(private prisma: PrismaService) {} + + async create(userId: string, data: CreateVerificationDocumentDto) { + return this.prisma.verificationDocument.create({ + data: { + userId, + documentType: data.documentType, + fileName: data.fileName, + fileUrl: data.fileUrl, + fileSize: parseInt(data.fileSize), + mimeType: data.mimeType, + status: 'PENDING', + }, + }); + } + + async findAllByUserId(userId: string) { + return this.prisma.verificationDocument.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + }); + } + + async findOne(id: string, userId: string) { + const document = await this.prisma.verificationDocument.findUnique({ + where: { id }, + }); + + if (!document) { + throw new NotFoundException('Verification document not found'); + } + + if (document.userId !== userId) { + throw new ForbiddenException('You do not have access to this document'); + } + + return document; + } + + async update(id: string, userId: string, data: UpdateVerificationDocumentDto) { + const document = await this.findOne(id, userId); + + // Only allow updates if document is still pending + if (document.status !== 'PENDING') { + throw new ForbiddenException('Cannot update a document that has been reviewed'); + } + + return this.prisma.verificationDocument.update({ + where: { id }, + data, + }); + } + + async review(id: string, adminId: string, data: ReviewVerificationDocumentDto) { + const document = await this.prisma.verificationDocument.findUnique({ + where: { id }, + }); + + if (!document) { + throw new NotFoundException('Verification document not found'); + } + + return this.prisma.verificationDocument.update({ + where: { id }, + data: { + status: data.status, + adminNotes: data.adminNotes, + reviewedBy: adminId, + reviewedAt: new Date(), + }, + }); + } + + async remove(id: string, userId: string) { + const document = await this.findOne(id, userId); + + // Only allow deletion if document is still pending + if (document.status !== 'PENDING') { + throw new ForbiddenException('Cannot delete a document that has been reviewed'); + } + + return this.prisma.verificationDocument.delete({ + where: { id }, + }); + } + + // Admin methods + async findAllForAdmin(page = 1, limit = 20, status?: string) { + const skip = (page - 1) * limit; + + const where = status ? { status } : {}; + + const [documents, total] = await Promise.all([ + this.prisma.verificationDocument.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + }), + this.prisma.verificationDocument.count({ where }), + ]); + + return { + documents, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }; + } +} From 2087b63522e49a483bcfb6bb7c60e5b7e63b172f Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Wed, 22 Apr 2026 17:46:39 +0100 Subject: [PATCH 2/6] feat: add email change verification (Issue #301) - Add pendingEmail, emailVerificationToken, and emailVerificationExpires fields to User model - Create DTOs for email change and verification - Implement EmailVerificationService with secure token generation - Add endpoint to request email change with verification - Add endpoint to verify email change with token - Add endpoint to cancel pending email change - Maintain old email until new email is verified - Token expires after 24 hours for security - Rollback on failure by clearing pending fields - Update UsersModule to include email verification Closes #323, Closes #301 --- prisma/schema.prisma | 3 + src/users/dto/email-change.dto.ts | 10 ++ src/users/email-verification.controller.ts | 32 +++++ src/users/email-verification.service.ts | 138 +++++++++++++++++++++ src/users/users.module.ts | 23 +++- 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 src/users/dto/email-change.dto.ts create mode 100644 src/users/email-verification.controller.ts create mode 100644 src/users/email-verification.service.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 503a6946..fc5cb311 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -73,6 +73,9 @@ model User { twoFactorSecret String? @map("two_factor_secret") twoFactorBackupCodes String[] @default([]) @map("two_factor_backup_codes") avatar String? + pendingEmail String? @map("pending_email") + emailVerificationToken String? @map("email_verification_token") + emailVerificationExpires DateTime? @map("email_verification_expires") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/src/users/dto/email-change.dto.ts b/src/users/dto/email-change.dto.ts new file mode 100644 index 00000000..2a6480c1 --- /dev/null +++ b/src/users/dto/email-change.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail } from 'class-validator'; + +export class ChangeEmailDto { + @IsEmail() + newEmail: string; +} + +export class VerifyEmailDto { + token: string; +} diff --git a/src/users/email-verification.controller.ts b/src/users/email-verification.controller.ts new file mode 100644 index 00000000..e320b409 --- /dev/null +++ b/src/users/email-verification.controller.ts @@ -0,0 +1,32 @@ +import { Controller, Post, Body, UseGuards } from '@nestjs/common'; +import { EmailVerificationService } from './email-verification.service'; +import { ChangeEmailDto, VerifyEmailDto } from './dto/email-change.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; + +@UseGuards(JwtAuthGuard) +@Controller('users/email') +export class EmailVerificationController { + constructor(private readonly emailVerificationService: EmailVerificationService) {} + + @Post('change') + requestEmailChange( + @CurrentUser() user: any, + @Body() changeEmailDto: ChangeEmailDto, + ) { + return this.emailVerificationService.requestEmailChange(user.id, changeEmailDto); + } + + @Post('verify') + verifyEmailChange( + @CurrentUser() user: any, + @Body() verifyEmailDto: VerifyEmailDto, + ) { + return this.emailVerificationService.verifyEmailChange(user.id, verifyEmailDto.token); + } + + @Post('cancel-change') + cancelEmailChange(@CurrentUser() user: any) { + return this.emailVerificationService.cancelEmailChange(user.id); + } +} diff --git a/src/users/email-verification.service.ts b/src/users/email-verification.service.ts new file mode 100644 index 00000000..b970f5b9 --- /dev/null +++ b/src/users/email-verification.service.ts @@ -0,0 +1,138 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../database/prisma.service'; +import { ChangeEmailDto } from './dto/email-change.dto'; +import { randomBytes } from 'crypto'; + +@Injectable() +export class EmailVerificationService { + constructor(private prisma: PrismaService) {} + + async requestEmailChange(userId: string, data: ChangeEmailDto) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + // Check if new email is already in use + const existingUser = await this.prisma.user.findUnique({ + where: { email: data.newEmail }, + }); + + if (existingUser) { + throw new BadRequestException('Email is already in use'); + } + + // Generate verification token + const token = randomBytes(32).toString('hex'); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); // Token expires in 24 hours + + // Store pending email and token + await this.prisma.user.update({ + where: { id: userId }, + data: { + pendingEmail: data.newEmail, + emailVerificationToken: token, + emailVerificationExpires: expiresAt, + }, + }); + + // TODO: Send verification email with token + // In production, integrate with email service (SendGrid, AWS SES, etc.) + console.log(`Verification token for ${data.newEmail}: ${token}`); + + return { + message: 'Verification email sent. Please check your new email to verify the change.', + pendingEmail: data.newEmail, + }; + } + + async verifyEmailChange(userId: string, token: string) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (!user.emailVerificationToken || !user.emailVerificationExpires) { + throw new BadRequestException('No pending email change'); + } + + // Check if token is expired + if (new Date() > user.emailVerificationExpires) { + // Clear expired token + await this.prisma.user.update({ + where: { id: userId }, + data: { + pendingEmail: null, + emailVerificationToken: null, + emailVerificationExpires: null, + }, + }); + throw new BadRequestException('Verification token has expired'); + } + + // Verify token + if (user.emailVerificationToken !== token) { + throw new BadRequestException('Invalid verification token'); + } + + if (!user.pendingEmail) { + throw new BadRequestException('No pending email to verify'); + } + + // Update email and clear verification fields + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { + email: user.pendingEmail, + pendingEmail: null, + emailVerificationToken: null, + emailVerificationExpires: null, + }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + updatedAt: true, + }, + }); + + return { + message: 'Email changed successfully', + user: updatedUser, + }; + } + + async cancelEmailChange(userId: string) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (!user.pendingEmail) { + throw new BadRequestException('No pending email change'); + } + + // Clear pending email and token + await this.prisma.user.update({ + where: { id: userId }, + data: { + pendingEmail: null, + emailVerificationToken: null, + emailVerificationExpires: null, + }, + }); + + return { message: 'Email change cancelled successfully' }; + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts index e7e0343f..b671ebfd 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,12 +1,29 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; +import { VerificationDocumentsService } from './verification-documents.service'; +import { VerificationDocumentsController, AdminVerificationDocumentsController } from './verification-documents.controller'; +import { EmailVerificationService } from './email-verification.service'; +import { EmailVerificationController } from './email-verification.controller'; import { PrismaModule } from '../database/prisma.module'; @Module({ imports: [PrismaModule], - controllers: [UsersController], - providers: [UsersService], - exports: [UsersService], + controllers: [ + UsersController, + VerificationDocumentsController, + AdminVerificationDocumentsController, + EmailVerificationController, + ], + providers: [ + UsersService, + VerificationDocumentsService, + EmailVerificationService, + ], + exports: [ + UsersService, + VerificationDocumentsService, + EmailVerificationService, + ], }) export class UsersModule {} From 68f142bfd1e13528588388757b35289ad2faa589 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Thu, 23 Apr 2026 08:43:12 +0100 Subject: [PATCH 3/6] fix: add missing VerificationDocument model to schema - Add VerificationDocument model and VerificationStatus enum - Fix type errors in verification documents service - Import VerificationStatus from @prisma/client Closes #323, Closes #301 --- prisma/schema.prisma | 32 +++++++++++++++++++ src/users/email-verification.controller.ts | 10 ++---- src/users/users.module.ts | 17 ++++------ .../verification-documents.controller.ts | 11 ++++--- src/users/verification-documents.service.ts | 11 +++++-- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fc5cb311..11fe364f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -59,6 +59,13 @@ enum DocumentType { FLOOR_PLAN } +// Verification document status +enum VerificationStatus { + PENDING + APPROVED + REJECTED +} + // User model model User { id String @id @default(uuid()) @@ -87,6 +94,7 @@ model User { apiKeys ApiKey[] passwordHistory PasswordHistory[] blacklistedTokens BlacklistedToken[] + verificationDocuments VerificationDocument[] @@index([email]) @@index([role]) @@ -228,3 +236,27 @@ model Document { @@index([documentType]) @@map("documents") } + +// Verification Document model +model VerificationDocument { + id String @id @default(uuid()) + userId String @map("user_id") + documentType String @map("document_type") // ID_CARD, PASSPORT, DRIVER_LICENSE, PROOF_OF_ADDRESS, etc. + fileName String @map("file_name") + fileUrl String @map("file_url") + fileSize Int @map("file_size") // in bytes + mimeType String @map("mime_type") + status VerificationStatus @default(PENDING) + adminNotes String? @map("admin_notes") + reviewedBy String? @map("reviewed_by") + reviewedAt DateTime? @map("reviewed_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([status]) + @@index([documentType]) + @@map("verification_documents") +} diff --git a/src/users/email-verification.controller.ts b/src/users/email-verification.controller.ts index e320b409..00bdd043 100644 --- a/src/users/email-verification.controller.ts +++ b/src/users/email-verification.controller.ts @@ -10,18 +10,12 @@ export class EmailVerificationController { constructor(private readonly emailVerificationService: EmailVerificationService) {} @Post('change') - requestEmailChange( - @CurrentUser() user: any, - @Body() changeEmailDto: ChangeEmailDto, - ) { + requestEmailChange(@CurrentUser() user: any, @Body() changeEmailDto: ChangeEmailDto) { return this.emailVerificationService.requestEmailChange(user.id, changeEmailDto); } @Post('verify') - verifyEmailChange( - @CurrentUser() user: any, - @Body() verifyEmailDto: VerifyEmailDto, - ) { + verifyEmailChange(@CurrentUser() user: any, @Body() verifyEmailDto: VerifyEmailDto) { return this.emailVerificationService.verifyEmailChange(user.id, verifyEmailDto.token); } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index b671ebfd..0317dfef 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -2,7 +2,10 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { VerificationDocumentsService } from './verification-documents.service'; -import { VerificationDocumentsController, AdminVerificationDocumentsController } from './verification-documents.controller'; +import { + VerificationDocumentsController, + AdminVerificationDocumentsController, +} from './verification-documents.controller'; import { EmailVerificationService } from './email-verification.service'; import { EmailVerificationController } from './email-verification.controller'; import { PrismaModule } from '../database/prisma.module'; @@ -15,15 +18,7 @@ import { PrismaModule } from '../database/prisma.module'; AdminVerificationDocumentsController, EmailVerificationController, ], - providers: [ - UsersService, - VerificationDocumentsService, - EmailVerificationService, - ], - exports: [ - UsersService, - VerificationDocumentsService, - EmailVerificationService, - ], + providers: [UsersService, VerificationDocumentsService, EmailVerificationService], + exports: [UsersService, VerificationDocumentsService, EmailVerificationService], }) export class UsersModule {} diff --git a/src/users/verification-documents.controller.ts b/src/users/verification-documents.controller.ts index 82c7aa0a..bd79a5f3 100644 --- a/src/users/verification-documents.controller.ts +++ b/src/users/verification-documents.controller.ts @@ -1,6 +1,10 @@ import { Controller, Get, Post, Body, Param, Put, Delete, UseGuards, Query } from '@nestjs/common'; import { VerificationDocumentsService } from './verification-documents.service'; -import { CreateVerificationDocumentDto, ReviewVerificationDocumentDto, UpdateVerificationDocumentDto } from './dto/verification-document.dto'; +import { + CreateVerificationDocumentDto, + ReviewVerificationDocumentDto, + UpdateVerificationDocumentDto, +} from './dto/verification-document.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; @@ -10,10 +14,7 @@ export class VerificationDocumentsController { constructor(private readonly verificationService: VerificationDocumentsService) {} @Post() - createDocument( - @CurrentUser() user: any, - @Body() createDto: CreateVerificationDocumentDto, - ) { + createDocument(@CurrentUser() user: any, @Body() createDto: CreateVerificationDocumentDto) { return this.verificationService.create(user.id, createDto); } diff --git a/src/users/verification-documents.service.ts b/src/users/verification-documents.service.ts index f64dfdcb..bddf4b9e 100644 --- a/src/users/verification-documents.service.ts +++ b/src/users/verification-documents.service.ts @@ -1,6 +1,11 @@ import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; -import { CreateVerificationDocumentDto, ReviewVerificationDocumentDto, UpdateVerificationDocumentDto } from './dto/verification-document.dto'; +import { + CreateVerificationDocumentDto, + ReviewVerificationDocumentDto, + UpdateVerificationDocumentDto, +} from './dto/verification-document.dto'; +import { VerificationStatus } from '@prisma/client'; @Injectable() export class VerificationDocumentsService { @@ -93,8 +98,8 @@ export class VerificationDocumentsService { // Admin methods async findAllForAdmin(page = 1, limit = 20, status?: string) { const skip = (page - 1) * limit; - - const where = status ? { status } : {}; + + const where: any = status ? { status: status as VerificationStatus } : {}; const [documents, total] = await Promise.all([ this.prisma.verificationDocument.findMany({ From 327a2fb9bf36ae43d1291270e18708ab6bb2a514 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Thu, 23 Apr 2026 09:04:39 +0100 Subject: [PATCH 4/6] chore: apply linting fixes to formatting --- src/users/verification-documents.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/users/verification-documents.service.ts b/src/users/verification-documents.service.ts index bddf4b9e..745d4458 100644 --- a/src/users/verification-documents.service.ts +++ b/src/users/verification-documents.service.ts @@ -98,7 +98,7 @@ export class VerificationDocumentsService { // Admin methods async findAllForAdmin(page = 1, limit = 20, status?: string) { const skip = (page - 1) * limit; - + const where: any = status ? { status: status as VerificationStatus } : {}; const [documents, total] = await Promise.all([ From 0708f5062d69b5d627f1a5e365b688b5aad19101 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Thu, 23 Apr 2026 11:52:04 +0100 Subject: [PATCH 5/6] fix: add missing closing brace in schema.prisma - Fix VerificationDocument model syntax error - Resolve Prisma schema validation error Closes #323, Closes #301 --- prisma/schema.prisma | 2 ++ src/users/dto/user.dto.ts | 11 ++++++++++- src/users/user-import.controller.ts | 4 +++- src/users/user-import.service.ts | 4 ++-- src/users/users.module.ts | 20 +++++++++++++++++--- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d63c00ac..d496b122 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -333,6 +333,8 @@ model VerificationDocument { @@index([status]) @@index([documentType]) @@map("verification_documents") +} + // Session model for tracking user sessions model Session { id String @id @default(uuid()) diff --git a/src/users/dto/user.dto.ts b/src/users/dto/user.dto.ts index b9f6ad0f..6c0176f6 100644 --- a/src/users/dto/user.dto.ts +++ b/src/users/dto/user.dto.ts @@ -1,4 +1,13 @@ -import { IsEmail, IsOptional, IsString, MinLength, IsIn, IsObject, IsInt, Min } from 'class-validator'; +import { + IsEmail, + IsOptional, + IsString, + MinLength, + IsIn, + IsObject, + IsInt, + Min, +} from 'class-validator'; import { Type } from 'class-transformer'; export class UpdatePreferencesDto { diff --git a/src/users/user-import.controller.ts b/src/users/user-import.controller.ts index caee8453..106d18b5 100644 --- a/src/users/user-import.controller.ts +++ b/src/users/user-import.controller.ts @@ -28,7 +28,9 @@ export class UserImportController { // Basic file type validation const allowedExtensions = ['.csv']; - const isCsvExtension = allowedExtensions.some((ext) => file.originalname.toLowerCase().endsWith(ext)); + const isCsvExtension = allowedExtensions.some((ext) => + file.originalname.toLowerCase().endsWith(ext), + ); const isCsvMime = file.mimetype === 'text/csv' || file.mimetype === 'application/vnd.ms-excel'; if (!isCsvExtension && !isCsvMime) { diff --git a/src/users/user-import.service.ts b/src/users/user-import.service.ts index 787aa963..2ad29a8f 100644 --- a/src/users/user-import.service.ts +++ b/src/users/user-import.service.ts @@ -79,12 +79,12 @@ export class UserImportService { } const hashedPassword = await hashPassword(password); - + // Generate unique referral code let referralCode: string; let isUnique = false; let attempts = 0; - + // Basic unique code generation do { referralCode = Math.random().toString(36).substring(2, 8).toUpperCase(); diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 68eb53fb..7cfae891 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -21,13 +21,27 @@ import { AuthModule } from '../auth/auth.module'; imports: [PrismaModule, AuthModule, ScheduleModule.forRoot()], controllers: [ UsersController, - AvatarUploadController, + AvatarUploadController, UserImportController, VerificationDocumentsController, AdminVerificationDocumentsController, EmailVerificationController, ], - providers: [UsersService,AvatarUploadService, ScheduledDeletionService, UserImportService, VerificationDocumentsService, EmailVerificationService], - exports: [UsersService,AvatarUploadService, ScheduledDeletionService, UserImportService, VerificationDocumentsService, EmailVerificationService], + providers: [ + UsersService, + AvatarUploadService, + ScheduledDeletionService, + UserImportService, + VerificationDocumentsService, + EmailVerificationService, + ], + exports: [ + UsersService, + AvatarUploadService, + ScheduledDeletionService, + UserImportService, + VerificationDocumentsService, + EmailVerificationService, + ], }) export class UsersModule {} From c6c3a7b08f675f9e07acb43e0cda99fc85a2532f Mon Sep 17 00:00:00 2001 From: "Abdulmalik A. (Personal)" Date: Thu, 23 Apr 2026 13:28:24 +0100 Subject: [PATCH 6/6] fix: add missing verificationDocuments relation to User model - Add verificationDocuments relation field to User model - Fix Prisma schema validation error P1012 - Regenerate Prisma client with complete schema Closes #323, Closes #301 --- prisma/schema.prisma | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ff79cf3..3ff33773 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -108,6 +108,7 @@ model User { blacklistedTokens BlacklistedToken[] preferences UserPreferences? activityLogs ActivityLog[] + verificationDocuments VerificationDocument[] sessions Session[] passwordResetTokens PasswordResetToken[] loginHistory LoginHistory[] @@ -377,3 +378,27 @@ model Session { @@index([isRevoked]) @@map("sessions") } + +// Verification Document model +model VerificationDocument { + id String @id @default(uuid()) + userId String @map("user_id") + documentType String @map("document_type") // ID_CARD, PASSPORT, DRIVER_LICENSE, PROOF_OF_ADDRESS, etc. + fileName String @map("file_name") + fileUrl String @map("file_url") + fileSize Int @map("file_size") // in bytes + mimeType String @map("mime_type") + status VerificationStatus @default(PENDING) + adminNotes String? @map("admin_notes") + reviewedBy String? @map("reviewed_by") + reviewedAt DateTime? @map("reviewed_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([status]) + @@index([documentType]) + @@map("verification_documents") +}