diff --git a/apps/api/src/admin/admin-registrations.controller.ts b/apps/api/src/admin/admin-registrations.controller.ts index 8aaae5a..fd1e256 100644 --- a/apps/api/src/admin/admin-registrations.controller.ts +++ b/apps/api/src/admin/admin-registrations.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Header, Param, Post, Req, UseGuards } from '@nestjs/common'; import { Request } from 'express'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RequestUser } from '../common/request-user'; @@ -12,6 +12,9 @@ export class AdminRegistrationsController { constructor(private readonly registrations: RegistrationService) {} @Get() + @Header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate') + @Header('Pragma', 'no-cache') + @Header('Expires', '0') list() { return this.registrations.list(); } diff --git a/apps/api/src/lldap/lldap.service.ts b/apps/api/src/lldap/lldap.service.ts index 0a7cf7d..c264001 100644 --- a/apps/api/src/lldap/lldap.service.ts +++ b/apps/api/src/lldap/lldap.service.ts @@ -1,5 +1,6 @@ import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Ber, BerWriter, Client } from 'ldapts'; interface LldapUserInput { username: string; @@ -70,6 +71,8 @@ export interface LldapAccountUser { @Injectable() export class LldapService { + private static readonly passwordModifyOid = '1.3.6.1.4.1.4203.1.11.1'; + private cachedHeaders?: { expiresAt: number; headers: Record }; constructor(private readonly config: ConfigService) {} @@ -84,11 +87,17 @@ export class LldapService { id: input.username, email: input.email, displayName: input.displayName, - password: input.password, }, }, ); + try { + await this.setPassword(input.username, input.password); + } catch (error) { + await this.deleteUser(input.username).catch(() => undefined); + throw error; + } + const defaultGroup = this.config.get('LLDAP_DEFAULT_GROUP'); if (defaultGroup) { await this.addUserToGroup(input.username, defaultGroup); @@ -96,12 +105,18 @@ export class LldapService { } async setPassword(username: string, password: string): Promise { - await this.graphql( - `mutation SetPassword($userId: String!, $password: String!) { - setPassword(userId: $userId, password: $password) - }`, - { userId: username, password }, - ); + const client = new Client({ url: this.config.getOrThrow('LLDAP_LDAP_URL') }); + try { + await client.bind(this.adminDn(), this.config.getOrThrow('LLDAP_ADMIN_PASSWORD')); + await client.exop( + LldapService.passwordModifyOid, + this.passwordModifyRequestValue(this.userDn(username), password), + ); + } catch (error) { + throw new InternalServerErrorException(`LLDAP password change failed: ${this.errorMessage(error)}`); + } finally { + await client.unbind().catch(() => undefined); + } } async updateUser(username: string, input: LldapUserUpdateInput): Promise { @@ -438,4 +453,30 @@ export class LldapService { this.cachedHeaders = { headers, expiresAt: Date.now() + 5 * 60_000 }; return headers; } + + private passwordModifyRequestValue(userDn: string, newPassword: string): Buffer { + const writer = new BerWriter(); + writer.startSequence(Ber.Sequence | Ber.Constructor); + writer.writeString(userDn, 0x80); + writer.writeString(newPassword, 0x82); + writer.endSequence(); + return writer.buffer; + } + + private adminDn(): string { + return this.userDn(this.config.getOrThrow('LLDAP_ADMIN_USERNAME')); + } + + private userDn(username: string): string { + const baseDn = this.config.getOrThrow('LLDAP_BASE_DN'); + return `uid=${this.escapeDn(username)},ou=people,${baseDn}`; + } + + private escapeDn(value: string): string { + return value.replace(/[\\,+"<>;=]/g, (char) => `\\${char}`); + } + + private errorMessage(error: unknown): string { + return error instanceof Error ? error.message : 'unknown error'; + } } diff --git a/apps/api/src/registration/dto/register.dto.ts b/apps/api/src/registration/dto/register.dto.ts index 53c9d95..2aeb01d 100644 --- a/apps/api/src/registration/dto/register.dto.ts +++ b/apps/api/src/registration/dto/register.dto.ts @@ -1,11 +1,6 @@ -import { IsEmail, IsString, Length, Matches } from 'class-validator'; +import { IsEmail, IsString, Length } from 'class-validator'; export class RegisterDto { - @IsString() - @Length(3, 64) - @Matches(/^[a-zA-Z0-9._-]+$/) - username!: string; - @IsEmail() email!: string; diff --git a/apps/api/src/registration/registration.service.ts b/apps/api/src/registration/registration.service.ts index 1446a9b..9707e70 100644 --- a/apps/api/src/registration/registration.service.ts +++ b/apps/api/src/registration/registration.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { AuditService } from '../audit/audit.service'; import { assertPasswordPolicy } from '../common/password-policy'; import { decryptSecret, encryptSecret, hashToken, randomToken } from '../common/token.util'; @@ -26,23 +26,24 @@ export class RegistrationService { async register(dto: RegisterDto, ipAddress?: string, userAgent?: string) { assertPasswordPolicy(dto.password); + const email = dto.email.toLowerCase(); - const existingLdapUser = await this.lldap.findUserByUsername(dto.username).catch(() => null); + const existingLdapUser = await this.lldap.findUserByUsername(email).catch(() => null); if (existingLdapUser) { - throw new ConflictException('Der Benutzername ist bereits vergeben.'); + throw new ConflictException('Diese E-Mail-Adresse ist bereits registriert.'); } const pending = await this.registrations.findOne({ - where: { username: dto.username, status: 'pending_email' }, + where: { username: email, status: 'pending_email' }, }); if (pending) { - throw new ConflictException('Fuer diesen Benutzernamen existiert bereits eine offene Registrierung.'); + throw new ConflictException('Fuer diese E-Mail-Adresse existiert bereits eine offene Registrierung.'); } const registration = await this.registrations.save( this.registrations.create({ - username: dto.username, - email: dto.email.toLowerCase(), + username: email, + email, displayName: dto.displayName, encryptedPassword: encryptSecret(dto.password, this.tokenSecret), status: 'pending_email', @@ -99,7 +100,10 @@ export class RegistrationService { } async list() { - return this.registrations.find({ order: { createdAt: 'DESC' } }); + return this.registrations.find({ + where: { status: In(['pending_email', 'pending_approval']) }, + order: { createdAt: 'DESC' }, + }); } async approve(id: string, reviewer: string, ipAddress?: string, userAgent?: string) { diff --git a/apps/web/package.json b/apps/web/package.json index 4bc5081..e35dda3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "start": "ng serve --host 0.0.0.0 --port 4200", + "start": "ng serve --host 0.0.0.0 --port 4200 --proxy-config proxy.conf.json", "build": "ng build", "lint": "eslint \"src/**/*.ts\"" }, diff --git a/apps/web/proxy.conf.json b/apps/web/proxy.conf.json new file mode 100644 index 0000000..175691e --- /dev/null +++ b/apps/web/proxy.conf.json @@ -0,0 +1,42 @@ +{ + "/.well-known": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/oidc": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/interaction": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/auth": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/account": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/admin": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/password": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/registration": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + } +} diff --git a/apps/web/src/app/pages/admin-registrations.component.ts b/apps/web/src/app/pages/admin-registrations.component.ts index 2f0fa9b..383bb64 100644 --- a/apps/web/src/app/pages/admin-registrations.component.ts +++ b/apps/web/src/app/pages/admin-registrations.component.ts @@ -47,7 +47,12 @@ export class AdminRegistrationsComponent implements OnInit { } private load(): void { - this.http.get(`${this.apiBaseUrl}/admin/registrations`).subscribe({ next: (items) => this.registrations.set(items), error: (e) => this.error(e) }); + this.http + .get(`${this.apiBaseUrl}/admin/registrations`, { + headers: { 'Cache-Control': 'no-cache', Pragma: 'no-cache' }, + params: { _: Date.now() }, + }) + .subscribe({ next: (items) => this.registrations.set(items), error: (e) => this.error(e) }); } private error(error: unknown): void { diff --git a/apps/web/src/app/pages/register.component.ts b/apps/web/src/app/pages/register.component.ts index 8303df0..f6726fa 100644 --- a/apps/web/src/app/pages/register.component.ts +++ b/apps/web/src/app/pages/register.component.ts @@ -12,20 +12,14 @@ import { API_BASE_URL } from '../shared/api-base-url';

Registrieren

-
- - -
+