initial: standalone repo from monorepo split
This commit is contained in:
267
src/auth/auth.service.ts
Normal file
267
src/auth/auth.service.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user