Compare commits

...

3 Commits

11 changed files with 233 additions and 65 deletions

View File

@ -28,6 +28,9 @@
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@types/dompurify": "^3.0.5",
"crypto-js": "^4.2.0",
"dompurify": "^3.1.3",
"primeng": "^17.11.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@ -45,6 +48,7 @@
"@stylistic/eslint-plugin": "^2.1.0",
"@stylistic/eslint-plugin-migrate": "^2.1.0",
"@stylistic/eslint-plugin-ts": "^2.1.0",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "6.19.0",
"@typescript-eslint/parser": "6.19.0",

View File

@ -29,6 +29,15 @@ dependencies:
'@angular/router':
specifier: ^17.3.0
version: 17.3.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/platform-browser@17.3.0)(rxjs@7.8.1)
'@types/dompurify':
specifier: ^3.0.5
version: 3.0.5
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dompurify:
specifier: ^3.1.3
version: 3.1.3
primeng:
specifier: ^17.11.0
version: 17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4)
@ -76,6 +85,9 @@ devDependencies:
'@stylistic/eslint-plugin-ts':
specifier: ^2.1.0
version: 2.1.0(eslint@8.57.0)(typescript@5.4.2)
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/jest':
specifier: ^29.5.12
version: 29.5.12
@ -3231,6 +3243,16 @@ packages:
'@types/node': 20.11.27
dev: true
/@types/crypto-js@4.2.2:
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
dev: true
/@types/dompurify@3.0.5:
resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==}
dependencies:
'@types/trusted-types': 2.0.7
dev: false
/@types/eslint-scope@3.7.7:
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
dependencies:
@ -3400,6 +3422,10 @@ packages:
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
dev: true
/@types/trusted-types@2.0.7:
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
dev: false
/@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies:
@ -4710,6 +4736,10 @@ packages:
which: 2.0.2
dev: true
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-loader@6.10.0(webpack@5.90.3):
resolution: {integrity: sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==}
engines: {node: '>= 12.13.0'}
@ -4998,6 +5028,10 @@ packages:
domelementtype: 2.3.0
dev: true
/dompurify@3.1.3:
resolution: {integrity: sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng==}
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:

View File

