diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 751011c..c11da25 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -18,6 +18,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatButtonModule, MatIconButton } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; +import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog"; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; @@ -46,6 +47,7 @@ import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search import { MovieImporterComponent } from './movie-importer/movie-importer.component'; import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component'; import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component'; +import { LoginDialog } from './login/login.dialog'; @NgModule({ @@ -77,6 +79,7 @@ import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie MovieImporterComponent, MovieImportNoSearchResultComponent, MovieImportSearchInfoComponent, + LoginDialog, ], imports: [ AppRoutingModule, @@ -94,8 +97,12 @@ import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie MatFormFieldModule, MatIconButton, MatDividerModule, - MatButtonModule - ], + MatButtonModule, + MatDialogClose, + MatDialogTitle, + MatDialogContent, + MatDialogActions +], providers: [ provideBrowserGlobalErrorListeners(), provideHttpClient( diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index 4aaacf6..d22e9df 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -7,10 +7,11 @@ import { MainComponent } from './main/main.component'; import { ScheduleComponent } from './schedule/schedule.component'; import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component'; import { MovieImporterComponent } from './movie-importer/movie-importer.component'; +import { AuthGuard } from './auth.guard'; const routes: Routes = [ // Seiten ohne Layout - { path: 'info', component: HomeComponent }, + { path: 'landing', component: HomeComponent }, { path: 'poc-model', component: PocModelComponent }, // Seiten mit MainLayout @@ -20,8 +21,13 @@ const routes: Routes = [ children: [ { path: '', component: MainComponent }, { path: 'schedule', component: ScheduleComponent }, - { path: 'admin/movie-importer', component: MovieImporterComponent }, - { path: 'selection/performance/:id', component: TheaterOverlayComponent}, //? + { + path: 'admin/movie-importer', + component: MovieImporterComponent, + canActivate: [AuthGuard], + data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee' + }, + { path: 'selection/performance/:id', component: TheaterOverlayComponent}, ], }, diff --git a/src/app/auth.guard.ts b/src/app/auth.guard.ts new file mode 100644 index 0000000..17f490b --- /dev/null +++ b/src/app/auth.guard.ts @@ -0,0 +1,43 @@ +import { AuthService, User, UserRole } from './auth.service'; +import { inject, Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { LoginDialog } from './login/login.dialog'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class AuthGuard implements CanActivate { + + private auth = inject(AuthService); + private dialog = inject(MatDialog); + + async canActivate(route: ActivatedRouteSnapshot): Promise { + const allowedRoles: UserRole[] = route.data['roles']; + + if (!allowedRoles || allowedRoles.length === 0) { + throw new Error('Keine erlaubten Rollen für diese Route definiert.'); + } + + const currentUser = this.auth.user() + if (currentUser && allowedRoles.includes(currentUser.role)) { + return true; + } + + const roleToLogin = allowedRoles[0]; // Standardmäßig erste Rolle auswählen + + const user: User | null = this.auth.getUserDataByRole(roleToLogin); + if (!user) { + throw new Error(`Ungültige Rolle: ${roleToLogin}`); + } + + const dialogRef = this.dialog.open(LoginDialog, { + disableClose: true, + backdropClass: 'backdropBackground', + data: { user }, + }); + + const result = await firstValueFrom(dialogRef.afterClosed()); + + return result === true + } +} diff --git a/src/app/auth.service.ts b/src/app/auth.service.ts new file mode 100644 index 0000000..42678c4 --- /dev/null +++ b/src/app/auth.service.ts @@ -0,0 +1,63 @@ +import { Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; + +export type UserRole = 'admin' | 'employee'; + +export interface User { + role: UserRole; + displayName: string; +} + + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private readonly SESSION_KEY = 'userRole'; + + private readonly USERS: Record = { + admin: { password: 'admin123', displayName: 'Admin' }, + employee: { password: 'employee123', displayName: 'Mitarbeiter' }, + }; + + user = signal(this.loadUserFromSession()); + + constructor(private router: Router) {} + + private loadUserFromSession(): User | null { + const stored = sessionStorage.getItem(this.SESSION_KEY); + return stored ? JSON.parse(stored) as User : null; + } + + login(role: UserRole, password: string): boolean { + const user = this.USERS[role]; + if (!user) { + return false + }; + + if (password === user.password) { + const userObj: User = { role, displayName: user.displayName }; + sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(userObj)); + this.user.set(userObj); + return true; + } + return false; + } + + logout() { + sessionStorage.removeItem(this.SESSION_KEY); + this.user.set(null); + this.router.navigate(['/']); + } + + getUserDataByRole(role: UserRole): User | null { + const userDef = this.USERS[role]; + return userDef ? { role, displayName: userDef.displayName } : null; + } + + isLoggedIn(role?: UserRole): boolean { + const current = this.user(); + if (!current) return false; + return role ? current.role === role : true; + } +} diff --git a/src/app/login/login.dialog.css b/src/app/login/login.dialog.css new file mode 100644 index 0000000..f785beb --- /dev/null +++ b/src/app/login/login.dialog.css @@ -0,0 +1,3 @@ +button { + min-width: 100px; +} diff --git a/src/app/login/login.dialog.html b/src/app/login/login.dialog.html new file mode 100644 index 0000000..b67584f --- /dev/null +++ b/src/app/login/login.dialog.html @@ -0,0 +1,31 @@ +
+

