Compare commits

..

No commits in common. "d96572f9750602778460920684075115e2a831ff" and "d8f65f124199f246dc51fb3b4ba6e49a1326f57f" have entirely different histories.

21 changed files with 332 additions and 278 deletions

View File

@ -14,7 +14,6 @@ export class CorsMiddleware implements NestMiddleware {
const requestOrigin = req.headers.origin; const requestOrigin = req.headers.origin;
if (!requestOrigin || allowedOrigins.includes(requestOrigin)) { if (!requestOrigin || allowedOrigins.includes(requestOrigin)) {
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', requestOrigin || '*'); res.header('Access-Control-Allow-Origin', requestOrigin || '*');
res.header( res.header(
'Access-Control-Allow-Methods', 'Access-Control-Allow-Methods',

View File

@ -12,7 +12,6 @@ export class CspMiddleware implements NestMiddleware {
if (cspDirectives) { if (cspDirectives) {
res.setHeader('Content-Security-Policy', cspDirectives); res.setHeader('Content-Security-Policy', cspDirectives);
} }
next(); next();
} }
} }

View File

@ -56,9 +56,9 @@ export class AuthController {
}) })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@UseGuards(SessionGuard) @UseGuards(SessionGuard)
@Post('signout') @Post('logout')
public async signout(@Req() request: Request): Promise<SuccessDto> { public async logout(@Req() request: Request): Promise<SuccessDto> {
return this.authService.signout(request.sessionID); return this.authService.logout(request.sessionID);
} }
@ApiCreatedResponse({ @ApiCreatedResponse({

View File

@ -110,18 +110,6 @@ export class AuthService {
} }
} }
public async signout(sessionId: string): Promise<{ success: boolean }> {
try {
this.sessionService.deleteSessionBySessionId(sessionId);
return { success: true };
} catch (error) {
throw new HttpException(
'Fehler beim Logout',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
public async checkAuthStatus( public async checkAuthStatus(
sessionId: string, sessionId: string,
userAgend: string userAgend: string
@ -157,4 +145,16 @@ export class AuthService {
return responseData; return responseData;
} }
public async logout(sessionId: string): Promise<{ success: boolean }> {
try {
this.sessionService.deleteSessionBySessionId(sessionId);
return { success: true };
} catch (error) {
throw new HttpException(
'Fehler beim Logout',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
} }

View File

@ -27,14 +27,10 @@ export class SessionInitService {
maxAge: 86400000, maxAge: 86400000,
httpOnly: true, httpOnly: true,
secure: secure:
this.configService.get<string>('NODE_ENV') === 'development' this.configService.get<string>('NODE_ENV') === 'production'
? false ? true
: true, : false,
sameSite: 'strict',
sameSite:
this.configService.get<string>('NODE_ENV') === 'development'
? 'strict'
: 'none',
}, },
}); });
} }

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet, Router } from '@angular/router';
import { AuthService } from './shared/service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
@ -9,6 +10,24 @@ import { RouterOutlet } from '@angular/router';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent implements OnInit {
public constructor() {} public constructor(
private readonly authService: AuthService,
private readonly router: Router
) {}
public ngOnInit(): void {
this.checkAuthentication();
}
private checkAuthentication(): void {
this.authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
if (isAuthenticated) {
console.log('User is authenticated');
this.router.navigateByUrl('dashboard');
} else {
this.router.navigateByUrl('signup');
}
});
}
} }

View File

@ -3,18 +3,12 @@ import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withComponentInputBinding } from '@angular/router'; import { provideRouter, withComponentInputBinding } from '@angular/router';
import { Configuration } from './api';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { ApiConfiguration } from './config/api-configuration'; import { AuthInterceptor } from './shared/interceptors/auth.interceptor';
const apiConfiguration = new ApiConfiguration({
withCredentials: true,
});
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
{ provide: Configuration, useValue: apiConfiguration }, provideHttpClient(withInterceptors([AuthInterceptor])),
provideHttpClient(withInterceptors([])),
provideRouter(routes, withComponentInputBinding()), provideRouter(routes, withComponentInputBinding()),
provideAnimations(), provideAnimations(),
], ],

