Implement authentication feature with role-based access control and login dialog
This commit is contained in:
@@ -18,6 +18,7 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatButtonModule, MatIconButton } from '@angular/material/button';
|
import { MatButtonModule, MatIconButton } from '@angular/material/button';
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog";
|
||||||
|
|
||||||
import { HeaderComponent } from './header/header.component';
|
import { HeaderComponent } from './header/header.component';
|
||||||
import { HomeComponent } from './home/home.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 { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||||
import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.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 { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
|
||||||
|
import { LoginDialog } from './login/login.dialog';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -77,6 +79,7 @@ import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie
|
|||||||
MovieImporterComponent,
|
MovieImporterComponent,
|
||||||
MovieImportNoSearchResultComponent,
|
MovieImportNoSearchResultComponent,
|
||||||
MovieImportSearchInfoComponent,
|
MovieImportSearchInfoComponent,
|
||||||
|
LoginDialog,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
@@ -94,7 +97,11 @@ import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie
|
|||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconButton,
|
MatIconButton,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
MatButtonModule
|
MatButtonModule,
|
||||||
|
MatDialogClose,
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { MainComponent } from './main/main.component';
|
|||||||
import { ScheduleComponent } from './schedule/schedule.component';
|
import { ScheduleComponent } from './schedule/schedule.component';
|
||||||
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
|
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
|
||||||
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||||
|
import { AuthGuard } from './auth.guard';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
// Seiten ohne Layout
|
// Seiten ohne Layout
|
||||||
{ path: 'info', component: HomeComponent },
|
{ path: 'landing', component: HomeComponent },
|
||||||
{ path: 'poc-model', component: PocModelComponent },
|
{ path: 'poc-model', component: PocModelComponent },
|
||||||
|
|
||||||
// Seiten mit MainLayout
|
// Seiten mit MainLayout
|
||||||
@@ -20,8 +21,13 @@ const routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{ path: '', component: MainComponent },
|
{ path: '', component: MainComponent },
|
||||||
{ path: 'schedule', component: ScheduleComponent },
|
{ 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},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
43
src/app/auth.guard.ts
Normal file
43
src/app/auth.guard.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/app/auth.service.ts
Normal file
63
src/app/auth.service.ts
Normal file
@@ -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<UserRole, { password: string; displayName: string }> = {
|
||||||
|
admin: { password: 'admin123', displayName: 'Admin' },
|
||||||
|
employee: { password: 'employee123', displayName: 'Mitarbeiter' },
|
||||||
|
};
|
||||||
|
|
||||||
|
user = signal<User | null>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/app/login/login.dialog.css
Normal file
3
src/app/login/login.dialog.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
31
src/app/login/login.dialog.html
Normal file
31
src/app/login/login.dialog.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="flex justify-between">
|
||||||
|
<h2 mat-dialog-title>{{ data.user.displayName }} Login</h2>
|
||||||
|
<img class="mr-4" src="assets/logo.png" width="75" height="75" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-dialog-content class="min-w-[400px]">
|
||||||
|
<p class="text-sm text-gray-600 mb-2">Bitte {{ data.user.displayName }}-Passwort eingeben:</p>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill" class="w-full">
|
||||||
|
<mat-label>Passwort</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[formControl]="passwordControl"
|
||||||
|
type="password"
|
||||||
|
(keyup.enter)="submit()"
|
||||||
|
/>
|
||||||
|
@if (passwordControl.hasError('required')) {
|
||||||
|
<mat-error>Passwort ist erforderlich.</mat-error>
|
||||||
|
}
|
||||||
|
@else if (passwordControl.hasError('wrongPassword')) {
|
||||||
|
<mat-error>Falsches Passwort. Bitte erneut versuchen.</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="h-4"></div>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions class="justify-end gap-2">
|
||||||
|
<button mat-stroked-button color="warn" (click)="cancel()">Abbrechen</button>
|
||||||
|
<button mat-flat-button color="primary" (click)="submit()">Anmelden</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
40
src/app/login/login.dialog.ts
Normal file
40
src/app/login/login.dialog.ts
Normal file
@@ -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<LoginDialog>,
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gradient-text:hover {
|
.gradient-text:hover {
|
||||||
background: linear-gradient(to right, #6366f1, #db2777);
|
background: linear-gradient(to right, #db2777, #db2777);
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
|
|||||||
@@ -15,4 +15,15 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
@if (currentUser() != null) {
|
||||||
|
<div class="mt-auto flex items-center justify-between gap-x-2 px-3 pb-3 text-gray-600">
|
||||||
|
<div class="flex items-center gap-x-1">
|
||||||
|
<span>Angemeldet als:</span>
|
||||||
|
<span class="text-indigo-500">{{ currentUser()?.displayName }}</span>
|
||||||
|
</div>
|
||||||
|
<button (click)="logout()" class="relative top-[1px] rounded opacity-50 hover:opacity-100 text-black">
|
||||||
|
<mat-icon style="font-size: 20px; width: 20px; height: 20px;">logout</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { AuthService } from './../auth.service';
|
||||||
|
import { Component, inject, computed, OnInit } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
@@ -11,4 +12,12 @@ export class NavbarComponent {
|
|||||||
{label: 'Programm', path: '/schedule'},
|
{label: 'Programm', path: '/schedule'},
|
||||||
{label: 'Film importieren', path: '/admin/movie-importer'},
|
{label: 'Film importieren', path: '/admin/movie-importer'},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
private auth = inject(AuthService)
|
||||||
|
|
||||||
|
currentUser = computed(() => this.auth.user());
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.auth.logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backdropBackground {
|
||||||
|
background-color: rgba(0, 0, 0, 0.75) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
Reference in New Issue
Block a user