diff --git a/backend/src/middleware/cors-middleware/cors.middlware.ts b/backend/src/middleware/cors-middleware/cors.middlware.ts index 653da38..8850df1 100644 --- a/backend/src/middleware/cors-middleware/cors.middlware.ts +++ b/backend/src/middleware/cors-middleware/cors.middlware.ts @@ -14,6 +14,7 @@ export class CorsMiddleware implements NestMiddleware { const requestOrigin = req.headers.origin; if (!requestOrigin || allowedOrigins.includes(requestOrigin)) { + res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Origin', requestOrigin || '*'); res.header( 'Access-Control-Allow-Methods', diff --git a/backend/src/middleware/csp-middleware/csp.middleware.ts b/backend/src/middleware/csp-middleware/csp.middleware.ts index 3754fa4..71597db 100644 --- a/backend/src/middleware/csp-middleware/csp.middleware.ts +++ b/backend/src/middleware/csp-middleware/csp.middleware.ts @@ -12,6 +12,7 @@ export class CspMiddleware implements NestMiddleware { if (cspDirectives) { res.setHeader('Content-Security-Policy', cspDirectives); } + next(); } } diff --git a/backend/src/modules/session/services/session-init.service.ts b/backend/src/modules/session/services/session-init.service.ts index 80e4315..9a4fb7b 100644 --- a/backend/src/modules/session/services/session-init.service.ts +++ b/backend/src/modules/session/services/session-init.service.ts @@ -27,10 +27,14 @@ export class SessionInitService { maxAge: 86400000, httpOnly: true, secure: - this.configService.get('NODE_ENV') === 'production' - ? true - : false, - sameSite: 'strict', + this.configService.get('NODE_ENV') === 'development' + ? false + : true, + + sameSite: + this.configService.get('NODE_ENV') === 'development' + ? 'strict' + : 'none', }, }); } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 763c832..9b8909b 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,7 +1,6 @@ -import { Component, OnInit } from '@angular/core'; -import { RouterOutlet, Router } from '@angular/router'; +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; -import { AuthService } from './shared/service'; @Component({ selector: 'app-root', standalone: true, @@ -10,24 +9,6 @@ import { AuthService } from './shared/service'; templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent implements OnInit { - 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'); - } - }); - } +export class AppComponent { + public constructor() {} } diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 9b95e78..f3f5066 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -3,12 +3,18 @@ import { ApplicationConfig } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { Configuration } from './api'; import { routes } from './app.routes'; -import { AuthInterceptor } from './shared/interceptors/auth.interceptor'; +import { ApiConfiguration } from './config/api-configuration'; + +const apiConfiguration = new ApiConfiguration({ + withCredentials: true, +}); export const appConfig: ApplicationConfig = { providers: [ - provideHttpClient(withInterceptors([AuthInterceptor])), + { provide: Configuration, useValue: apiConfiguration }, + provideHttpClient(withInterceptors([])), provideRouter(routes, withComponentInputBinding()), provideAnimations(), ], diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 2608632..5175263 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,11 +1,12 @@ import { Routes } from '@angular/router'; -import { AuthGuard } from './shared/guard/auth.guard'; - const publicRoutes: Routes = [ { path: '', - loadComponent: () => import('./app.component').then((m) => m.AppComponent), + loadComponent: () => + import('./pages/home-root/home-root.component').then( + (m) => m.HomeComponent + ), }, { path: 'signup', @@ -30,7 +31,7 @@ const protectedRoutes: Routes = [ import('./pages/dashboard-root/dashboard-root.component').then( (m) => m.DashboardRootComponent ), - canActivate: [AuthGuard], + canActivate: [], }, ]; diff --git a/frontend/src/app/config/api-configuration.ts b/frontend/src/app/config/api-configuration.ts new file mode 100644 index 0000000..e99c72b --- /dev/null +++ b/frontend/src/app/config/api-configuration.ts @@ -0,0 +1,9 @@ +import { Configuration, ConfigurationParameters } from '../api'; + +export class ApiConfiguration extends Configuration { + public constructor(params?: Partial) { + super({ + ...params, + }); + } +} diff --git a/frontend/src/app/pages/home-root/home-root.component.html b/frontend/src/app/pages/home-root/home-root.component.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/home-root/home-root.component.scss b/frontend/src/app/pages/home-root/home-root.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/home-root/home-root.component.ts b/frontend/src/app/pages/home-root/home-root.component.ts new file mode 100644 index 0000000..535f2c4 --- /dev/null +++ b/frontend/src/app/pages/home-root/home-root.component.ts @@ -0,0 +1,38 @@ +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 }, + }); + } + } + ); + } +} diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 0e3bc6d..2c37a02 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -1,94 +1,110 @@
-

Hi, Welcome to Ticket App.

