proxy
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<string, string> };
|
||||
|
||||
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<string>('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<void> {
|
||||
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<string>('LLDAP_LDAP_URL') });
|
||||
try {
|
||||
await client.bind(this.adminDn(), this.config.getOrThrow<string>('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<void> {
|
||||
@@ -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<string>('LLDAP_ADMIN_USERNAME'));
|
||||
}
|
||||
|
||||
private userDn(username: string): string {
|
||||
const baseDn = this.config.getOrThrow<string>('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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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\""
|
||||
},
|
||||
|
||||
42
apps/web/proxy.conf.json
Normal file
42
apps/web/proxy.conf.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,12 @@ export class AdminRegistrationsComponent implements OnInit {
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
this.http.get<any[]>(`${this.apiBaseUrl}/admin/registrations`).subscribe({ next: (items) => this.registrations.set(items), error: (e) => this.error(e) });
|
||||
this.http
|
||||
.get<any[]>(`${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 {
|
||||
|
||||
@@ -12,20 +12,14 @@ import { API_BASE_URL } from '../shared/api-base-url';
|
||||
<section class="panel wide">
|
||||
<h1>Registrieren</h1>
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<div class="grid">
|
||||
<label>
|
||||
Benutzername
|
||||
<input formControlName="username" autocomplete="username">
|
||||
</label>
|
||||
<label>
|
||||
Anzeigename
|
||||
<input formControlName="displayName" autocomplete="name">
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
E-Mail
|
||||
<input type="email" formControlName="email" autocomplete="email">
|
||||
</label>
|
||||
<label>
|
||||
Anzeigename
|
||||
<input formControlName="displayName" autocomplete="name">
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
<input type="password" formControlName="password" autocomplete="new-password">
|
||||
@@ -55,9 +49,8 @@ export class RegisterComponent {
|
||||
@Inject(API_BASE_URL) private readonly apiBaseUrl: string,
|
||||
) {
|
||||
this.form = this.fb.nonNullable.group({
|
||||
username: ['', [Validators.required, Validators.minLength(3), Validators.pattern(/^[a-zA-Z0-9._-]+$/)]],
|
||||
displayName: ['', Validators.required],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
displayName: ['', Validators.required],
|
||||
password: ['', [Validators.required, Validators.minLength(12)]],
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user