View File

@ -1,12 +1,11 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { AuthGuard } from './shared/guard/auth.guard';
const publicRoutes: Routes = [ const publicRoutes: Routes = [
{ {
path: '', path: '',
loadComponent: () => loadComponent: () => import('./app.component').then((m) => m.AppComponent),
import('./pages/home-root/home-root.component').then(
(m) => m.HomeComponent
),
}, },
{ {
path: 'signup', path: 'signup',
@ -31,7 +30,7 @@ const protectedRoutes: Routes = [
import('./pages/dashboard-root/dashboard-root.component').then( import('./pages/dashboard-root/dashboard-root.component').then(
(m) => m.DashboardRootComponent (m) => m.DashboardRootComponent
), ),
canActivate: [], canActivate: [AuthGuard],
}, },
]; ];

View File

@ -1,9 +0,0 @@
import { Configuration, ConfigurationParameters } from '../api';
export class ApiConfiguration extends Configuration {
public constructor(params?: Partial<ConfigurationParameters>) {
super({
...params,
});
}
}

View File

@ -1,38 +0,0 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { SuccessDtoApiModel } from '../../api';
import { AuthService } from '../../shared/service';
@Component({
selector: 'app-foo',
standalone: true,
providers: [],
imports: [],
templateUrl: './home-root.component.html',
styleUrl: './home-root.component.scss',
})
export class HomeComponent implements OnInit {
public constructor(
private readonly authService: AuthService,
private readonly router: Router
) {}
public ngOnInit(): void {
this.authService.status().subscribe(
(response: SuccessDtoApiModel) => {
if (response.success) {
this.router.navigate(['/dashboard']);
}
},
(error: HttpErrorResponse) => {
if (error.status === 401) {
this.router.navigate(['signup'], {
queryParams: { login: true },
});
}
}
);
}
}

View File

@ -1,110 +1,94 @@
<div id="background"> <div id="background">
<div class="img-zone"> <div class="img-zone">
<div class="img-wrapper"> <div class="img-wrapper">
@if (userSignupSuccess()) { <h1>Hi, Welcome to Ticket App.</h1>
<div class="success">
<h1>Danke für deine Registrierung!</h1>
<h2>
Wir haben dir eine Mail geschickt an
{{ form?.get('email')?.value }}. Bitte bestätige deine
E-Mail-Adresse um fortzufahren.
</h2>
<p>Du kannst diesen Tab nun schließen</p>
</div>
} @else {
<div class="headline">
<h1>Hi, Welcome to Ticket App.</h1>
</div>
}
</div> </div>
</div> </div>
<div class="content-zone">
<h1>
@if (isSignupSignal()) {
Anmelden
} @else if (isRegisterSignal()) {
Registrieren
} @else {
Erste Schritte
}
</h1>
@if (!userSignupSuccess()) { @if (isDisplayButtons()) {
<div class="content-zone"> <div class="action">
<h1> <button
@if (isSignupSignal()) { pButton
Anmelden type="button"
} @else if (isRegisterSignal()) { label="Anmelden"
Registrieren (click)="toggleAction('signup')"></button>
} @else { <button
Erste Schritte pButton
type="button"
label="Registrieren"
(click)="toggleAction('register')"></button>
</div>
}
@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>
</div>
<input
pInputText
id="email"
formControlName="email"
aria-describedby="e-mail" />
</div>
<div class="password">
<div class="label">
<label for="password">Password</label>
</div>
<p-password
class="custom-p-password"
id="password"
formControlName="password"
aria-describedby="password"
[toggleMask]="true"></p-password>
</div>
@if (isRegisterSignal()) {
<div class="terms">
<p-checkbox
formControlName="terms"
label="Ich habe die AGB gelesen und stimme zu."
name="terms"
[binary]="true"></p-checkbox>
</div>
}
<div class="signup">
<button
pButton
type="submit"
[label]="
isSignupSignal()
? 'Anmelden'
: '✨ Jetzt KOSTENFREI loslegen ✨'
"></button>
</div>
<div class="change-mask">
<a
(click)="switchMask()"
(keyup.enter)="switchMask()"
tabindex="0">
@if (isSignupSignal()) {
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
} @else {
Schon einen Account? Hier einloggen
}
</a>
</div>
</form>
} }
</h1> </div>
}
@if (isDisplayButtons()) { </div>
<div class="action">
<button
pButton
type="button"
label="Anmelden"
(click)="toggleAction('signup')"></button>
<button
pButton
type="button"
label="Registrieren"
(click)="toggleAction('register')"></button>
</div>
}
@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>
</div>
<input
pInputText
id="email"
formControlName="email"
aria-describedby="e-mail" />
</div>
<div class="password">
<div class="label">
<label for="password">Password</label>
</div>
<p-password
class="custom-p-password"
id="password"
formControlName="password"
aria-describedby="password"
[toggleMask]="true"></p-password>
</div>
@if (isRegisterSignal()) {
<div class="terms">
<p-checkbox
formControlName="terms"
label="Ich habe die AGB gelesen und stimme zu."
name="terms"
[binary]="true"></p-checkbox>
</div>
}
<div class="signup">
<button
pButton
type="submit"
[label]="
isSignupSignal()
? 'Anmelden'
: '✨ Jetzt KOSTENFREI loslegen ✨'
"></button>
</div>
<div class="change-mask">
<a
(click)="switchMask()"
(keyup.enter)="switchMask()"
tabindex="0">
@if (isSignupSignal()) {
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
} @else {
Schon einen Account? Hier einloggen
}
</a>
</div>
</form>
}
</div>
}
</div>
}
</div> </div>

