diff --git a/.lintstagedrc.json b/.lintstagedrc.json index c68ab0ad..8f55e693 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,4 @@ { - "concurrent": false, "*.ts": ["eslint --fix", "prettier --write"], "*.json": ["prettier --write"], "*.md": ["prettier --write"] diff --git a/src/cache/cache-monitoring.service.ts b/src/cache/cache-monitoring.service.ts index 99db1db8..348ea456 100644 --- a/src/cache/cache-monitoring.service.ts +++ b/src/cache/cache-monitoring.service.ts @@ -1,10 +1,10 @@ +import { Injectable, Logger } from '@nestjs/common'; + /** * Cache Monitoring Service - * Monitors cache performance, hits/misses, and health + * Monitors cache performance, hits/misses, and health. */ -import { Injectable, Logger } from '@nestjs/common'; - export interface CacheMetrics { hits: number; misses: number; @@ -22,46 +22,54 @@ export interface CacheHealthStatus { commandsProcessed: number; } +interface CacheMetricState { + hits: number; + misses: number; + totalRequests: number; + responseTimes: number[]; +} + @Injectable() export class CacheMonitoringService { private readonly logger = new Logger(CacheMonitoringService.name); - private metrics = { + private metrics: CacheMetricState = { hits: 0, misses: 0, totalRequests: 0, - responseTimes: [] as number[], + responseTimes: [], }; /** - * Record cache hit + * Record cache hit. */ recordHit(): void { - this.metrics.hits++; - this.metrics.totalRequests++; + this.metrics.hits += 1; + this.metrics.totalRequests += 1; } /** - * Record cache miss + * Record cache miss. */ recordMiss(): void { - this.metrics.misses++; - this.metrics.totalRequests++; + this.metrics.misses += 1; + this.metrics.totalRequests += 1; } /** - * Record response time + * Record response time. */ recordResponseTime(timeMs: number): void { this.metrics.responseTimes.push(timeMs); - // Keep only last 1000 measurements + + // Keep only the last 1000 measurements. if (this.metrics.responseTimes.length > 1000) { this.metrics.responseTimes.shift(); } } /** - * Get current cache metrics + * Get current cache metrics. */ getMetrics(): CacheMetrics { const hitRate = @@ -69,21 +77,22 @@ export class CacheMonitoringService { const avgResponseTime = this.metrics.responseTimes.length > 0 - ? this.metrics.responseTimes.reduce((a, b) => a + b, 0) / this.metrics.responseTimes.length + ? this.metrics.responseTimes.reduce((total, responseTime) => total + responseTime, 0) / + this.metrics.responseTimes.length : 0; return { hits: this.metrics.hits, misses: this.metrics.misses, - hitRate: parseFloat(hitRate.toFixed(2)), + hitRate: Number(hitRate.toFixed(2)), totalRequests: this.metrics.totalRequests, - avgResponseTime: parseFloat(avgResponseTime.toFixed(2)), + avgResponseTime: Number(avgResponseTime.toFixed(2)), timestamp: new Date(), }; } /** - * Reset metrics + * Reset metrics. */ resetMetrics(): void { this.metrics = { @@ -92,22 +101,23 @@ export class CacheMonitoringService { totalRequests: 0, responseTimes: [], }; + this.logger.log('Cache metrics reset'); } /** - * Get cache alerts based on performance + * Get cache alerts based on performance. */ getAlerts(): string[] { const alerts: string[] = []; const metrics = this.getMetrics(); - // Alert if hit rate is too low + // Alert if hit rate is too low. if (metrics.hitRate < 30 && metrics.totalRequests > 100) { alerts.push(`⚠️ Low cache hit rate: ${metrics.hitRate}%`); } - // Alert if average response time is high + // Alert if average response time is high. if (metrics.avgResponseTime > 100) { alerts.push(`⚠️ High average response time: ${metrics.avgResponseTime}ms`); } @@ -116,10 +126,11 @@ export class CacheMonitoringService { } /** - * Log cache performance summary + * Log cache performance summary. */ logSummary(): void { const metrics = this.getMetrics(); + this.logger.log( `Cache Performance - Hits: ${metrics.hits}, Misses: ${metrics.misses}, ` + `Hit Rate: ${metrics.hitRate}%, Avg Response: ${metrics.avgResponseTime}ms`, diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index a553fcb9..8c5362e8 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -1,4 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; +import type { Prisma } from '@prisma/client'; + import { PrismaService } from '../database/prisma.service'; import { NotificationsGateway } from './notifications.gateway'; import { EmailService } from '../email/email.service'; @@ -7,16 +9,15 @@ import { UserPreferencesService, shouldDeliverNotificationFromPrefs, } from '../users/user-preferences.service'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Transaction, TransactionStatus, User } from '@prisma/client'; /** * Schema-default notification preferences for users who have never set * their own. Mirrors the Prisma defaults in `prisma/schema.prisma` for - * UserPreferences (email:true, sms:false, inApp:true, push:false, - * eventTypes:[], quietHours disabled). Used by handleTransactionUpdate when - * `user.preferences` is null, avoiding the hidden upsert side-effect that - * `UserPreferencesService.findByUserId` performs when prefs are missing. + * UserPreferences. + * + * Used by handleTransactionUpdate when `user.preferences` is null, + * avoiding the hidden upsert side-effect that + * `UserPreferencesService.findByUserId` performs when preferences are missing. */ const NOTIFICATION_PREFERENCES_DEFAULTS = { emailNotifications: true, @@ -28,53 +29,70 @@ const NOTIFICATION_PREFERENCES_DEFAULTS = { quietHoursStart: null as string | null, quietHoursEnd: null as string | null, timezone: 'UTC', - perEventSettings: null as Record | null, + perEventSettings: null as Prisma.InputJsonValue | null, }; +type NotificationMetadata = Prisma.InputJsonValue; + @Injectable() export class NotificationsService { private readonly logger = new Logger(NotificationsService.name); constructor( - private prisma: PrismaService, - private gateway: NotificationsGateway, - private emailService: EmailService, - private smsService: SmsService, - private userPreferencesService: UserPreferencesService, + private readonly prisma: PrismaService, + private readonly gateway: NotificationsGateway, + private readonly emailService: EmailService, + private readonly smsService: SmsService, + private readonly userPreferencesService: UserPreferencesService, ) {} - async handleTransactionUpdate(transactionId: string) { + async handleTransactionUpdate(transactionId: string): Promise { const transaction = await this.prisma.transaction.findUnique({ where: { id: transactionId }, include: { - buyer: { include: { preferences: true } }, - seller: { include: { preferences: true } }, + buyer: { + include: { + preferences: true, + }, + }, + seller: { + include: { + preferences: true, + }, + }, property: true, }, }); - if (!transaction) return; + if (!transaction) { + return; + } const parties = [ - { user: transaction.buyer, role: 'Buyer' }, - { user: transaction.seller, role: 'Seller' }, + { + user: transaction.buyer, + role: 'Buyer', + }, + { + user: transaction.seller, + role: 'Seller', + }, ]; await Promise.all( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - parties.map(async ({ user, role }) => { + parties.map(async ({ user }) => { const title = `Transaction ${transaction.status}`; const message = `Your transaction for property "${transaction.property.title}" has been updated to ${transaction.status}.`; - // #765 — use the already-included user.preferences to avoid 3 redundant - // DB roundtrips per party. If a user has no preferences row, fall back - // to schema defaults locally instead of triggering the upsert - // side-effect in `UserPreferencesService.findByUserId`. - const prefs = (user.preferences ?? NOTIFICATION_PREFERENCES_DEFAULTS) as Parameters< - typeof shouldDeliverNotificationFromPrefs - >[0]; + // #765 — Use the already-included user.preferences to avoid + // redundant database round trips. If preferences do not exist, + // use the schema defaults locally. + const prefs = user.preferences ?? NOTIFICATION_PREFERENCES_DEFAULTS; + const canInApp = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'inApp'); + const canEmail = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'email'); + const canSms = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'sms'); await Promise.all([ @@ -84,6 +102,7 @@ export class NotificationsService { status: transaction.status, }) : Promise.resolve(), + canEmail ? this.emailService.sendTransactionStatusEmail(user.email, transaction.status, { transactionId: transaction.id, @@ -104,6 +123,7 @@ export class NotificationsService { transaction.status === 'CANCELLED' ? new Date().toLocaleDateString() : undefined, }) : Promise.resolve(), + canSms && user.phone ? this.smsService.sendSms(user.phone, message) : Promise.resolve(), ]); }), @@ -115,35 +135,40 @@ export class NotificationsService { title: string, message: string, type: string, - metadata?: any, + metadata?: NotificationMetadata, ) { - // 1. Save to database const notification = await this.prisma.notification.create({ data: { userId, title, message, type, - metadata: metadata || {}, + metadata: metadata ?? {}, }, }); - // 2. Try real-time delivery - // FCM Push Integration const user = await this.prisma.user.findUnique({ where: { id: userId }, - select: { fcmToken: true }, + select: { + fcmToken: true, + }, }); + if (user?.fcmToken) { this.logger.log(`Sending FCM notification to token: ${user.fcmToken}`); - // In production, use admin.messaging().send() here + + // In production, use Firebase Admin SDK: + // admin.messaging().send(...) } + const delivered = await this.gateway.sendToUser(userId, 'notification', notification); if (delivered) { await this.prisma.notification.update({ where: { id: notification.id }, - data: { status: 'DELIVERED' }, + data: { + status: 'DELIVERED', + }, }); } @@ -153,7 +178,9 @@ export class NotificationsService { async getUserNotifications(userId: string) { return this.prisma.notification.findMany({ where: { userId }, - orderBy: { createdAt: 'desc' }, + orderBy: { + createdAt: 'desc', + }, take: 50, }); } @@ -169,20 +196,36 @@ export class NotificationsService { return this.prisma.notification.update({ where: { id }, - data: { status: 'READ', readAt: new Date() }, + data: { + status: 'READ', + readAt: new Date(), + }, }); } async markAllAsRead(userId: string) { return this.prisma.notification.updateMany({ - where: { userId, status: { not: 'READ' } }, - data: { status: 'READ', readAt: new Date() }, + where: { + userId, + status: { + not: 'READ', + }, + }, + data: { + status: 'READ', + readAt: new Date(), + }, }); } async getUnreadCount(userId: string) { return this.prisma.notification.count({ - where: { userId, status: { not: 'READ' } }, + where: { + userId, + status: { + not: 'READ', + }, + }, }); } @@ -200,26 +243,36 @@ export class NotificationsService { }); } - async deliverPending(userId: string) { - // Issue #911 – Replace the N+1 loop (one UPDATE per notification) with a - // single batch UPDATE after collecting the IDs that were delivered. + async deliverPending(userId: string): Promise { + // Issue #911 – Replace the N+1 UPDATE operations with one batch UPDATE + // after collecting the IDs of successfully delivered notifications. const pending = await this.prisma.notification.findMany({ - where: { userId, status: 'PENDING' }, + where: { + userId, + status: 'PENDING', + }, }); const deliveredIds: string[] = []; + for (const notification of pending) { const delivered = await this.gateway.sendToUser(userId, 'notification', notification); + if (delivered) { deliveredIds.push(notification.id); } } if (deliveredIds.length > 0) { - // Single batch update instead of one UPDATE per notification await this.prisma.notification.updateMany({ - where: { id: { in: deliveredIds } }, - data: { status: 'DELIVERED' }, + where: { + id: { + in: deliveredIds, + }, + }, + data: { + status: 'DELIVERED', + }, }); } } @@ -229,7 +282,12 @@ export class NotificationsService { title: string, message: string, type: string, - scheduleData: { scheduledAt: Date; isRecurring?: boolean; cron?: string; timezone?: string }, + scheduleData: { + scheduledAt: Date; + isRecurring?: boolean; + cron?: string; + timezone?: string; + }, ) { return this.prisma.notification.create({ data: { @@ -248,7 +306,9 @@ export class NotificationsService { where: { id, status: 'PENDING', - scheduledAt: { not: null }, + scheduledAt: { + not: null, + }, }, }); } diff --git a/src/users/user-preferences.service.ts b/src/users/user-preferences.service.ts index 541e7eba..c0026540 100644 --- a/src/users/user-preferences.service.ts +++ b/src/users/user-preferences.service.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Injectable, NotFoundException } from '@nestjs/common'; +import type { Prisma } from '@prisma/client'; import { PrismaService } from '../database/prisma.service'; import { CreateUserPreferencesDto, @@ -201,7 +202,7 @@ export function shouldDeliverNotificationFromPrefs( quietHoursStart?: string | null; quietHoursEnd?: string | null; timezone?: string | null; - perEventSettings?: Record | null; + perEventSettings?: Prisma.InputJsonValue | null; }, eventType: string, channel: 'email' | 'sms' | 'push' | 'inApp', @@ -216,8 +217,20 @@ export function shouldDeliverNotificationFromPrefs( if (!channelMap[channel]) return false; // 2. Per-event channel override (channel explicitly disabled for this event) - const perEvent = (prefs.perEventSettings as Record | null) ?? {}; - if (perEvent[eventType] && perEvent[eventType][channel] === false) return false; + const rawPerEvent = prefs.perEventSettings ?? {}; + const perEvent = + rawPerEvent && typeof rawPerEvent === 'object' && !Array.isArray(rawPerEvent) + ? (rawPerEvent as Record) + : {}; + const eventOverride = perEvent[eventType]; + if ( + eventOverride && + typeof eventOverride === 'object' && + !Array.isArray(eventOverride) && + eventOverride[channel] === false + ) { + return false; + } // 3. Event-type subscription — empty array means all events are allowed const subscribedTypes: string[] = prefs.notificationEventTypes ?? []; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index d14ec0af..36533908 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -6,32 +6,57 @@ import { BadRequestException, } from '@nestjs/common'; import * as crypto from 'crypto'; -import { PrismaService } from '../database/prisma.service'; +import * as fs from 'fs'; +import * as path from 'path'; import { Prisma } from '@prisma/client'; + +import { PrismaService } from '../database/prisma.service'; import { redactEmail } from '../auth/security.utils'; -import { CreateUserDto, SearchUsersDto, UpdatePreferencesDto, UpdateUserDto } from './dto/user.dto'; -import { DeactivateAccountDto, ReactivateAccountDto } from './dto/deactivation.dto'; import { hashPassword, sanitizeUser, createSha256, generateReactivationToken, } from '../auth/security.utils'; -import * as fs from 'fs'; -import * as path from 'path'; +import { + CreateUserDto, + SearchUsersDto, + UpdatePreferencesDto, + UpdateUserDto, +} from './dto/user.dto'; +import { + DeactivateAccountDto, + ReactivateAccountDto, +} from './dto/deactivation.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { ProfileResponseDto } from './dto/profile-response.dto'; import { SessionsService } from '../sessions/sessions.service'; @Injectable() export class UsersService implements OnModuleInit { + private readonly logger = new Logger(UsersService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly sessionsService: SessionsService, + ) {} + async getProfile(userId: string): Promise { const user = await this.prisma.user.findUnique({ - where: { id: userId, isDeactivated: false }, + where: { + id: userId, + isDeactivated: false, + }, include: { - properties: { select: { id: true } }, - buyerTransactions: { select: { id: true } }, - sellerTransactions: { select: { id: true } }, + properties: { + select: { id: true }, + }, + buyerTransactions: { + select: { id: true }, + }, + sellerTransactions: { + select: { id: true }, + }, _count: { select: { properties: true, @@ -66,7 +91,10 @@ export class UsersService implements OnModuleInit { preferredChannel: user.preferredChannel, languagePreference: user.languagePreference, timezone: user.timezone, - contactHours: user.contactHours as { start: string; end: string } | null, + contactHours: user.contactHours as { + start: string; + end: string; + } | null, address: null, occupation: null, company: null, @@ -76,14 +104,17 @@ export class UsersService implements OnModuleInit { lastActivityAt: user.lastActivityAt, statistics: { propertiesCount: user._count.properties, - transactionsCount: user._count.buyerTransactions + user._count.sellerTransactions, + transactionsCount: + user._count.buyerTransactions + user._count.sellerTransactions, accountAgeDays, }, }; } - async updateProfile(userId: string, data: UpdateProfileDto): Promise { - // Check if email is being changed and if it's already taken + async updateProfile( + userId: string, + data: UpdateProfileDto, + ): Promise { if (data.email) { const existingUser = await this.prisma.user.findFirst({ where: { @@ -97,29 +128,50 @@ export class UsersService implements OnModuleInit { } } - // Build update data — only include provided fields const updateData: Prisma.UserUpdateInput = {}; - if (data.firstName !== undefined) updateData.firstName = data.firstName; - if (data.lastName !== undefined) updateData.lastName = data.lastName; - if (data.email !== undefined) updateData.email = data.email; - if (data.phone !== undefined) updateData.phone = data.phone; - if (data.avatar !== undefined) updateData.avatar = data.avatar; - if (data.preferredChannel !== undefined) updateData.preferredChannel = data.preferredChannel; - if (data.languagePreference !== undefined) + if (data.firstName !== undefined) { + updateData.firstName = data.firstName; + } + + if (data.lastName !== undefined) { + updateData.lastName = data.lastName; + } + + if (data.email !== undefined) { + updateData.email = data.email; + } + + if (data.phone !== undefined) { + updateData.phone = data.phone; + } + + if (data.avatar !== undefined) { + updateData.avatar = data.avatar; + } + + if (data.preferredChannel !== undefined) { + updateData.preferredChannel = data.preferredChannel; + } + + if (data.languagePreference !== undefined) { updateData.languagePreference = data.languagePreference; - if (data.timezone !== undefined) updateData.timezone = data.timezone; - if (data.contactHours !== undefined) - updateData.contactHours = data.contactHours as unknown as Prisma.InputJsonValue; + } - // Update user - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const updatedUser = await this.prisma.user.update({ + if (data.timezone !== undefined) { + updateData.timezone = data.timezone; + } + + if (data.contactHours !== undefined) { + updateData.contactHours = + data.contactHours as unknown as Prisma.InputJsonValue; + } + + await this.prisma.user.update({ where: { id: userId }, data: updateData, }); - // Log the profile update activity await this.prisma.activityLog.create({ data: { userId, @@ -127,29 +179,32 @@ export class UsersService implements OnModuleInit { entityType: 'USER', entityId: userId, description: 'User updated their profile', - metadata: { updatedFields: Object.keys(updateData) }, + metadata: { + updatedFields: Object.keys(updateData), + }, }, }); - // Return fresh profile return this.getProfile(userId); } - private readonly logger = new Logger(UsersService.name); - - constructor( - private prisma: PrismaService, - private readonly sessionsService: SessionsService, - ) {} + onModuleInit(): void { + setInterval( + () => { + void this.cleanupExports(); + }, + 60 * 60 * 1000, + ); - onModuleInit() { - setInterval(() => this.cleanupExports(), 60 * 60 * 1000); - this.cleanupExports(); + void this.cleanupExports(); } - private async cleanupExports() { + private async cleanupExports(): Promise { const exportsDir = path.join(process.cwd(), 'exports'); - if (!fs.existsSync(exportsDir)) return; + + if (!fs.existsSync(exportsDir)) { + return; + } const files = fs.readdirSync(exportsDir); const now = Date.now(); @@ -158,6 +213,7 @@ export class UsersService implements OnModuleInit { files.forEach((file) => { const filepath = path.join(exportsDir, file); const stats = fs.statSync(filepath); + if (now - stats.mtimeMs > expirationTime) { fs.unlinkSync(filepath); this.logger.log(`Deleted expired export file: ${file}`); @@ -180,7 +236,8 @@ export class UsersService implements OnModuleInit { } const propertiesCount = user.properties.length; - const transactionsCount = user.buyerTransactions.length + user.sellerTransactions.length; + const transactionsCount = + user.buyerTransactions.length + user.sellerTransactions.length; const now = new Date(); const createdAt = new Date(user.createdAt); @@ -200,15 +257,25 @@ export class UsersService implements OnModuleInit { const passwordHash = await hashPassword(data.password); let referralCode: string; + do { - referralCode = Math.random().toString(36).substring(2, 8).toUpperCase(); - } while (await this.prisma.user.findUnique({ where: { referralCode } })); + referralCode = Math.random() + .toString(36) + .substring(2, 8) + .toUpperCase(); + } while ( + await this.prisma.user.findUnique({ + where: { referralCode }, + }) + ); let referredById: string | null = null; + if (data.referralCode) { const referrer = await this.prisma.user.findUnique({ where: { referralCode: data.referralCode }, }); + if (referrer) { referredById = referrer.id; } @@ -424,7 +491,7 @@ export class UsersService implements OnModuleInit { }); if (!user) { - throw new Error('User not found'); + throw new NotFoundException('User not found'); } const safeUser = { @@ -450,11 +517,11 @@ export class UsersService implements OnModuleInit { }); if (!user) { - throw new Error('User not found'); + throw new NotFoundException('User not found'); } if (user.isDeactivated) { - throw new Error('Account is already deactivated'); + throw new BadRequestException('Account is already deactivated'); } const scheduledDeletionAt = data.scheduleDeletion @@ -480,7 +547,13 @@ export class UsersService implements OnModuleInit { }); this.logger.log( - `User ${userId} (${redactEmail(user.email)}) deactivated. Scheduled deletion: ${scheduledDeletionAt ? scheduledDeletionAt.toISOString() : 'None'}`, + `User ${userId} (${redactEmail( + user.email, + )}) deactivated. Scheduled deletion: ${ + scheduledDeletionAt + ? scheduledDeletionAt.toISOString() + : 'None' + }`, ); await this.sessionsService.revokeAllSessions(userId); @@ -501,8 +574,7 @@ export class UsersService implements OnModuleInit { throw new BadRequestException('Account is not deactivated'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { token, hash } = generateReactivationToken(); + const { hash } = generateReactivationToken(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); await this.prisma.user.update({ @@ -513,10 +585,13 @@ export class UsersService implements OnModuleInit { }, }); - this.logger.log(`Reactivation token generated for user ${userId} (${user.email})`); + this.logger.log( + `Reactivation token generated for user ${userId} (${user.email})`, + ); return { - message: 'Reactivation token generated. Use the token to reactivate your account.', + message: + 'Reactivation token generated. Use the token to reactivate your account.', }; } @@ -547,10 +622,12 @@ export class UsersService implements OnModuleInit { reactivationTokenExpires: null, }, }); - throw new BadRequestException('Reactivation token has expired. Please request a new one.'); + + throw new BadRequestException( + 'Reactivation token has expired. Please request a new one.', + ); } - // Constant-time comparison of token hash const providedHash = createSha256(data.token); const storedHash = user.reactivationToken; const storedHashBuffer = Buffer.from(storedHash); @@ -589,11 +666,13 @@ export class UsersService implements OnModuleInit { action: 'REACTIVATE', entityType: 'USER', entityId: userId, - description: `Account reactivated`, + description: 'Account reactivated', }, }); - this.logger.log(`User ${userId} (${redactEmail(user.email)}) reactivated`); + this.logger.log( + `User ${userId} (${redactEmail(user.email)}) reactivated`, + ); return updatedUser; } @@ -620,33 +699,72 @@ export class UsersService implements OnModuleInit { const { q, email, name, page = 1, limit = 10 } = filters; const skip = (page - 1) * limit; - const where: Record = { + const where: Prisma.UserWhereInput = { isDeactivated: false, }; if (q) { where.OR = [ - { email: { contains: q, mode: 'insensitive' } }, - { firstName: { contains: q, mode: 'insensitive' } }, - { lastName: { contains: q, mode: 'insensitive' } }, + { + email: { + contains: q, + mode: 'insensitive', + }, + }, + { + firstName: { + contains: q, + mode: 'insensitive', + }, + }, + { + lastName: { + contains: q, + mode: 'insensitive', + }, + }, ]; } if (email) { - where.email = { contains: email, mode: 'insensitive' }; + where.email = { + contains: email, + mode: 'insensitive', + }; } if (name) { const nameParts = name.split(' '); + if (nameParts.length > 1) { where.AND = [ - { firstName: { contains: nameParts[0], mode: 'insensitive' } }, - { lastName: { contains: nameParts[nameParts.length - 1], mode: 'insensitive' } }, + { + firstName: { + contains: nameParts[0], + mode: 'insensitive', + }, + }, + { + lastName: { + contains: nameParts[nameParts.length - 1], + mode: 'insensitive', + }, + }, ]; } else { where.OR = [ - { firstName: { contains: name, mode: 'insensitive' } }, - { lastName: { contains: name, mode: 'insensitive' } }, + { + firstName: { + contains: name, + mode: 'insensitive', + }, + }, + { + lastName: { + contains: name, + mode: 'insensitive', + }, + }, ]; } } @@ -666,7 +784,9 @@ export class UsersService implements OnModuleInit { avatar: true, createdAt: true, }, - orderBy: { createdAt: 'desc' }, + orderBy: { + createdAt: 'desc', + }, }), this.prisma.user.count({ where }), ]); @@ -721,25 +841,28 @@ export class UsersService implements OnModuleInit { return { deletedCount: 0 }; } - const userIds = usersToDelete.map((user: { id: string }) => user.id); + const userIds = usersToDelete.map((user) => user.id); + const userEmails = usersToDelete.map((user) => user.email); - const result = await this.prisma.$transaction(async (tx: Prisma.TransactionClient) => { - await tx.loginAttempt.deleteMany({ - where: { - email: { - in: usersToDelete.map((user: { email: string }) => user.email), + const result = await this.prisma.$transaction( + async (tx: Prisma.TransactionClient) => { + await tx.loginAttempt.deleteMany({ + where: { + email: { + in: userEmails, + }, }, - }, - }); + }); - return tx.user.deleteMany({ - where: { - id: { - in: userIds, + return tx.user.deleteMany({ + where: { + id: { + in: userIds, + }, }, - }, - }); - }); + }); + }, + ); this.logger.log(`Deleted ${result.count} deactivated users`); @@ -748,12 +871,16 @@ export class UsersService implements OnModuleInit { async getReferralStats(userId: string) { const referralCount = await this.prisma.user.count({ - where: { referredById: userId }, + where: { + referredById: userId, + }, }); const user = await this.prisma.user.findUnique({ where: { id: userId }, - select: { referralCode: true }, + select: { + referralCode: true, + }, }); return { @@ -764,7 +891,9 @@ export class UsersService implements OnModuleInit { async getMyReferrals(userId: string) { return this.prisma.user.findMany({ - where: { referredById: userId }, + where: { + referredById: userId, + }, select: { id: true, firstName: true, @@ -777,8 +906,12 @@ export class UsersService implements OnModuleInit { async getLoginHistory(userId: string) { return this.prisma.loginHistory.findMany({ - where: { userId }, - orderBy: { timestamp: 'desc' }, + where: { + userId, + }, + orderBy: { + timestamp: 'desc', + }, select: { timestamp: true, ipAddress: true, @@ -790,7 +923,9 @@ export class UsersService implements OnModuleInit { async verify(id: string) { return this.prisma.user.update({ where: { id }, - data: { isVerified: true }, + data: { + isVerified: true, + }, select: { id: true, isVerified: true, @@ -801,11 +936,13 @@ export class UsersService implements OnModuleInit { async unverify(id: string) { return this.prisma.user.update({ where: { id }, - data: { isVerified: false }, + data: { + isVerified: false, + }, select: { id: true, isVerified: true, }, }); } -} +} \ No newline at end of file