Files
prosapp-migration/backend/src/auth/auth.service.ts
T
Lizandro GuarnizoandClaude Sonnet 4.6 4d98c7cb70 feat(auth): reject login and invalidate token for blocked users
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>
2026-07-02 14:56:28 -05:00

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,
},
};
}
}