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 { 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(
|
||||
|
||||
@@ -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},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -15,4 +15,15 @@
|
||||
}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.backdropBackground {
|
||||
background-color: rgba(0, 0, 0, 0.75) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
|
||||
Reference in New Issue
Block a user