View File

@ -12,20 +12,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
.success { h1 {
margin-left: 4em; font-size: 4em;
h1 { margin-left: 1em;
font-size: 4em;
}
} }
.headline {
h1 {
font-size: 4em;
margin-left: 1em;
}
}
} }
} }

View File

@ -25,14 +25,8 @@ import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { PasswordModule } from 'primeng/password'; import { PasswordModule } from 'primeng/password';
import { import { AuthService } from '../../shared/service';
Configuration, import { LoginCredentials } from '../../shared/types';
SigninResponseDtoApiModel,
SuccessDtoApiModel,
UserCredentialsDtoApiModel,
} from '../../api';
import { ApiConfiguration } from '../../config/api-configuration';
import { AuthService, SessionStorageService } from '../../shared/service';
import { import {
customEmailValidator, customEmailValidator,
customPasswordValidator, customPasswordValidator,
@ -53,20 +47,13 @@ type AuthAction = 'register' | 'signup';
PasswordModule, PasswordModule,
HttpClientModule, HttpClientModule,
], ],
providers: [ providers: [],
{
provide: Configuration,
useFactory: (): unknown =>
new ApiConfiguration({ withCredentials: true }),
},
],
templateUrl: './register-root.component.html', templateUrl: './register-root.component.html',
styleUrl: './register-root.component.scss', styleUrl: './register-root.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RegisterRootComponent implements OnInit { export class RegisterRootComponent implements OnInit {
public verified: InputSignal<boolean> = input<boolean>(false); public verified: InputSignal<boolean> = input<boolean>(false);
public login: InputSignal<boolean> = input<boolean>(false);
public email: InputSignal<string> = input<string>(''); public email: InputSignal<string> = input<string>('');
public form: FormGroup | undefined; public form: FormGroup | undefined;
public isRegisterSignal: WritableSignal<boolean> = signal(false); public isRegisterSignal: WritableSignal<boolean> = signal(false);
@ -75,14 +62,12 @@ export class RegisterRootComponent implements OnInit {
public emailInvalid: WritableSignal<string | null> = signal(null); public emailInvalid: WritableSignal<string | null> = signal(null);
public passwordInvalid: WritableSignal<string | null> = signal(null); public passwordInvalid: WritableSignal<string | null> = signal(null);
public termsInvalid: WritableSignal<string | null> = signal(null); public termsInvalid: WritableSignal<string | null> = signal(null);
public userSignupSuccess: WritableSignal<boolean> = signal(false);
private removeQueryParams: WritableSignal<boolean> = signal(false); private removeQueryParams: WritableSignal<boolean> = signal(false);
public constructor( public constructor(
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly router: Router, private readonly router: Router
private readonly sessionStorageService: SessionStorageService
) { ) {
effect(() => { effect(() => {
if (this.form) { if (this.form) {
@ -105,22 +90,13 @@ export class RegisterRootComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.initializeForm(); this.initializeForm();
this.setupValueChanges(); this.setupValueChanges();
this.preselectForm();
if ((this.email() && this.verified()) || this.login()) { if (this.email() || this.verified()) {
this.handleRedirect(); this.handleRedirect();
this.removeQueryParams.set(true); this.removeQueryParams.set(true);
} }
} }
public preselectForm(): void {
if (!this.email() || !this.verified()) {
const email = this.sessionStorageService.getItem('email');
this.form?.get('email')?.setValue(email);
}
}
public toggleAction(action: AuthAction): void { public toggleAction(action: AuthAction): void {
if (action === 'register') { if (action === 'register') {
this.isRegisterSignal.set(true); this.isRegisterSignal.set(true);
@ -137,7 +113,7 @@ export class RegisterRootComponent implements OnInit {
if (this.form?.valid) { if (this.form?.valid) {
if (this.isRegisterSignal()) { if (this.isRegisterSignal()) {
this.signup(this.form.value); this.register(this.form.value);
} else { } else {
this.signin(this.form.value); this.signin(this.form.value);
} }
@ -174,6 +150,7 @@ export class RegisterRootComponent implements OnInit {
} }
private handleRedirect(): void { private handleRedirect(): void {
console.log('handleRedirect');
if (this.verified()) { if (this.verified()) {
this.isDisplayButtons.set(false); this.isDisplayButtons.set(false);
this.isRegisterSignal.set(false); this.isRegisterSignal.set(false);
@ -182,12 +159,6 @@ export class RegisterRootComponent implements OnInit {
if (this.email()) { if (this.email()) {
this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email()))); this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email())));
} }
if (this.login()) {
this.isSignupSignal.set(true);
this.isDisplayButtons.set(false);
this.isRegisterSignal.set(false);
}
} }
private clearRouteParams(): void { private clearRouteParams(): void {
@ -266,23 +237,11 @@ export class RegisterRootComponent implements OnInit {
} }
} }
private signin(logiCredentials: UserCredentialsDtoApiModel): void { private signin(logiCredentials: LoginCredentials): void {
this.authService this.authService.signin(logiCredentials);
.signin(logiCredentials)
.subscribe((response: SigninResponseDtoApiModel) => {
if (response) {
this.router.navigate(['/dashboard']);
}
});
} }
private signup(logiCredentials: UserCredentialsDtoApiModel): void { private register(logiCredentials: LoginCredentials): void {
this.authService this.authService.signup(logiCredentials);
.signup(logiCredentials)
.subscribe((response: SuccessDtoApiModel) => {
if (response.success) {
this.userSignupSuccess.set(true);
}
});
} }
} }

View File

@ -0,0 +1,32 @@
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../service';
export const AuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree => {
const authService: AuthService = inject(AuthService);
const router: Router = inject(Router);
authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
if (!isAuthenticated) {
router.navigateByUrl('signup');
}
});
return true;
};

View File

@ -0,0 +1,79 @@
import {
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpEvent,
HttpErrorResponse,
} from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { AuthService } from '../service';
export const AuthInterceptor: HttpInterceptorFn = (
request: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const router = inject(Router);
const authService = inject(AuthService);
const handleRequest = (
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
const accessToken = authService.access_token;
if (accessToken) {
req = addAuthHeader(req, accessToken);
}
return next(req);
};
const addAuthHeader = (
req: HttpRequest<unknown>,
token: string
): HttpRequest<unknown> => {
return req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
};
const handle401Error = (
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
console.log(authService.refresh_token);
if (!authService.refresh_token) {
router.navigateByUrl('signup');
return throwError(() => new Error('Authentication required'));
}
return authService.refreshToken().pipe(
switchMap((tokens) => {
req = addAuthHeader(req, tokens.access_token);
return next(req);
}),
catchError((refreshError) => {
router.navigateByUrl('signup');
return throwError(() => new Error(refreshError));
})
);
};
const handleError = (
error: HttpErrorResponse,
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
if (error.status === 401) {
return handle401Error(req);
}
return throwError(() => new Error('Unhandled error'));
};
return handleRequest(request).pipe(
catchError((error) => handleError(error, request))
);
};

View File

@ -1,16 +1,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, tap } from 'rxjs';
import {
SigninResponseDtoApiModel,
UserCredentialsDtoApiModel,
} from '../../api';
import { AuthenticationApiService } from '../../api/api/authentication.api.service'; import { AuthenticationApiService } from '../../api/api/authentication.api.service';
import { LoginCredentials, Tokens } from '../types';
type SuccessResponse = { import { LocalStorageService } from './local-storage.service';
success: boolean; import { SessionStorageService } from './session-storage.service';
};
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -18,28 +14,73 @@ type SuccessResponse = {
export class AuthService { export class AuthService {
public isAuthenticated$: BehaviorSubject<boolean> = public isAuthenticated$: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(false); new BehaviorSubject<boolean>(false);
private _access_token: string | null = null;
private _refresh_token: string | null = null;
public get access_token(): string | null {
return this._access_token;
}
public get refresh_token(): string | null {
return this._refresh_token;
}
public constructor( public constructor(
private readonly localStorageService: LocalStorageService,
private readonly sessionStorageService: SessionStorageService,
private readonly authenticationApiService: AuthenticationApiService private readonly authenticationApiService: AuthenticationApiService
) {} ) {
this._access_token =
public signup( this.localStorageService.getItem<string>('access_token');
credentials: UserCredentialsDtoApiModel this._refresh_token =
): Observable<SuccessResponse> { this.sessionStorageService.getItem<string>('refresh_token');
return this.authenticationApiService.authControllerSignup(credentials);
} }
public signin( public signin(credentials: LoginCredentials): void {
credentials: UserCredentialsDtoApiModel this.authenticationApiService
): Observable<SigninResponseDtoApiModel> { .authControllerSignin(credentials)
return this.authenticationApiService.authControllerSignin(credentials); .subscribe((response: Tokens) => {
this.handleSuccess(response);
});
} }
public signout(): Observable<SuccessResponse> { public signup(credentials: LoginCredentials): void {
return this.authenticationApiService.authControllerSignout(); this.authenticationApiService
.authControllerSignup(credentials)
.subscribe((response: Tokens) => {
this.handleSuccess(response);
});
} }
public status(): Observable<SuccessResponse> { public refreshToken(): Observable<Tokens> {
return this.authenticationApiService.authControllerStatus(); if (this._refresh_token) {
return this.authenticationApiService
.authControllerRefresh(this._refresh_token)
.pipe(tap((response: Tokens) => this.handleSuccess(response)));
} else {
throw new Error('Refresh token is missing');
}
}
public signout(): void {
this.authenticationApiService
.authControllerLogout()
.subscribe((response: boolean) => {
if (response) {
this._access_token = null;
this._refresh_token = null;
this.localStorageService.removeItem('access_token');
this.sessionStorageService.removeItem('refresh_token');
this.isAuthenticated$.next(false);
}
});
}
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,2 @@
export * from './login-credentials';
export * from './tokens';

View File

@ -0,0 +1,4 @@
export type LoginCredentials = {
email: string;
password: string;
};

View File

@ -0,0 +1,4 @@
export type Tokens = {
access_token: string;
refresh_token: string;
};