{{ data.user.displayName }} Login

+ Logo +
+ + +

Bitte {{ data.user.displayName }}-Passwort eingeben:

+ + + Passwort + + @if (passwordControl.hasError('required')) { + Passwort ist erforderlich. + } + @else if (passwordControl.hasError('wrongPassword')) { + Falsches Passwort. Bitte erneut versuchen. + } + + +
+
+ + + + + diff --git a/src/app/login/login.dialog.ts b/src/app/login/login.dialog.ts new file mode 100644 index 0000000..48b7f1d --- /dev/null +++ b/src/app/login/login.dialog.ts @@ -0,0 +1,40 @@ +import { Component, Inject, inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { FormControl, Validators } from '@angular/forms'; +import { AuthService, User } from '../auth.service'; + +@Component({ + selector: 'app-login', + standalone: false, + templateUrl: './login.dialog.html', + styleUrls: ['./login.dialog.css'], +}) +export class LoginDialog { + auth = inject(AuthService); + passwordControl = new FormControl('', Validators.required); + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { user: User } + ) {} + + submit(): void { + const role = this.data.user.role; + + if (!this.passwordControl.value) { + this.passwordControl.setErrors({ required: true }); + this.passwordControl.markAsTouched(); + return; + } + if (!this.auth.login(role, this.passwordControl.value)) { + this.passwordControl.setErrors({ wrongPassword: true }); + this.passwordControl.markAsTouched(); + return; + } + this.dialogRef.close(true); + } + + cancel(): void { + this.dialogRef.close(false); + } +} diff --git a/src/app/navbar/navbar.component.css b/src/app/navbar/navbar.component.css index 7d41e7d..e63a669 100644 --- a/src/app/navbar/navbar.component.css +++ b/src/app/navbar/navbar.component.css @@ -13,7 +13,7 @@ nav { } .gradient-text:hover { - background: linear-gradient(to right, #6366f1, #db2777); + background: linear-gradient(to right, #db2777, #db2777); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 75353b1..36a3db4 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -15,4 +15,15 @@ } + @if (currentUser() != null) { +
+
+ Angemeldet als: + {{ currentUser()?.displayName }} +
+ +
+ } diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 321347c..5f2de5f 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { AuthService } from './../auth.service'; +import { Component, inject, computed, OnInit } from '@angular/core'; @Component({ selector: 'app-navbar', @@ -11,4 +12,12 @@ export class NavbarComponent { {label: 'Programm', path: '/schedule'}, {label: 'Film importieren', path: '/admin/movie-importer'}, ] + + private auth = inject(AuthService) + + currentUser = computed(() => this.auth.user()); + + logout() { + this.auth.logout(); + } } diff --git a/src/custom-theme.scss b/src/custom-theme.scss index e55c90b..5615bb5 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -29,6 +29,10 @@ color: white !important; } +.backdropBackground { + background-color: rgba(0, 0, 0, 0.75) !important; + backdrop-filter: blur(2px); +} html {