268 lines
7.9 KiB
TypeScript
268 lines
7.9 KiB
TypeScript
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<TokenResponse> {
|
|
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<TokenResponse> {
|
|
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<void> {
|
|
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<string, Record<string, boolean>> = {};
|
|
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<TokenResponse> {
|
|
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<string>('JWT_REFRESH_SECRET'),
|
|
expiresIn: this.config.get<string>('JWT_REFRESH_EXPIRES_IN', '7d') as any,
|
|
});
|
|
|
|
const expiresIn = this.config.get<string>('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 };
|
|
}
|
|
}
|