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