- 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
187 lines
5.5 KiB
TypeScript
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);
|
|
}
|
|
}
|