Files
fiberops-api/src/user/user.service.ts
kevin-asprec 59ee1fbe33 feat: add notification triggers, comment roles, mention list, and static file serving
- Notify users on ticket assignment and remittance confirm/reject
- Add collector role to comment endpoints
- Add /users/mention-list endpoint for @mention support
- Serve uploaded files via express.static
2026-05-06 14:30:08 +08:00

187 lines
5.5 KiB
TypeScript

import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { JournalService } from '../accounting/journal.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService {
constructor(
private readonly prisma: PrismaService,
private readonly journal: JournalService,
) {}
private formatUser(user: any) {
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
isActive: user.isActive,
roles: user.roles?.map((r: any) => r.role) ?? [],
tenantRoles: user.tenantRoles?.map((tr: any) => ({
id: tr.tenantRole.id,
name: tr.tenantRole.name,
slug: tr.tenantRole.slug,
})) ?? [],
createdAt: user.createdAt,
};
}
private readonly userInclude = {
roles: { select: { role: true } },
tenantRoles: {
include: {
tenantRole: { select: { id: true, name: true, slug: true } },
},
},
};
async findAll(tenantId: string) {
const db = this.prisma.forTenant(tenantId);
const users = await db.user.findMany({
include: this.userInclude,
orderBy: { createdAt: 'desc' },
});
return users.map((u) => this.formatUser(u));
}
async findForMention(tenantId: string) {
const db = this.prisma.forTenant(tenantId);
const users = await db.user.findMany({
where: { isActive: true },
select: { id: true, firstName: true, lastName: true },
orderBy: { firstName: 'asc' },
});
return users;
}
async findById(tenantId: string, userId: string) {
const db = this.prisma.forTenant(tenantId);
const user = await db.user.findFirst({
where: { id: userId },
include: this.userInclude,
});
if (!user) throw new NotFoundException('User not found');
return this.formatUser(user);
}
async create(tenantId: string, dto: CreateUserDto) {
const existing = await this.prisma.user.findFirst({
where: { tenantId, email: dto.email },
});
if (existing) throw new ConflictException('Email already exists in this tenant');
const hashedPassword = await bcrypt.hash(dto.password, 12);
const user = await this.prisma.user.create({
data: {
tenantId,
email: dto.email,
password: hashedPassword,
firstName: dto.firstName,
lastName: dto.lastName,
},
});
// Assign tenant roles if provided
if (dto.tenantRoleIds?.length) {
await this.prisma.userTenantRole.createMany({
data: dto.tenantRoleIds.map((roleId) => ({
userId: user.id,
tenantRoleId: roleId,
})),
});
}
// Legacy: also create UserRole for backward compat (uses first tenant role slug)
if (dto.roles?.length) {
await this.prisma.userRole.createMany({
data: dto.roles.map((role) => ({ userId: user.id, role })),
});
}
// Auto-create custodial CoA accounts
this.journal.createCustodialAccounts(
tenantId, user.id, `${user.firstName} ${user.lastName}`,
).catch(() => {});
const created = await this.prisma.user.findUnique({
where: { id: user.id },
include: this.userInclude,
});
return this.formatUser(created);
}
async update(tenantId: string, userId: string, dto: UpdateUserDto) {
const db = this.prisma.forTenant(tenantId);
const existing = await db.user.findFirst({ where: { id: userId } });
if (!existing) throw new NotFoundException('User not found');
// Update basic fields
const updateData: any = {};
if (dto.firstName) updateData.firstName = dto.firstName;
if (dto.lastName) updateData.lastName = dto.lastName;
if (Object.keys(updateData).length > 0) {
await db.user.update({
where: { id: userId },
data: updateData,
});
}
// Update tenant roles if provided
if (dto.tenantRoleIds !== undefined) {
// Verify all role IDs belong to this tenant
const roles = await this.prisma.tenantRole.findMany({
where: { id: { in: dto.tenantRoleIds }, tenantId, deletedAt: null },
});
if (roles.length !== dto.tenantRoleIds.length) {
throw new NotFoundException('One or more role IDs are invalid');
}
await this.prisma.userTenantRole.deleteMany({ where: { userId } });
if (dto.tenantRoleIds.length > 0) {
await this.prisma.userTenantRole.createMany({
data: dto.tenantRoleIds.map((roleId) => ({
userId,
tenantRoleId: roleId,
})),
});
}
}
// Legacy role update (backward compat)
if (dto.roles) {
await this.prisma.userRole.deleteMany({ where: { userId } });
await this.prisma.userRole.createMany({
data: dto.roles.map((role) => ({ userId, role })),
});
}
const updated = await this.prisma.user.findUnique({
where: { id: userId },
include: this.userInclude,
});
return this.formatUser(updated);
}
async toggleActive(tenantId: string, userId: string) {
const db = this.prisma.forTenant(tenantId);
const existing = await db.user.findFirst({ where: { id: userId } });
if (!existing) throw new NotFoundException('User not found');
const user = await this.prisma.user.update({
where: { id: userId },
data: { isActive: !existing.isActive },
include: this.userInclude,
});
return this.formatUser(user);
}
}