Checks is_active on email login, phone OTP login, and /auth/me so existing tokens also stop working immediately after a user is blocked. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { SmsService } from '../sms/sms.service';
|
|
import { MailService } from '../mail/mail.service';
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private jwt: JwtService,
|
|
private sms: SmsService,
|
|
private mail: MailService,
|
|
) {}
|
|
|
|
async register(email: string, password: string, name: string) {
|
|
const existing = await this.prisma.users.findUnique({ where: { email } });
|
|
if (existing) throw new ConflictException('Email ya registrado');
|
|
|
|
const password_hash = await bcrypt.hash(password, 10);
|
|
const user = await this.prisma.users.create({
|
|
data: { email, password_hash, name },
|
|
});
|
|
|
|
this.mail.sendWelcome(name, email).catch(() => {});
|
|
return this.generateToken(user);
|
|
}
|
|
|
|
async login(email: string, password: string) {
|
|
const user = await this.prisma.users.findUnique({ where: { email } });
|
|
if (!user || !user.password_hash) throw new UnauthorizedException('Credenciales inválidas');
|
|
|
|
const valid = await bcrypt.compare(password, user.password_hash);
|
|
if (!valid) throw new UnauthorizedException('Credenciales inválidas');
|
|
|
|
if (user.is_active === false) throw new UnauthorizedException('Tu cuenta ha sido bloqueada');
|
|
|
|
return this.generateToken(user);
|
|
}
|
|
|
|
async sendPhoneOtp(phone: string): Promise<void> {
|
|
await this.sms.sendOtp(phone);
|
|
}
|
|
|
|
async loginOrCreateByPhone(phone: string, code: string, name?: string) {
|
|
const valid = this.sms.verifyOtp(phone, code);
|
|
if (!valid) throw new BadRequestException('Código OTP inválido o expirado');
|
|
|
|
let user = await this.prisma.users.findUnique({ where: { phone } });
|
|
if (!user) {
|
|
user = await this.prisma.users.create({
|
|
data: { phone, name: name || phone, is_phone_verified: true },
|
|
});
|
|
} else {
|
|
if (user.is_active === false) throw new UnauthorizedException('Tu cuenta ha sido bloqueada');
|
|
user = await this.prisma.users.update({
|
|
where: { id: user.id },
|
|
data: { is_phone_verified: true },
|
|
});
|
|
}
|
|
return this.generateToken(user);
|
|
}
|
|
|
|
async verifyOtpAndLinkPhone(userId: string, phone: string, code: string) {
|
|
const valid = this.sms.verifyOtp(phone, code);
|
|
if (!valid) throw new BadRequestException('Código OTP inválido o expirado');
|
|
|
|
const existing = await this.prisma.users.findUnique({ where: { phone } });
|
|
if (existing && existing.id !== userId) {
|
|
throw new ConflictException('Teléfono ya registrado por otro usuario');
|
|
}
|
|
return this.prisma.users.update({
|
|
where: { id: userId },
|
|
data: { phone, is_phone_verified: true },
|
|
});
|
|
}
|
|
|
|
async linkEmail(userId: string, email: string, password: string) {
|
|
const existing = await this.prisma.users.findUnique({ where: { email } });
|
|
if (existing) throw new ConflictException('Email ya registrado');
|
|
|
|
const password_hash = await bcrypt.hash(password, 10);
|
|
return this.prisma.users.update({
|
|
where: { id: userId },
|
|
data: { email, password_hash },
|
|
});
|
|
}
|
|
|
|
async changePassword(userId: string, currentPassword: string, newPassword: string) {
|
|
const user = await this.prisma.users.findUnique({ where: { id: userId } });
|
|
if (!user || !user.password_hash) throw new BadRequestException('El usuario no tiene contraseña configurada');
|
|
|
|
const valid = await bcrypt.compare(currentPassword, user.password_hash);
|
|
if (!valid) throw new UnauthorizedException('Contraseña actual incorrecta');
|
|
|
|
const password_hash = await bcrypt.hash(newPassword, 10);
|
|
await this.prisma.users.update({ where: { id: userId }, data: { password_hash } });
|
|
return { ok: true };
|
|
}
|
|
|
|
async me(userId: string) {
|
|
const user = await this.prisma.users.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
professionals: {
|
|
include: { schedules: true, payment_methods: true, specializations: true },
|
|
},
|
|
reputations: true,
|
|
},
|
|
});
|
|
if (!user) throw new UnauthorizedException('Usuario no encontrado');
|
|
if (user.is_active === false) throw new UnauthorizedException('Tu cuenta ha sido bloqueada');
|
|
return { ...user, professional_state: user.pro_state };
|
|
}
|
|
|
|
async getProfessionalId(userId: string): Promise<string | null> {
|
|
const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } });
|
|
return prof?.id || null;
|
|
}
|
|
|
|
private generateToken(user: any) {
|
|
const payload = { sub: user.id, email: user.email, phone: user.phone };
|
|
return {
|
|
access_token: this.jwt.sign(payload),
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
phone: user.phone,
|
|
name: user.name,
|
|
is_phone_verified: user.is_phone_verified ?? false,
|
|
},
|
|
};
|
|
}
|
|
}
|