add moodules

This commit is contained in:
2026-04-17 23:42:24 +01:00
parent 532458ecfa
commit a7fbb2c466
54 changed files with 3074 additions and 74 deletions
+13
View File
@@ -0,0 +1,13 @@
import { IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateMeDto {
@IsOptional()
@IsString()
@MaxLength(120)
displayName?: string;
@IsOptional()
@IsString()
@MaxLength(512)
avatarKey?: string;
}
@@ -0,0 +1,23 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { KeycloakRequestUser } from '../../shared/decorators/current-user.decorator';
import { UsersService } from './users.service';
@Injectable()
export class UserProvisioningGuard implements CanActivate {
constructor(private readonly usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const keycloakUser = req.user as KeycloakRequestUser | undefined;
if (!keycloakUser?.userId) {
throw new UnauthorizedException();
}
req.dbUser = await this.usersService.ensureUser(keycloakUser);
return true;
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CurrentDbUser } from '../../shared/decorators/current-db-user.decorator';
import { User } from '@prisma/client';
import { UpdateMeDto } from './dto/update-me.dto';
import { UsersService } from './users.service';
import { UserProvisioningGuard } from './user-provisioning.guard';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@UseGuards(AuthGuard('keycloak'), UserProvisioningGuard)
@Get('me')
me(@CurrentDbUser() user: User) {
return user;
}
@UseGuards(AuthGuard('keycloak'), UserProvisioningGuard)
@Patch('me')
updateMe(@CurrentDbUser() user: User, @Body() dto: UpdateMeDto) {
return this.usersService.updateProfile(user.id, dto);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthModule } from '../auth/auth.module';
import { UserProvisioningGuard } from './user-provisioning.guard';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'keycloak' }),
AuthModule,
],
controllers: [UsersController],
providers: [UsersService, UserProvisioningGuard],
exports: [UsersService, UserProvisioningGuard],
})
export class UsersModule {}
+64
View File
@@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
import { Prisma, User, UserRole } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { KeycloakRequestUser } from '../../shared/decorators/current-user.decorator';
function mapKeycloakRolesToUserRole(realmRoles: string[]): UserRole {
const lower = realmRoles.map((r) => r.toLowerCase());
if (lower.includes('admin')) return UserRole.ADMIN;
if (lower.includes('editor')) return UserRole.EDITOR;
if (lower.includes('author')) return UserRole.AUTHOR;
return UserRole.READER;
}
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async ensureUser(kc: KeycloakRequestUser): Promise<User> {
const email = kc.email ?? `${kc.userId}@placeholder.local`;
const role = mapKeycloakRolesToUserRole(kc.roles ?? []);
const existing = await this.prisma.user.findUnique({
where: { keycloakId: kc.userId },
});
if (existing) {
const data: Prisma.UserUpdateInput = {};
if (kc.email && kc.email !== existing.email) data.email = kc.email;
if (kc.name && kc.name !== existing.displayName) {
data.displayName = kc.name;
}
if (Object.keys(data).length === 0) {
return existing;
}
return this.prisma.user.update({ where: { id: existing.id }, data });
}
return this.prisma.user.create({
data: {
keycloakId: kc.userId,
email,
displayName: kc.name ?? null,
role,
},
});
}
async findById(id: string) {
return this.prisma.user.findUnique({ where: { id } });
}
async updateProfile(userId: string, dto: { displayName?: string; avatarKey?: string }) {
return this.prisma.user.update({
where: { id: userId },
data: {
displayName: dto.displayName,
avatarKey: dto.avatarKey,
},
});
}
async setRole(userId: string, role: UserRole) {
return this.prisma.user.update({ where: { id: userId }, data: { role } });
}
}