Files
backend/src/users/users.service.ts

621 lines
17 KiB
TypeScript
Raw Normal View History

import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { UserStatus, UserRole, Prisma, VerificationStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import * as argon2 from 'argon2';
type UserWithProfiles = Prisma.UserGetPayload<{
include: { userProfile: true; agentProfile: { include: { agentType: true } } };
}>;
@Injectable()
export class UsersService {
constructor(
private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2,
) {}
// =============================================
// USER PROFILE METHODS (for regular users)
// =============================================
async getMyUserProfile(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { userProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.USER) {
throw new BadRequestException('This endpoint is for regular users only');
}
// Create user profile if it doesn't exist
let profile = user.userProfile;
if (!profile) {
profile = await this.prisma.userProfile.create({
data: {
userId: user.id,
},
});
}
return {
id: profile.id,
userId: user.id,
email: user.email,
firstName: profile.firstName,
lastName: profile.lastName,
phone: profile.phone,
avatar: profile.avatar,
city: profile.city,
state: profile.state,
country: profile.country,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
};
}
async updateMyUserProfile(
userId: string,
data: {
firstName?: string;
lastName?: string;
phone?: string;
avatar?: string | null;
city?: string;
state?: string;
country?: string;
},
) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { userProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.USER) {
throw new BadRequestException('This endpoint is for regular users only');
}
// Create or update user profile
const profile = await this.prisma.userProfile.upsert({
where: { userId },
create: {
userId,
...data,
},
update: data,
});
return {
id: profile.id,
userId: user.id,
email: user.email,
firstName: profile.firstName,
lastName: profile.lastName,
phone: profile.phone,
avatar: profile.avatar,
city: profile.city,
state: profile.state,
country: profile.country,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
};
}
// =============================================
// ADMIN METHODS
// =============================================
async findAll(page = 1, limit = 10, role?: UserRole) {
const skip = (page - 1) * limit;
// Build where clause for optional role filter
const where: Prisma.UserWhereInput = role ? { role } : {};
const [users, total] = await Promise.all([
this.prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
userProfile: true,
agentProfile: {
include: {
agentType: true,
},
},
},
}) as Promise<UserWithProfiles[]>,
this.prisma.user.count({ where }),
]);
return {
users: users.map((user) => {
// Get the appropriate profile based on role
const profile = user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
return {
id: user.id,
email: user.email,
role: user.role,
status: user.status,
emailVerified: user.emailVerified,
authProvider: user.authProvider,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
profile: profile
? {
firstName: profile.firstName,
lastName: profile.lastName,
avatar: profile.avatar,
phone: profile.phone,
city: profile.city,
state: profile.state,
country: profile.country,
}
: null,
};
}),
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: string) {
const user = (await this.prisma.user.findUnique({
where: { id },
include: {
userProfile: true,
agentProfile: {
include: {
agentType: true,
},
},
},
})) as UserWithProfiles | null;
if (!user) {
return null;
}
// Base user data
const baseData = {
id: user.id,
email: user.email,
role: user.role,
status: user.status,
emailVerified: user.emailVerified,
emailVerifiedAt: user.emailVerifiedAt,
authProvider: user.authProvider,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
};
// For agents, return full agent profile
if (user.role === UserRole.AGENT && user.agentProfile) {
return {
...baseData,
profile: {
firstName: user.agentProfile.firstName,
lastName: user.agentProfile.lastName,
avatar: user.agentProfile.avatar,
phone: user.agentProfile.phone,
city: user.agentProfile.city,
state: user.agentProfile.state,
country: user.agentProfile.country,
},
agentProfile: {
id: user.agentProfile.id,
slug: user.agentProfile.slug,
bio: user.agentProfile.bio,
headline: user.agentProfile.headline,
companyName: user.agentProfile.companyName,
licenseNumber: user.agentProfile.licenseNumber,
yearsOfExperience: user.agentProfile.yearsOfExperience,
isProfileComplete: user.agentProfile.isProfileComplete,
profileCompleteness: user.agentProfile.profileCompleteness,
agentTypeId: user.agentProfile.agentTypeId,
agentType: user.agentProfile.agentType,
// Verification fields
isVerified: user.agentProfile.isVerified,
verificationStatus: user.agentProfile.verificationStatus,
verificationNote: user.agentProfile.verificationNote,
verifiedAt: user.agentProfile.verifiedAt,
verifiedBy: user.agentProfile.verifiedBy,
},
};
}
// For regular users
return {
...baseData,
profile: user.userProfile
? {
firstName: user.userProfile.firstName,
lastName: user.userProfile.lastName,
avatar: user.userProfile.avatar,
phone: user.userProfile.phone,
city: user.userProfile.city,
state: user.userProfile.state,
country: user.userProfile.country,
}
: null,
};
}
async updateStatus(id: string, status: UserStatus) {
return this.prisma.user.update({
where: { id },
data: { status },
});
}
async updateAgentType(userId: string, agentTypeId: string) {
// Get user to verify they are an agent
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { agentProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.AGENT) {
throw new BadRequestException('User is not an agent');
}
if (!user.agentProfile) {
throw new NotFoundException('Agent profile not found');
}
// Verify agent type exists
const agentType = await this.prisma.agentType.findUnique({
where: { id: agentTypeId },
});
if (!agentType) {
throw new BadRequestException('Invalid agent type');
}
// Update agent profile with new agent type
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: user.agentProfile.id },
data: { agentTypeId },
include: { agentType: true },
});
return {
id: user.id,
agentTypeId: updatedProfile.agentTypeId,
agentType: updatedProfile.agentType,
message: 'Agent type updated successfully',
};
}
async getVerificationDocuments(userId: string) {
// Get user to verify they are an agent
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { agentProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.AGENT) {
throw new BadRequestException('User is not an agent');
}
if (!user.agentProfile) {
throw new NotFoundException('Agent profile not found');
}
// Find the upload-documents section field
const field = await this.prisma.profileField.findFirst({
where: {
section: { slug: 'upload-documents' },
slug: 'documents',
},
});
if (!field) return [];
const fieldValue = await this.prisma.agentProfileFieldValue.findUnique({
where: {
agentProfileId_fieldId: {
agentProfileId: user.agentProfile.id,
fieldId: field.id,
},
},
});
return fieldValue?.jsonValue || [];
}
async updateVerification(
userId: string,
data: {
status: VerificationStatus;
note?: string;
},
adminId: string,
) {
// Get user to verify they are an agent
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { agentProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.AGENT) {
throw new BadRequestException('User is not an agent');
}
if (!user.agentProfile) {
throw new NotFoundException('Agent profile not found');
}
const updateData: Prisma.AgentProfileUpdateInput = {
verificationStatus: data.status,
verificationNote: data.note || null,
};
if (data.status === VerificationStatus.APPROVED) {
updateData.isVerified = true;
updateData.verifiedAt = new Date();
updateData.verifiedBy = adminId;
} else if (data.status === VerificationStatus.REJECTED) {
updateData.isVerified = false;
updateData.verifiedAt = null;
updateData.verifiedBy = null;
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: user.agentProfile.id },
data: updateData,
include: {
agentType: true,
},
});
// Save verification history
await this.prisma.verificationHistory.create({
data: {
agentProfileId: user.agentProfile.id,
status: data.status,
note: data.note || null,
adminId,
},
});
// Emit notification to the agent
this.eventEmitter.emit('notification.verification', {
recipientUserId: userId,
status: data.status,
note: data.note,
agentName: `${updatedProfile.firstName || ''} ${updatedProfile.lastName || ''}`.trim(),
});
return {
id: user.id,
agentProfileId: updatedProfile.id,
isVerified: updatedProfile.isVerified,
verificationStatus: updatedProfile.verificationStatus,
verificationNote: updatedProfile.verificationNote,
verifiedAt: updatedProfile.verifiedAt,
verifiedBy: updatedProfile.verifiedBy,
message: `Verification ${data.status.toLowerCase()} successfully`,
};
}
// Get verification history for an agent
async getVerificationHistory(agentProfileId: string) {
return this.prisma.verificationHistory.findMany({
where: { agentProfileId },
include: {
admin: { select: { id: true, email: true } },
},
orderBy: { createdAt: 'desc' },
});
}
// =============================================
// NOTIFICATION PREFERENCES
// =============================================
async getNotificationPreferences(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { notificationPreferences: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
// Return preferences or default empty object
return user.notificationPreferences || { email: {}, push: {} };
}
async updateNotificationPreferences(
userId: string,
preferences: { email?: Record<string, boolean>; push?: Record<string, boolean> },
) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new NotFoundException('User not found');
}
const updatedUser = await this.prisma.user.update({
where: { id: userId },
data: {
notificationPreferences: preferences,
},
select: { notificationPreferences: true },
});
return updatedUser.notificationPreferences;
}
// =============================================
// PRIVACY PREFERENCES
// =============================================
async getPrivacyPreferences(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { privacyPreferences: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
// Return preferences or default empty object
return user.privacyPreferences || { privacySettings: {}, dataSettings: {} };
}
async updatePrivacyPreferences(
userId: string,
preferences: {
privacySettings?: Record<string, string>;
dataSettings?: Record<string, boolean>;
},
) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new NotFoundException('User not found');
}
const updatedUser = await this.prisma.user.update({
where: { id: userId },
data: {
privacyPreferences: preferences,
},
select: { privacyPreferences: true },
});
return updatedUser.privacyPreferences;
}
// =============================================
// DELETE ACCOUNT
// =============================================
async deleteAccount(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, role: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.USER) {
throw new BadRequestException('This endpoint is for regular users only');
}
// Delete in a transaction to ensure data consistency
await this.prisma.$transaction(async (tx) => {
// Delete user profile if it exists
await tx.userProfile.deleteMany({
where: { userId },
});
// Delete the user account
await tx.user.delete({
where: { id: userId },
});
});
return { message: 'Account deleted successfully' };
}
// =============================================
// ADMIN MANAGEMENT (SUPER_ADMIN only)
// =============================================
async getAdminUsers() {
return this.prisma.user.findMany({
where: { role: { in: ['ADMIN', 'SUPER_ADMIN'] } },
select: {
id: true,
email: true,
role: true,
status: true,
createdAt: true,
lastLoginAt: true,
},
orderBy: { createdAt: 'desc' },
});
}
async createAdminUser(email: string, password: string) {
const existing = await this.prisma.user.findUnique({ where: { email } });
if (existing) {
throw new BadRequestException('A user with this email already exists');
}
const hashedPassword = await argon2.hash(password);
return this.prisma.user.create({
data: {
email,
password: hashedPassword,
role: 'ADMIN',
status: 'ACTIVE',
emailVerified: true,
emailVerifiedAt: new Date(),
authProvider: 'LOCAL',
},
select: {
id: true,
email: true,
role: true,
status: true,
createdAt: true,
},
});
}
async deleteAdminUser(id: string, requesterId: string) {
if (id === requesterId) {
throw new BadRequestException('You cannot delete your own account');
}
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException('User not found');
if (user.role === 'SUPER_ADMIN') {
throw new ForbiddenException('Cannot delete a super admin');
}
if (user.role !== 'ADMIN') {
throw new BadRequestException('This user is not an admin');
}
await this.prisma.user.delete({ where: { id } });
return null;
}
}