import { Injectable, UnauthorizedException, ConflictException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcrypt'; import { PrismaService } from '../prisma/prisma.service'; import { JwtPayload, TokenResponse, getPermissionsForRoles } from '@fiberops/shared'; import { LoginDto } from './dto/login.dto'; import { RegisterTenantDto } from './dto/register-tenant.dto'; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwt: JwtService, private readonly config: ConfigService, ) {} async login(dto: LoginDto): Promise { const user = await this.prisma.user.findFirst({ where: { email: dto.email, isActive: true }, include: { roles: true, tenant: true }, }); if (!user) { throw new UnauthorizedException('Invalid credentials'); } // Super admin may not have a tenant; regular users must have active tenant const isSuperAdmin = user.roles.some((r) => r.role === 'super_admin'); if (!isSuperAdmin && (!user.tenant || !user.tenant.isActive)) { throw new UnauthorizedException('Invalid credentials'); } const passwordValid = await bcrypt.compare(dto.password, user.password); if (!passwordValid) { throw new UnauthorizedException('Invalid credentials'); } const roles = user.roles.map((r) => r.role); const tokens = await this.generateTokens({ sub: user.id, tenantId: user.tenantId, roles, permissions: getPermissionsForRoles(roles), }); return { ...tokens, mustChangePassword: user.mustChangePassword, }; } async registerTenant(dto: RegisterTenantDto) { const existing = await this.prisma.tenant.findUnique({ where: { slug: dto.slug }, }); if (existing) { throw new ConflictException('Tenant slug already taken'); } const hashedPassword = await bcrypt.hash(dto.adminPassword, 12); const tenant = await this.prisma.tenant.create({ data: { name: dto.tenantName, slug: dto.slug, settings: { companyName: dto.tenantName, currency: 'PHP', timezone: 'Asia/Manila', }, users: { create: { email: dto.adminEmail, password: hashedPassword, firstName: dto.adminFirstName, lastName: dto.adminLastName, roles: { create: { role: 'tenant_admin' }, }, }, }, }, include: { users: { include: { roles: true }, }, }, }); const admin = tenant.users[0]!; const roles = admin.roles.map((r) => r.role); const tokens = await this.generateTokens({ sub: admin.id, tenantId: tenant.id, roles, permissions: getPermissionsForRoles(roles), }); return { tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug, }, user: { id: admin.id, email: admin.email, firstName: admin.firstName, lastName: admin.lastName, roles, }, ...tokens, }; } async refreshTokens(refreshToken: string): Promise { const stored = await this.prisma.refreshToken.findUnique({ where: { token: refreshToken }, }); if (!stored || stored.expiresAt < new Date()) { if (stored) { await this.prisma.refreshToken.delete({ where: { id: stored.id } }); } throw new UnauthorizedException('Invalid or expired refresh token'); } // Delete the used refresh token (rotation) await this.prisma.refreshToken.delete({ where: { id: stored.id } }); const user = await this.prisma.user.findUnique({ where: { id: stored.userId }, include: { roles: true, tenant: true }, }); if (!user || !user.isActive) { throw new UnauthorizedException('Account is inactive'); } const isSuperAdmin = user.roles.some((r) => r.role === 'super_admin'); if (!isSuperAdmin && (!user.tenant || !user.tenant.isActive)) { throw new UnauthorizedException('Account is inactive'); } const roles = user.roles.map((r) => r.role); return this.generateTokens({ sub: user.id, tenantId: user.tenantId, roles, permissions: getPermissionsForRoles(roles), }); } async logout(userId: string): Promise { await this.prisma.refreshToken.deleteMany({ where: { userId }, }); } async getProfile(userId: string) { const user = await this.prisma.user.findUnique({ where: { id: userId }, include: { roles: true, tenant: { select: { id: true, name: true, slug: true, settings: true } }, tenantRoles: { include: { tenantRole: { include: { permissions: true }, }, }, }, }, }); if (!user) { throw new UnauthorizedException('User not found'); } const roles = user.roles.map((r) => r.role); const isSuperAdmin = roles.includes('super_admin'); // Resolve permission matrix from tenant roles const accessMap: Record> = {}; if (!isSuperAdmin) { for (const utr of user.tenantRoles) { if (!utr.tenantRole.isActive || utr.tenantRole.deletedAt) continue; for (const p of utr.tenantRole.permissions) { if (!accessMap[p.module]) { accessMap[p.module] = { canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false }; } if (p.canView) accessMap[p.module].canView = true; if (p.canCreate) accessMap[p.module].canCreate = true; if (p.canUpdate) accessMap[p.module].canUpdate = true; if (p.canArchive) accessMap[p.module].canArchive = true; if (p.canApprove) accessMap[p.module].canApprove = true; if (p.canExport) accessMap[p.module].canExport = true; } } } return { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, mustChangePassword: user.mustChangePassword, roles, permissions: getPermissionsForRoles(roles), accessMap: isSuperAdmin ? 'all' : accessMap, tenantRoles: user.tenantRoles.map((tr) => ({ id: tr.tenantRole.id, name: tr.tenantRole.name, slug: tr.tenantRole.slug, })), tenant: user.tenant, }; } async changePassword(userId: string, currentPassword: string, newPassword: string) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedException('User not found'); const valid = await bcrypt.compare(currentPassword, user.password); if (!valid) throw new UnauthorizedException('Current password is incorrect'); const hashed = await bcrypt.hash(newPassword, 12); await this.prisma.user.update({ where: { id: userId }, data: { password: hashed, mustChangePassword: false }, }); return { success: true }; } private async generateTokens(payload: JwtPayload): Promise { const jti = crypto.randomUUID(); const accessToken = this.jwt.sign({ ...payload, jti }); const refreshJti = crypto.randomUUID(); const refreshToken = this.jwt.sign({ ...payload, jti: refreshJti }, { secret: this.config.get('JWT_REFRESH_SECRET'), expiresIn: this.config.get('JWT_REFRESH_EXPIRES_IN', '7d') as any, }); const expiresIn = this.config.get('JWT_REFRESH_EXPIRES_IN', '7d'); const expiresAt = new Date(); const days = parseInt(expiresIn) || 7; expiresAt.setDate(expiresAt.getDate() + days); await this.prisma.refreshToken.create({ data: { token: refreshToken, userId: payload.sub, expiresAt, }, }); return { accessToken, refreshToken }; } }