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 { Request } from 'express';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { RequestUser } from '../common/request-user';
|
import { RequestUser } from '../common/request-user';
|
||||||
@@ -12,6 +12,9 @@ export class AdminRegistrationsController {
|
|||||||
constructor(private readonly registrations: RegistrationService) {}
|
constructor(private readonly registrations: RegistrationService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
||||||
|
@Header('Pragma', 'no-cache')
|
||||||
|
@Header('Expires', '0')
|
||||||
list() {
|
list() {
|
||||||
return this.registrations.list();
|
return this.registrations.list();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Ber, BerWriter, Client } from 'ldapts';
|
||||||
|
|
||||||
interface LldapUserInput {
|
interface LldapUserInput {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -70,6 +71,8 @@ export interface LldapAccountUser {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LldapService {
|
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> };
|
private cachedHeaders?: { expiresAt: number; headers: Record<string, string> };
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService) {}
|
||||||
@@ -84,11 +87,17 @@ export class LldapService {
|
|||||||
id: input.username,
|
id: input.username,
|
||||||
email: input.email,
|
email: input.email,
|
||||||
displayName: input.displayName,
|
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');
|
const defaultGroup = this.config.get<string>('LLDAP_DEFAULT_GROUP');
|
||||||
if (defaultGroup) {
|
if (defaultGroup) {
|
||||||
await this.addUserToGroup(input.username, defaultGroup);
|
await this.addUserToGroup(input.username, defaultGroup);
|
||||||
@@ -96,12 +105,18 @@ export class LldapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setPassword(username: string, password: string): Promise<void> {
|
async setPassword(username: string, password: string): Promise<void> {
|
||||||
await this.graphql(
|
const client = new Client({ url: this.config.getOrThrow<string>('LLDAP_LDAP_URL') });
|
||||||
`mutation SetPassword($userId: String!, $password: String!) {
|
try {
|
||||||
setPassword(userId: $userId, password: $password)
|
await client.bind(this.adminDn(), this.config.getOrThrow<string>('LLDAP_ADMIN_PASSWORD'));
|
||||||
}`,
|
await client.exop(
|
||||||
{ userId: username, password },
|
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> {
|
async updateUser(username: string, input: LldapUserUpdateInput): Promise<void> {
|
||||||
@@ -438,4 +453,30 @@ export class LldapService {
|
|||||||
this.cachedHeaders = { headers, expiresAt: Date.now() + 5 * 60_000 };
|
this.cachedHeaders = { headers, expiresAt: Date.now() + 5 * 60_000 };
|
||||||
return headers;
|
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 {
|
export class RegisterDto {
|
||||||
@IsString()
|
|
||||||
@Length(3, 64)
|
|
||||||
@Matches(/^[a-zA-Z0-9._-]+$/)
|
|
||||||
username!: string;
|
|
||||||
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { In, IsNull, Repository } from 'typeorm';
|
||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
import { assertPasswordPolicy } from '../common/password-policy';
|
import { assertPasswordPolicy } from '../common/password-policy';
|
||||||
import { decryptSecret, encryptSecret, hashToken, randomToken } from '../common/token.util';
|
import { decryptSecret, encryptSecret, hashToken, randomToken } from '../common/token.util';
|
||||||
@@ -26,23 +26,24 @@ export class RegistrationService {
|
|||||||
|
|
||||||
async register(dto: RegisterDto, ipAddress?: string, userAgent?: string) {
|
async register(dto: RegisterDto, ipAddress?: string, userAgent?: string) {
|
||||||
assertPasswordPolicy(dto.password);
|
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) {
|
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({
|
const pending = await this.registrations.findOne({
|
||||||
where: { username: dto.username, status: 'pending_email' },
|
where: { username: email, status: 'pending_email' },
|
||||||
});
|
});
|
||||||
if (pending) {
|
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(
|
const registration = await this.registrations.save(
|
||||||
this.registrations.create({
|
this.registrations.create({
|
||||||
username: dto.username,
|
username: email,
|
||||||
email: dto.email.toLowerCase(),
|
email,
|
||||||
displayName: dto.displayName,
|
displayName: dto.displayName,
|
||||||
encryptedPassword: encryptSecret(dto.password, this.tokenSecret),
|
encryptedPassword: encryptSecret(dto.password, this.tokenSecret),
|
||||||
status: 'pending_email',
|
status: 'pending_email',
|
||||||
@@ -99,7 +100,10 @@ export class RegistrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async list() {
|
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) {
|
async approve(id: string, reviewer: string, ipAddress?: string, userAgent?: string) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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",
|
"build": "ng build",
|
||||||
"lint": "eslint \"src/**/*.ts\""
|
"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 {
|
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 {
|
private error(error: unknown): void {
|
||||||
|
|||||||
@@ -12,20 +12,14 @@ import { API_BASE_URL } from '../shared/api-base-url';
|
|||||||
<section class="panel wide">
|
<section class="panel wide">
|
||||||
<h1>Registrieren</h1>
|
<h1>Registrieren</h1>
|
||||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
<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>
|
<label>
|
||||||
E-Mail
|
E-Mail
|
||||||
<input type="email" formControlName="email" autocomplete="email">
|
<input type="email" formControlName="email" autocomplete="email">
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Anzeigename
|
||||||
|
<input formControlName="displayName" autocomplete="name">
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Passwort
|
Passwort
|
||||||
<input type="password" formControlName="password" autocomplete="new-password">
|
<input type="password" formControlName="password" autocomplete="new-password">
|
||||||
@@ -55,9 +49,8 @@ export class RegisterComponent {
|
|||||||
@Inject(API_BASE_URL) private readonly apiBaseUrl: string,
|
@Inject(API_BASE_URL) private readonly apiBaseUrl: string,
|
||||||
) {
|
) {
|
||||||
this.form = this.fb.nonNullable.group({
|
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]],
|
email: ['', [Validators.required, Validators.email]],
|
||||||
|
displayName: ['', Validators.required],
|
||||||
password: ['', [Validators.required, Validators.minLength(12)]],
|
password: ['', [Validators.required, Validators.minLength(12)]],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user