1017 lines
27 KiB
TypeScript
1017 lines
27 KiB
TypeScript
import {
|
|
Injectable,
|
|
UnauthorizedException,
|
|
ConflictException,
|
|
BadRequestException,
|
|
NotFoundException,
|
|
Inject,
|
|
forwardRef,
|
|
} from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
import * as argon2 from 'argon2';
|
|
import slugify from 'slugify';
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import {
|
|
RegisterDto,
|
|
LoginDto,
|
|
SocialAuthDto,
|
|
ForgotPasswordDto,
|
|
ResetPasswordDto,
|
|
RefreshTokenDto,
|
|
VerifyEmailDto,
|
|
ChangePasswordDto,
|
|
} from './dto';
|
|
import { UserRole, AuthProvider, UserStatus } from '@prisma/client';
|
|
import { TwoFactorService } from './two-factor/two-factor.service';
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly jwtService: JwtService,
|
|
private readonly configService: ConfigService,
|
|
private readonly eventEmitter: EventEmitter2,
|
|
@Inject(forwardRef(() => TwoFactorService))
|
|
private readonly twoFactorService: TwoFactorService,
|
|
) {}
|
|
|
|
// ==========================================
|
|
// REGISTER
|
|
// ==========================================
|
|
async register(dto: RegisterDto) {
|
|
// Validate agentTypeId is required for AGENT role
|
|
if (dto.role === 'AGENT') {
|
|
if (!dto.agentTypeId) {
|
|
throw new BadRequestException('Agent type is required for agent registration');
|
|
}
|
|
|
|
// Verify agent type exists
|
|
const agentType = await this.prisma.agentType.findUnique({
|
|
where: { id: dto.agentTypeId },
|
|
});
|
|
|
|
if (!agentType) {
|
|
throw new BadRequestException('Invalid agent type selected');
|
|
}
|
|
}
|
|
|
|
// Check if user exists
|
|
const existingUser = await this.prisma.user.findUnique({
|
|
where: { email: dto.email.toLowerCase() },
|
|
});
|
|
|
|
if (existingUser) {
|
|
throw new ConflictException('Email already registered');
|
|
}
|
|
|
|
// Hash password
|
|
const hashedPassword = await argon2.hash(dto.password);
|
|
|
|
// Determine role
|
|
const role = dto.role === 'AGENT' ? UserRole.AGENT : UserRole.USER;
|
|
|
|
// Create user
|
|
const user = await this.prisma.user.create({
|
|
data: {
|
|
email: dto.email.toLowerCase(),
|
|
password: hashedPassword,
|
|
role,
|
|
status: UserStatus.ACTIVE,
|
|
authProvider: AuthProvider.LOCAL,
|
|
},
|
|
});
|
|
|
|
// Create profile based on role
|
|
if (role === UserRole.USER) {
|
|
await this.prisma.userProfile.create({
|
|
data: {
|
|
userId: user.id,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
},
|
|
});
|
|
} else if (role === UserRole.AGENT) {
|
|
const slug = await this.generateUniqueSlug(dto.firstName, dto.lastName);
|
|
await this.prisma.agentProfile.create({
|
|
data: {
|
|
userId: user.id,
|
|
slug,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
...(dto.agentTypeId && { agentTypeId: dto.agentTypeId }),
|
|
},
|
|
});
|
|
}
|
|
|
|
// Emit event for email verification (handled by email service)
|
|
this.eventEmitter.emit('user.registered', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
});
|
|
|
|
// Return message to verify email - no tokens until email is verified
|
|
return {
|
|
message: 'Registration successful! Please check your email to verify your account before logging in.',
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// LOGIN
|
|
// ==========================================
|
|
async login(dto: LoginDto, userAgent?: string, ipAddress?: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { email: dto.email.toLowerCase() },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Invalid email or password');
|
|
}
|
|
|
|
if (!user.password) {
|
|
throw new UnauthorizedException(
|
|
'This account uses social login. Please sign in with Google or Facebook.',
|
|
);
|
|
}
|
|
|
|
if (user.status !== UserStatus.ACTIVE) {
|
|
throw new UnauthorizedException('Your account has been deactivated. Please contact support.');
|
|
}
|
|
|
|
// Check if email is verified
|
|
if (!user.emailVerified) {
|
|
throw new UnauthorizedException(
|
|
'Please verify your email before logging in. Check your inbox for the verification link.',
|
|
);
|
|
}
|
|
|
|
// Verify password
|
|
const isPasswordValid = await argon2.verify(user.password, dto.password);
|
|
|
|
if (!isPasswordValid) {
|
|
throw new UnauthorizedException('Invalid email or password');
|
|
}
|
|
|
|
// Check if 2FA is enabled
|
|
if (user.twoFactorEnabled) {
|
|
// Generate temporary token for 2FA verification
|
|
const tempToken = await this.twoFactorService.generateTempToken(user.id);
|
|
|
|
return {
|
|
requiresTwoFactor: true,
|
|
tempToken,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Generate tokens
|
|
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
|
|
// Create session
|
|
await this.createSession(
|
|
user.id,
|
|
tokens.accessToken,
|
|
tokens.refreshToken,
|
|
userAgent,
|
|
ipAddress,
|
|
);
|
|
|
|
// Update last login
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { lastLoginAt: new Date() },
|
|
});
|
|
|
|
// Get profile based on role
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
return {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
avatar: profile?.avatar,
|
|
},
|
|
...tokens,
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// SOCIAL AUTH
|
|
// ==========================================
|
|
async socialAuth(dto: SocialAuthDto) {
|
|
const providerIdField =
|
|
dto.provider === 'google'
|
|
? 'googleId'
|
|
: dto.provider === 'facebook'
|
|
? 'facebookId'
|
|
: dto.provider === 'apple'
|
|
? 'appleId'
|
|
: 'twitterId';
|
|
|
|
// Check if user exists with this provider ID
|
|
let user = await this.prisma.user.findFirst({
|
|
where: { [providerIdField]: dto.providerId },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
// Check if user exists with this email
|
|
user = await this.prisma.user.findUnique({
|
|
where: { email: dto.email.toLowerCase() },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (user) {
|
|
// Link provider to existing account
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { [providerIdField]: dto.providerId },
|
|
});
|
|
} else if (dto.mode === 'login') {
|
|
// Login mode: user must exist
|
|
throw new BadRequestException(
|
|
'No account found with this email. Please sign up first.',
|
|
);
|
|
} else {
|
|
// Create new user (signup mode)
|
|
const role = dto.role === 'AGENT' ? UserRole.AGENT : UserRole.USER;
|
|
const authProvider =
|
|
dto.provider === 'google'
|
|
? AuthProvider.GOOGLE
|
|
: dto.provider === 'facebook'
|
|
? AuthProvider.FACEBOOK
|
|
: dto.provider === 'apple'
|
|
? AuthProvider.APPLE
|
|
: AuthProvider.TWITTER;
|
|
|
|
// Parse name
|
|
const nameParts = dto.name?.split(' ') || [];
|
|
const firstName = nameParts[0] || '';
|
|
const lastName = nameParts.slice(1).join(' ') || '';
|
|
|
|
user = await this.prisma.user.create({
|
|
data: {
|
|
email: dto.email.toLowerCase(),
|
|
role,
|
|
status: UserStatus.ACTIVE,
|
|
emailVerified: true,
|
|
emailVerifiedAt: new Date(),
|
|
authProvider,
|
|
[providerIdField]: dto.providerId,
|
|
},
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
// Create profile based on role
|
|
if (role === UserRole.USER) {
|
|
await this.prisma.userProfile.create({
|
|
data: {
|
|
userId: user.id,
|
|
firstName,
|
|
lastName,
|
|
avatar: dto.avatar,
|
|
},
|
|
});
|
|
} else if (role === UserRole.AGENT) {
|
|
const slug = await this.generateUniqueSlug(firstName, lastName);
|
|
await this.prisma.agentProfile.create({
|
|
data: {
|
|
userId: user.id,
|
|
slug,
|
|
firstName,
|
|
lastName,
|
|
avatar: dto.avatar,
|
|
agentTypeId: dto.agentTypeId || undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Refetch user with profile
|
|
user = await this.prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!user) {
|
|
throw new BadRequestException('Failed to create or find user');
|
|
}
|
|
|
|
if (user.status !== UserStatus.ACTIVE) {
|
|
throw new UnauthorizedException('Your account has been deactivated. Please contact support.');
|
|
}
|
|
|
|
// Generate tokens
|
|
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
|
|
// Create session
|
|
await this.createSession(user.id, tokens.accessToken, tokens.refreshToken);
|
|
|
|
// Update last login
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { lastLoginAt: new Date() },
|
|
});
|
|
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
return {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
avatar: profile?.avatar,
|
|
},
|
|
...tokens,
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// FORGOT PASSWORD
|
|
// ==========================================
|
|
async forgotPassword(dto: ForgotPasswordDto) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { email: dto.email.toLowerCase() },
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('No account found with this email address');
|
|
}
|
|
|
|
if (!user.password) {
|
|
throw new BadRequestException(
|
|
'This account uses social login (Google/Facebook). Please sign in with your social account.',
|
|
);
|
|
}
|
|
|
|
// Generate reset token (short-lived JWT)
|
|
const resetToken = this.jwtService.sign(
|
|
{ sub: user.id, email: user.email, type: 'password-reset' },
|
|
{
|
|
secret: this.configService.get<string>('jwt.secret'),
|
|
expiresIn: '1h',
|
|
},
|
|
);
|
|
|
|
// Emit event for email service
|
|
this.eventEmitter.emit('password.reset-requested', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
resetToken,
|
|
resetUrl: `${this.configService.get<string>('app.frontendUrl')}/reset-password?token=${resetToken}`,
|
|
});
|
|
|
|
return {
|
|
message: 'Password reset link has been sent to your email',
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// RESET PASSWORD
|
|
// ==========================================
|
|
async resetPassword(dto: ResetPasswordDto) {
|
|
try {
|
|
const payload = this.jwtService.verify(dto.token, {
|
|
secret: this.configService.get<string>('jwt.secret'),
|
|
});
|
|
|
|
if (payload.type !== 'password-reset') {
|
|
throw new BadRequestException('Invalid reset token');
|
|
}
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: payload.sub },
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
// Hash new password
|
|
const hashedPassword = await argon2.hash(dto.password);
|
|
|
|
// Update password
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { password: hashedPassword },
|
|
});
|
|
|
|
// Invalidate all existing sessions
|
|
await this.prisma.session.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
|
|
// Emit event
|
|
this.eventEmitter.emit('password.reset-completed', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
});
|
|
|
|
return {
|
|
message: 'Password reset successful. Please login with your new password.',
|
|
};
|
|
} catch (error) {
|
|
if (error.name === 'TokenExpiredError') {
|
|
throw new BadRequestException('Reset token has expired');
|
|
}
|
|
if (error.name === 'JsonWebTokenError') {
|
|
throw new BadRequestException('Invalid reset token');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// CHANGE PASSWORD
|
|
// ==========================================
|
|
async changePassword(userId: string, dto: ChangePasswordDto) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
if (!user.password) {
|
|
throw new BadRequestException(
|
|
'This account uses social login. Password cannot be changed.',
|
|
);
|
|
}
|
|
|
|
// Verify current password
|
|
const isCurrentPasswordValid = await argon2.verify(
|
|
user.password,
|
|
dto.currentPassword,
|
|
);
|
|
|
|
if (!isCurrentPasswordValid) {
|
|
throw new BadRequestException('Current password is incorrect');
|
|
}
|
|
|
|
// Check if new password is same as current
|
|
const isSamePassword = await argon2.verify(user.password, dto.newPassword);
|
|
if (isSamePassword) {
|
|
throw new BadRequestException(
|
|
'New password must be different from current password',
|
|
);
|
|
}
|
|
|
|
// Hash new password
|
|
const hashedPassword = await argon2.hash(dto.newPassword);
|
|
|
|
// Update password
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { password: hashedPassword },
|
|
});
|
|
|
|
// Emit event
|
|
this.eventEmitter.emit('password.changed', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
});
|
|
|
|
return {
|
|
message: 'Password changed successfully',
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// VERIFY EMAIL
|
|
// ==========================================
|
|
async verifyEmail(dto: VerifyEmailDto) {
|
|
try {
|
|
const payload = this.jwtService.verify(dto.token, {
|
|
secret: this.configService.get<string>('jwt.secret'),
|
|
});
|
|
|
|
if (payload.type !== 'email-verification') {
|
|
throw new BadRequestException('Invalid verification token');
|
|
}
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: payload.sub },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
if (user.emailVerified) {
|
|
return {
|
|
message: 'Email is already verified',
|
|
};
|
|
}
|
|
|
|
// Update user as verified
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
emailVerified: true,
|
|
emailVerifiedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
// Get profile for name
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
// Emit event for welcome email
|
|
this.eventEmitter.emit('user.email-verified', {
|
|
email: user.email,
|
|
name: profile?.firstName,
|
|
});
|
|
|
|
return {
|
|
message: 'Email verified successfully',
|
|
};
|
|
} catch (error) {
|
|
if (error.name === 'TokenExpiredError') {
|
|
throw new BadRequestException('Verification token has expired');
|
|
}
|
|
if (error.name === 'JsonWebTokenError') {
|
|
throw new BadRequestException('Invalid verification token');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// RESEND VERIFICATION EMAIL
|
|
// ==========================================
|
|
async resendVerificationEmail(userId: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
if (user.emailVerified) {
|
|
return {
|
|
message: 'Email is already verified',
|
|
};
|
|
}
|
|
|
|
// Get profile for name
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
// Emit event for email service to send verification
|
|
this.eventEmitter.emit('user.registered', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
});
|
|
|
|
return {
|
|
message: 'Verification email sent',
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// REFRESH TOKEN
|
|
// ==========================================
|
|
async refreshToken(dto: RefreshTokenDto) {
|
|
try {
|
|
const payload = this.jwtService.verify(dto.refreshToken, {
|
|
secret: this.configService.get<string>('jwt.secret'),
|
|
});
|
|
|
|
// Check if session exists
|
|
const session = await this.prisma.session.findFirst({
|
|
where: {
|
|
userId: payload.sub,
|
|
refreshToken: dto.refreshToken,
|
|
},
|
|
});
|
|
|
|
if (!session) {
|
|
throw new UnauthorizedException('Invalid refresh token');
|
|
}
|
|
|
|
// Get user
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: payload.sub },
|
|
});
|
|
|
|
if (!user || user.status !== UserStatus.ACTIVE) {
|
|
throw new UnauthorizedException('User not found or inactive');
|
|
}
|
|
|
|
// Generate new tokens
|
|
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
|
|
// Update session
|
|
await this.prisma.session.update({
|
|
where: { id: session.id },
|
|
data: {
|
|
token: tokens.accessToken,
|
|
refreshToken: tokens.refreshToken,
|
|
expiresAt: new Date(
|
|
Date.now() +
|
|
this.parseExpiration(
|
|
this.configService.get<string>('jwt.refreshExpiration') || '7d',
|
|
),
|
|
),
|
|
},
|
|
});
|
|
|
|
return tokens;
|
|
} catch (error) {
|
|
if (
|
|
error.name === 'TokenExpiredError' ||
|
|
error.name === 'JsonWebTokenError'
|
|
) {
|
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// LOGOUT
|
|
// ==========================================
|
|
async logout(userId: string, accessToken?: string) {
|
|
if (accessToken) {
|
|
// Delete specific session
|
|
await this.prisma.session.deleteMany({
|
|
where: {
|
|
userId,
|
|
token: accessToken,
|
|
},
|
|
});
|
|
} else {
|
|
// Delete all sessions for user
|
|
await this.prisma.session.deleteMany({
|
|
where: { userId },
|
|
});
|
|
}
|
|
|
|
return { message: 'Logged out successfully' };
|
|
}
|
|
|
|
// ==========================================
|
|
// TWO-FACTOR AUTHENTICATION - VERIFY LOGIN
|
|
// ==========================================
|
|
async verifyTwoFactorLogin(
|
|
tempToken: string,
|
|
token: string,
|
|
userAgent?: string,
|
|
ipAddress?: string,
|
|
) {
|
|
// Verify temp token and get user ID
|
|
const userId = await this.twoFactorService.verifyTempToken(tempToken);
|
|
|
|
// Verify the TOTP token
|
|
const isValid = await this.twoFactorService.verifyToken(userId, token);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Invalid verification code');
|
|
}
|
|
|
|
// Get user with profile
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user || user.status !== UserStatus.ACTIVE) {
|
|
throw new UnauthorizedException('User not found or inactive');
|
|
}
|
|
|
|
// Generate tokens
|
|
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
|
|
// Create session
|
|
await this.createSession(
|
|
user.id,
|
|
tokens.accessToken,
|
|
tokens.refreshToken,
|
|
userAgent,
|
|
ipAddress,
|
|
);
|
|
|
|
// Update last login
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { lastLoginAt: new Date() },
|
|
});
|
|
|
|
// Get profile based on role
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
return {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
avatar: profile?.avatar,
|
|
},
|
|
...tokens,
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// TWO-FACTOR AUTHENTICATION - VERIFY BACKUP CODE
|
|
// ==========================================
|
|
async verifyBackupCodeLogin(
|
|
tempToken: string,
|
|
backupCode: string,
|
|
userAgent?: string,
|
|
ipAddress?: string,
|
|
) {
|
|
// Verify temp token and get user ID
|
|
const userId = await this.twoFactorService.verifyTempToken(tempToken);
|
|
|
|
// Verify and consume the backup code
|
|
const isValid = await this.twoFactorService.verifyBackupCode(
|
|
userId,
|
|
backupCode,
|
|
);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Invalid backup code');
|
|
}
|
|
|
|
// Get user with profile
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user || user.status !== UserStatus.ACTIVE) {
|
|
throw new UnauthorizedException('User not found or inactive');
|
|
}
|
|
|
|
// Generate tokens
|
|
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
|
|
// Create session
|
|
await this.createSession(
|
|
user.id,
|
|
tokens.accessToken,
|
|
tokens.refreshToken,
|
|
userAgent,
|
|
ipAddress,
|
|
);
|
|
|
|
// Update last login
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { lastLoginAt: new Date() },
|
|
});
|
|
|
|
// Get profile based on role
|
|
const profile =
|
|
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
return {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
avatar: profile?.avatar,
|
|
},
|
|
...tokens,
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// GET CURRENT USER
|
|
// ==========================================
|
|
async getCurrentUser(userId: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
userProfile: true,
|
|
agentProfile: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
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,
|
|
firstName: profile?.firstName,
|
|
lastName: profile?.lastName,
|
|
avatar: profile?.avatar,
|
|
phone: profile?.phone,
|
|
city: profile?.city,
|
|
state: profile?.state,
|
|
country: profile?.country,
|
|
...(user.role === UserRole.AGENT && user.agentProfile
|
|
? {
|
|
slug: user.agentProfile.slug,
|
|
bio: user.agentProfile.bio,
|
|
headline: user.agentProfile.headline,
|
|
yearsOfExperience: user.agentProfile.yearsOfExperience,
|
|
licenseNumber: user.agentProfile.licenseNumber,
|
|
companyName: user.agentProfile.companyName,
|
|
isProfileComplete: user.agentProfile.isProfileComplete,
|
|
profileCompleteness: user.agentProfile.profileCompleteness,
|
|
}
|
|
: {}),
|
|
createdAt: user.createdAt,
|
|
lastLoginAt: user.lastLoginAt,
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// HELPER METHODS
|
|
// ==========================================
|
|
private async generateTokens(userId: string, email: string, role: UserRole) {
|
|
const payload = { sub: userId, email, role };
|
|
const secret = this.configService.get<string>('jwt.secret');
|
|
const accessExpiresIn = this.parseExpirationToSeconds(
|
|
this.configService.get<string>('jwt.accessExpiration') || '15m',
|
|
);
|
|
const refreshExpiresIn = this.parseExpirationToSeconds(
|
|
this.configService.get<string>('jwt.refreshExpiration') || '7d',
|
|
);
|
|
|
|
const accessToken = this.jwtService.sign(payload, {
|
|
secret,
|
|
expiresIn: accessExpiresIn,
|
|
});
|
|
|
|
const refreshToken = this.jwtService.sign(payload, {
|
|
secret,
|
|
expiresIn: refreshExpiresIn,
|
|
});
|
|
|
|
return { accessToken, refreshToken };
|
|
}
|
|
|
|
private parseExpirationToSeconds(expiration: string): number {
|
|
const match = expiration.match(/^(\d+)([smhd])$/);
|
|
if (!match) return 900; // Default 15 minutes
|
|
|
|
const value = parseInt(match[1], 10);
|
|
const unit = match[2];
|
|
|
|
switch (unit) {
|
|
case 's':
|
|
return value;
|
|
case 'm':
|
|
return value * 60;
|
|
case 'h':
|
|
return value * 60 * 60;
|
|
case 'd':
|
|
return value * 24 * 60 * 60;
|
|
default:
|
|
return 900;
|
|
}
|
|
}
|
|
|
|
private async createSession(
|
|
userId: string,
|
|
accessToken: string,
|
|
refreshToken: string,
|
|
userAgent?: string,
|
|
ipAddress?: string,
|
|
) {
|
|
const expiresAt = new Date(
|
|
Date.now() +
|
|
this.parseExpiration(
|
|
this.configService.get<string>('jwt.refreshExpiration') || '7d',
|
|
),
|
|
);
|
|
|
|
// Use upsert to handle potential duplicate tokens
|
|
await this.prisma.session.upsert({
|
|
where: { token: accessToken },
|
|
update: {
|
|
refreshToken,
|
|
userAgent,
|
|
ipAddress,
|
|
expiresAt,
|
|
},
|
|
create: {
|
|
userId,
|
|
token: accessToken,
|
|
refreshToken,
|
|
userAgent,
|
|
ipAddress,
|
|
expiresAt,
|
|
},
|
|
});
|
|
}
|
|
|
|
private parseExpiration(expiration: string): number {
|
|
const match = expiration.match(/^(\d+)([smhd])$/);
|
|
if (!match) return 7 * 24 * 60 * 60 * 1000; // Default 7 days
|
|
|
|
const value = parseInt(match[1], 10);
|
|
const unit = match[2];
|
|
|
|
switch (unit) {
|
|
case 's':
|
|
return value * 1000;
|
|
case 'm':
|
|
return value * 60 * 1000;
|
|
case 'h':
|
|
return value * 60 * 60 * 1000;
|
|
case 'd':
|
|
return value * 24 * 60 * 60 * 1000;
|
|
default:
|
|
return 7 * 24 * 60 * 60 * 1000;
|
|
}
|
|
}
|
|
|
|
private async generateUniqueSlug(
|
|
firstName?: string,
|
|
lastName?: string,
|
|
): Promise<string> {
|
|
const baseName = `${firstName || ''} ${lastName || ''}`.trim() || 'agent';
|
|
let slug = slugify(baseName, { lower: true, strict: true });
|
|
|
|
// Check if slug exists
|
|
let counter = 0;
|
|
let uniqueSlug = slug;
|
|
|
|
while (true) {
|
|
const existing = await this.prisma.agentProfile.findUnique({
|
|
where: { slug: uniqueSlug },
|
|
});
|
|
|
|
if (!existing) break;
|
|
|
|
counter++;
|
|
uniqueSlug = `${slug}-${counter}`;
|
|
}
|
|
|
|
return uniqueSlug;
|
|
}
|
|
}
|