@ -6,16 +6,16 @@
</div>
<div class="content-zone">
<h1>
@if (this.isSignupSignal()) {
@if (isSignupSignal()) {
Anmelden
} @else if (this.isRegisterSignal()) {
} @else if (isRegisterSignal()) {
Registrieren
} @else {
Erste Schritte
}
</h1>
@if (this.isDisplayButtons()) {
@if (isDisplayButtons()) {
<div class="action">
<button
pButton
@ -30,10 +30,10 @@
</div>
}
<div
class="register-wrapper"
*ngIf="isSignupSignal() || isRegisterSignal()">
<form [formGroup]="form" *ngIf="form" (ngSubmit)="onSubmit()">
@if (isSignupSignal() || isRegisterSignal()) {
<div class="register-wrapper">
@if (form) {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="e-mail">
<div class="label">
<label for="email">E-Mail</label>
@ -55,13 +55,13 @@
aria-describedby="password"
[toggleMask]="true"></p-password>
</div>
@if (this.isRegisterSignal()) {
@if (isRegisterSignal()) {
<div class="terms">
<p-checkbox
formControlName="terms"
label="Ich habe die AGB gelesen und stimme zu."
name="terms"
[binary]="true" />
[binary]="true"></p-checkbox>
</div>
}
<div class="signup">
@ -69,12 +69,17 @@
pButton
type="submit"
[label]="
isSignupSignal() ? 'Anmelden' : '✨ Jetzt KOSTENFREI loslegen ✨'
isSignupSignal()
? 'Anmelden'
: '✨ Jetzt KOSTENFREI loslegen ✨'
"></button>
</div>
<div class="change-mask">
<a (click)="switchMask()" (keyup.enter)="switchMask()" tabindex="0">
@if (this.isSignupSignal()) {
<a
(click)="switchMask()"
(keyup.enter)="switchMask()"
tabindex="0">
@if (isSignupSignal()) {
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
} @else {
Schon einen Account? Hier einloggen
@ -82,6 +87,8 @@
</a>
</div>
</form>
</div>
}
</div>
}
</div>
</div>

View File

@ -1,38 +1,77 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
import { LoginCredentials, Tokens } from '../types';
import { LocalStorageService } from './local-storage.service';
import { SessionStorageService } from './session-storage.service';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private isAuthenticated: boolean = false;
private readonly path: string = '/api/auth';
private access_token: string | null = null;
private refresh_token: string | null = null;
private isAuthenticated$: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(false);
public constructor(
private readonly httpClient: HttpClient,
private readonly router: Router
private readonly localStorageService: LocalStorageService,
private readonly sessionStorageService: SessionStorageService
) {}
public signin(credentials: LoginCredentials): void {
this.httpClient
.post<Tokens>(environment.api.base + '/api/auth/signin', credentials)
.post<Tokens>(environment.api.base + `${this.path}/signin`, credentials)
.subscribe((response: Tokens) => {
this.access_token = response.access_token;
this.refresh_token = response.refresh_token;
this.handleSuccess(response);
});
}
public signup(credentials: LoginCredentials): void {
this.httpClient
.post<Tokens>(environment.api.base + '/api/auth/signup', credentials)
.post<Tokens>(environment.api.base + `${this.path}/signup`, credentials)
.subscribe((response: Tokens) => {
this.access_token = response.access_token;
this.refresh_token = response.refresh_token;
// The checked accept terms should be saved with a timestamp in the db
this.handleSuccess(response);
});
}
public signout(): void {
this.access_token = null;
this.refresh_token = null;
this.localStorageService.removeItem('access_token');
this.sessionStorageService.removeItem('refresh_token');
this.isAuthenticated$.next(false);
}
public refreshToken(): void {
const headers = new HttpHeaders().set(
'Authorization',
'Bearer ' + this.refresh_token
);
this.httpClient
.post<Tokens>(
environment.api.base + `${this.path}/refresh`,
{},
{ headers: headers }
)
.subscribe((response: Tokens) => {
this.handleSuccess(response);
});
}
private handleSuccess(tokens: Tokens): void {
this.access_token = tokens.access_token;
this.refresh_token = tokens.refresh_token;
this.localStorageService.setItem('access_token', tokens.access_token);
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
this.isAuthenticated$.next(true);
}
}

View File

@ -0,0 +1,33 @@
import { EncryptionService } from './encryption.service';
import { SanitizationService } from './sanitization.service';
export abstract class BaseStorageService {
protected abstract getStorage(): Storage;
public setItem<T>(key: string, value: T): void {
const sanitizedValue = SanitizationService.sanitize(JSON.stringify(value));
const encryptedValue = EncryptionService.encrypt(sanitizedValue);
this.getStorage().setItem(key, encryptedValue);
}
public getItem<T>(key: string): T | null {
const encryptedValue = this.getStorage().getItem(key);
if (encryptedValue) {
const decryptedValue = EncryptionService.decrypt(encryptedValue);
const sanitizedValue = SanitizationService.sanitize(decryptedValue);
return JSON.parse(sanitizedValue) as T;
}
return null;
}
public removeItem(key: string): void {
this.getStorage().removeItem(key);
}
public clear(): void {
this.getStorage().clear();
}
}

View File

@ -0,0 +1,15 @@
import * as CryptoJS from 'crypto-js';
import { environment } from '../../../environments/environment';
export class EncryptionService {
private static readonly key: string = environment.security.encryptionKey;
public static encrypt(data: string): string {
return CryptoJS.AES.encrypt(data, this.key).toString();
}
public static decrypt(data: string): string {
return CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
}
}

View File

@ -1 +1,3 @@
export * from './auth.service';
export * from './local-storage.service';
export * from './session-storage.service';

View File

@ -0,0 +1,12 @@
import { Injectable } from '@angular/core';
import { BaseStorageService } from './base-storage.service';
@Injectable({
providedIn: 'root',
})
export class LocalStorageService extends BaseStorageService {
protected getStorage(): Storage {
return localStorage;
}
}

View File

@ -0,0 +1,7 @@
import DOMPurify from 'dompurify';
export class SanitizationService {
public static sanitize(data: string): string {
return DOMPurify.sanitize(data);
}
}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@angular/core';
import { BaseStorageService } from './base-storage.service';
@Injectable({
providedIn: 'root',
})
export class SessionStorageService extends BaseStorageService {
protected getStorage(): Storage {
return sessionStorage;
}
}

View File

@ -3,6 +3,9 @@ export const environment = {
api: {
base: 'http://localhost:3000',
},
security: {
encryptionKey: 'my-secret',
},
oauth: {
clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA',
redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth',