Letzte Phase des RBAC-Umbaus: die transitionale Rollen-Infrastruktur, die seit Phase 1 parallel zum neuen Role/Permission-System mitlief, ist jetzt komplett weg. - Prisma-Migration: User.legacyRole-Spalte und LegacyRole-Enum gedroppt (nichts liest die Spalte mehr, seit Phase 2 jeder Controller auf @RequirePermission laeuft). - roles.guard.ts, roles.guard.spec.ts, roles.decorator.ts geloescht; RolesGuard aus den APP_GUARD-Providern entfernt. - AuthenticatedUser/JWT-Payload verlieren das role-Feld (permissions werden ohnehin frisch pro Request aus der DB aufgeloest, nicht aus dem Claim). - Users-API: CreateUserDto/UpdateUserDto akzeptieren jetzt roleId (eine echte Role.id) statt der alten role-Enum-Werte; UsersService validiert, dass die Rolle existiert und aktiv ist, bevor sie zugewiesen wird. - Alle 34 e2e-Fixtures und der Seed-Script folgen dem neuen Schema. Verifiziert: tsc, lint, build, unit (220/220), volle e2e-Suite 159/159 gruen - keine uebersprungenen Tests. Co-Authored-By: Claude Sonnet 5 <[email protected]>
187 lines
4.9 KiB
TypeScript
187 lines
4.9 KiB
TypeScript
import {
|
|
ConflictException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { AuditLogService } from '../audit/audit-log.service';
|
|
import { Prisma } from '../../generated/prisma';
|
|
import { CreateUserDto } from './dto/create-user.dto';
|
|
import { UpdateUserDto } from './dto/update-user.dto';
|
|
|
|
const BCRYPT_SALT_ROUNDS = 12;
|
|
|
|
const USER_WITH_ROLE = {
|
|
include: { role: true },
|
|
} satisfies { include: Prisma.UserInclude };
|
|
|
|
type UserWithRole = Prisma.UserGetPayload<typeof USER_WITH_ROLE>;
|
|
|
|
export interface UserSummary {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
roleId: string;
|
|
roleKey: string;
|
|
roleName: string;
|
|
isActive: boolean;
|
|
createdAt: Date;
|
|
deactivatedAt: Date | null;
|
|
}
|
|
|
|
@Injectable()
|
|
export class UsersService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly auditLog: AuditLogService,
|
|
) {}
|
|
|
|
private toSummary(user: UserWithRole): UserSummary {
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
roleId: user.roleId,
|
|
roleKey: user.role.key,
|
|
roleName: user.role.name,
|
|
isActive: user.isActive,
|
|
createdAt: user.createdAt,
|
|
deactivatedAt: user.deactivatedAt,
|
|
};
|
|
}
|
|
|
|
private async assertActiveRole(roleId: string): Promise<void> {
|
|
const role = await this.prisma.role.findUnique({ where: { id: roleId } });
|
|
if (!role) {
|
|
throw new NotFoundException('Role not found');
|
|
}
|
|
if (role.status !== 'ACTIVE') {
|
|
throw new ConflictException('Role is not active');
|
|
}
|
|
}
|
|
|
|
async findAll(): Promise<UserSummary[]> {
|
|
const users = await this.prisma.user.findMany({
|
|
...USER_WITH_ROLE,
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
return users.map((user) => this.toSummary(user));
|
|
}
|
|
|
|
async findById(id: string): Promise<UserWithRole> {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
...USER_WITH_ROLE,
|
|
});
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
return user;
|
|
}
|
|
|
|
async findSummaryById(id: string): Promise<UserSummary> {
|
|
return this.toSummary(await this.findById(id));
|
|
}
|
|
|
|
async findByEmail(email: string): Promise<UserWithRole | null> {
|
|
return this.prisma.user.findUnique({ where: { email }, ...USER_WITH_ROLE });
|
|
}
|
|
|
|
async create(
|
|
dto: CreateUserDto,
|
|
actingUserId?: string,
|
|
): Promise<UserSummary> {
|
|
const existing = await this.findByEmail(dto.email);
|
|
if (existing) {
|
|
throw new ConflictException('Email already in use');
|
|
}
|
|
await this.assertActiveRole(dto.roleId);
|
|
const passwordHash = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
const user = await this.prisma.user.create({
|
|
data: {
|
|
email: dto.email,
|
|
passwordHash,
|
|
name: dto.name,
|
|
roleId: dto.roleId,
|
|
},
|
|
...USER_WITH_ROLE,
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'User',
|
|
entityId: user.id,
|
|
action: 'CREATE',
|
|
userId: actingUserId,
|
|
afterData: this.toSummary(user),
|
|
});
|
|
return this.toSummary(user);
|
|
}
|
|
|
|
async update(
|
|
id: string,
|
|
dto: UpdateUserDto,
|
|
actingUserId?: string,
|
|
): Promise<UserSummary> {
|
|
const before = await this.findById(id);
|
|
if (dto.roleId) {
|
|
await this.assertActiveRole(dto.roleId);
|
|
}
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: {
|
|
name: dto.name,
|
|
roleId: dto.roleId,
|
|
},
|
|
...USER_WITH_ROLE,
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'User',
|
|
entityId: id,
|
|
action: 'UPDATE',
|
|
userId: actingUserId,
|
|
beforeData: this.toSummary(before),
|
|
afterData: this.toSummary(user),
|
|
});
|
|
return this.toSummary(user);
|
|
}
|
|
|
|
async deactivate(id: string, actingUserId?: string): Promise<UserSummary> {
|
|
const before = await this.findById(id);
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { isActive: false, deactivatedAt: new Date() },
|
|
...USER_WITH_ROLE,
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'User',
|
|
entityId: id,
|
|
action: 'DEACTIVATE',
|
|
userId: actingUserId,
|
|
beforeData: this.toSummary(before),
|
|
afterData: this.toSummary(user),
|
|
});
|
|
return this.toSummary(user);
|
|
}
|
|
|
|
async activate(id: string, actingUserId?: string): Promise<UserSummary> {
|
|
const before = await this.findById(id);
|
|
if (before.isActive) {
|
|
throw new ConflictException('User is already active');
|
|
}
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { isActive: true, deactivatedAt: null },
|
|
...USER_WITH_ROLE,
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'User',
|
|
entityId: id,
|
|
action: 'UPDATE',
|
|
userId: actingUserId,
|
|
beforeData: this.toSummary(before),
|
|
afterData: this.toSummary(user),
|
|
});
|
|
return this.toSummary(user);
|
|
}
|
|
}
|