+ @if (userSignupSuccess()) { +
+

Danke für deine Registrierung!

+

+ Wir haben dir eine Mail geschickt an + {{ form?.get('email')?.value }}. Bitte bestätige deine + E-Mail-Adresse um fortzufahren. +

+

Du kannst diesen Tab nun schließen

+
+ } @else { +
+

Hi, Welcome to Ticket App.

+
+ }
-
-

- @if (isSignupSignal()) { - Anmelden - } @else if (isRegisterSignal()) { - Registrieren - } @else { - Erste Schritte - } -

- @if (isDisplayButtons()) { -
- - -
- } - - @if (isSignupSignal() || isRegisterSignal()) { -
- @if (form) { -
-
-
- -
- -
-
-
- -
- -
- @if (isRegisterSignal()) { -
- -
- } - - -
+ @if (!userSignupSuccess()) { +
+

+ @if (isSignupSignal()) { + Anmelden + } @else if (isRegisterSignal()) { + Registrieren + } @else { + Erste Schritte } -

- } -
+ + + @if (isDisplayButtons()) { +
+ + +
+ } + @if (isSignupSignal() || isRegisterSignal()) { +
+ @if (form) { +
+
+
+ +
+ +
+
+
+ +
+ +
+ @if (isRegisterSignal()) { +
+ +
+ } + + +
+ } +
+ } +
+ }
diff --git a/frontend/src/app/pages/register-root/register-root.component.scss b/frontend/src/app/pages/register-root/register-root.component.scss index 88633cf..ed1f330 100644 --- a/frontend/src/app/pages/register-root/register-root.component.scss +++ b/frontend/src/app/pages/register-root/register-root.component.scss @@ -12,10 +12,20 @@ display: flex; align-items: center; - h1 { - font-size: 4em; - margin-left: 1em; + .success { + margin-left: 4em; + h1 { + font-size: 4em; + } } + + .headline { + h1 { + font-size: 4em; + margin-left: 1em; + } + } + } } diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index f70ee29..67bddda 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -25,8 +25,14 @@ import { CheckboxModule } from 'primeng/checkbox'; import { InputTextModule } from 'primeng/inputtext'; import { PasswordModule } from 'primeng/password'; -import { AuthService } from '../../shared/service'; -import { LoginCredentials } from '../../shared/types'; +import { + Configuration, + SigninResponseDtoApiModel, + SuccessDtoApiModel, + UserCredentialsDtoApiModel, +} from '../../api'; +import { ApiConfiguration } from '../../config/api-configuration'; +import { AuthService, SessionStorageService } from '../../shared/service'; import { customEmailValidator, customPasswordValidator, @@ -47,13 +53,20 @@ type AuthAction = 'register' | 'signup'; PasswordModule, HttpClientModule, ], - providers: [], + providers: [ + { + provide: Configuration, + useFactory: (): unknown => + new ApiConfiguration({ withCredentials: true }), + }, + ], templateUrl: './register-root.component.html', styleUrl: './register-root.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegisterRootComponent implements OnInit { public verified: InputSignal = input(false); + public login: InputSignal = input(false); public email: InputSignal = input(''); public form: FormGroup | undefined; public isRegisterSignal: WritableSignal = signal(false); @@ -62,12 +75,14 @@ export class RegisterRootComponent implements OnInit { public emailInvalid: WritableSignal = signal(null); public passwordInvalid: WritableSignal = signal(null); public termsInvalid: WritableSignal = signal(null); + public userSignupSuccess: WritableSignal = signal(false); private removeQueryParams: WritableSignal = signal(false); public constructor( private readonly formBuilder: FormBuilder, private readonly authService: AuthService, - private readonly router: Router + private readonly router: Router, + private readonly sessionStorageService: SessionStorageService ) { effect(() => { if (this.form) { @@ -90,13 +105,22 @@ export class RegisterRootComponent implements OnInit { public ngOnInit(): void { this.initializeForm(); this.setupValueChanges(); + this.preselectForm(); - if (this.email() || this.verified()) { + if ((this.email() && this.verified()) || this.login()) { this.handleRedirect(); 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 { if (action === 'register') { this.isRegisterSignal.set(true); @@ -113,7 +137,7 @@ export class RegisterRootComponent implements OnInit { if (this.form?.valid) { if (this.isRegisterSignal()) { - this.register(this.form.value); + this.signup(this.form.value); } else { this.signin(this.form.value); } @@ -150,7 +174,6 @@ export class RegisterRootComponent implements OnInit { } private handleRedirect(): void { - console.log('handleRedirect'); if (this.verified()) { this.isDisplayButtons.set(false); this.isRegisterSignal.set(false); @@ -159,6 +182,12 @@ export class RegisterRootComponent implements OnInit { if (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 { @@ -237,11 +266,23 @@ export class RegisterRootComponent implements OnInit { } } - private signin(logiCredentials: LoginCredentials): void { - this.authService.signin(logiCredentials); + private signin(logiCredentials: UserCredentialsDtoApiModel): void { + this.authService + .signin(logiCredentials) + .subscribe((response: SigninResponseDtoApiModel) => { + if (response) { + this.router.navigate(['/dashboard']); + } + }); } - private register(logiCredentials: LoginCredentials): void { - this.authService.signup(logiCredentials); + private signup(logiCredentials: UserCredentialsDtoApiModel): void { + this.authService + .signup(logiCredentials) + .subscribe((response: SuccessDtoApiModel) => { + if (response.success) { + this.userSignupSuccess.set(true); + } + }); } } diff --git a/frontend/src/app/shared/guard/auth.guard.ts b/frontend/src/app/shared/guard/auth.guard.ts deleted file mode 100644 index 36fb5f3..0000000 --- a/frontend/src/app/shared/guard/auth.guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 - | Promise - | boolean - | UrlTree => { - const authService: AuthService = inject(AuthService); - const router: Router = inject(Router); - - authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => { - if (!isAuthenticated) { - router.navigateByUrl('signup'); - } - }); - - return true; -}; diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts deleted file mode 100644 index 00d0384..0000000 --- a/frontend/src/app/shared/interceptors/auth.interceptor.ts +++ /dev/null @@ -1,79 +0,0 @@ -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, - next: HttpHandlerFn -): Observable> => { - const router = inject(Router); - const authService = inject(AuthService); - - const handleRequest = ( - req: HttpRequest - ): Observable> => { - const accessToken = authService.access_token; - - if (accessToken) { - req = addAuthHeader(req, accessToken); - } - return next(req); - }; - - const addAuthHeader = ( - req: HttpRequest, - token: string - ): HttpRequest => { - return req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); - }; - - const handle401Error = ( - req: HttpRequest - ): Observable> => { - 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 - ): Observable> => { - if (error.status === 401) { - return handle401Error(req); - } - return throwError(() => new Error('Unhandled error')); - }; - - return handleRequest(request).pipe( - catchError((error) => handleError(error, request)) - ); -}; diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index 381fed3..2f9b247 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -1,12 +1,16 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, tap } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { + SigninResponseDtoApiModel, + UserCredentialsDtoApiModel, +} from '../../api'; import { AuthenticationApiService } from '../../api/api/authentication.api.service'; -import { LoginCredentials, Tokens } from '../types'; -import { LocalStorageService } from './local-storage.service'; -import { SessionStorageService } from './session-storage.service'; +type SuccessResponse = { + success: boolean; +}; @Injectable({ providedIn: 'root', @@ -14,73 +18,28 @@ import { SessionStorageService } from './session-storage.service'; export class AuthService { public isAuthenticated$: BehaviorSubject = new BehaviorSubject(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( - private readonly localStorageService: LocalStorageService, - private readonly sessionStorageService: SessionStorageService, private readonly authenticationApiService: AuthenticationApiService - ) { - this._access_token = - this.localStorageService.getItem('access_token'); - this._refresh_token = - this.sessionStorageService.getItem('refresh_token'); + ) {} + + public signup( + credentials: UserCredentialsDtoApiModel + ): Observable { + return this.authenticationApiService.authControllerSignup(credentials); } - public signin(credentials: LoginCredentials): void { - this.authenticationApiService - .authControllerSignin(credentials) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + public signin( + credentials: UserCredentialsDtoApiModel + ): Observable { + return this.authenticationApiService.authControllerSignin(credentials); } - public signup(credentials: LoginCredentials): void { - this.authenticationApiService - .authControllerSignup(credentials) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + public signout(): Observable { + return this.authenticationApiService.authControllerSignout(); } - public refreshToken(): Observable { - 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); + public status(): Observable { + return this.authenticationApiService.authControllerStatus(); } } diff --git a/frontend/src/app/shared/types/index.ts b/frontend/src/app/shared/types/index.ts index c95db91..e69de29 100644 --- a/frontend/src/app/shared/types/index.ts +++ b/frontend/src/app/shared/types/index.ts @@ -1,2 +0,0 @@ -export * from './login-credentials'; -export * from './tokens'; diff --git a/frontend/src/app/shared/types/login-credentials.ts b/frontend/src/app/shared/types/login-credentials.ts deleted file mode 100644 index e37f277..0000000 --- a/frontend/src/app/shared/types/login-credentials.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type LoginCredentials = { - email: string; - password: string; -}; diff --git a/frontend/src/app/shared/types/tokens.ts b/frontend/src/app/shared/types/tokens.ts deleted file mode 100644 index 1c0a510..0000000 --- a/frontend/src/app/shared/types/tokens.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Tokens = { - access_token: string; - refresh_token: string; -};