feat: update Auth Guards and create main layout to simplify app.component

This commit is contained in:
Chneemann 2025-03-23 16:53:39 +01:00
parent 876aadcd05
commit 4b795ae977
13 changed files with 319 additions and 274 deletions

View file

@ -1,40 +1 @@
@if (this.isLoggedIn === null) {
<router-outlet></router-outlet>
<app-overlay></app-overlay>
} @else {
<div
[ngClass]="{
'blur-background': sharedService.isAnyDialogOpen
}"
>
<div class="container">
<app-sidebar></app-sidebar>
<app-header></app-header>
<div class="main-content">
<router-outlet></router-outlet>
</div>
</div>
<app-sidebar-mobile></app-sidebar-mobile>
</div>
@if (sharedService.isNewContactDialogOpen) {
<app-contact-edit
[ngClass]="{
'blur-background': sharedService.isDeleteContactDialogOpen,
'd-none': sharedService.isDeleteContactDialogOpen
}"
></app-contact-edit>
} @if (sharedService.isEditContactDialogOpen) {
<app-contact-edit
[currentUserId]="sharedService.currentUserId"
[ngClass]="{
'blur-background': sharedService.isDeleteContactDialogOpen,
'd-none': sharedService.isDeleteContactDialogOpen
}"
></app-contact-edit>
} @if (sharedService.isDeleteContactDialogOpen) {
<app-contact-delete
[currentUserId]="sharedService.currentUserId"
></app-contact-delete>
}
<app-overlay></app-overlay>
}
<router-outlet></router-outlet> <app-overlay></app-overlay>

View file

@ -1,80 +0,0 @@
.container {
display: flex;
}
app-header {
position: fixed;
top: 0;
left: 232px;
right: 0;
height: 96px;
width: 100%;
background-color: var(--white);
z-index: 2;
}
app-sidebar {
width: 232px;
height: 100vh;
}
app-sidebar-mobile {
display: none;
position: fixed;
bottom: 0;
width: 100%;
height: 80px;
}
.main-content {
position: fixed;
top: 96px;
left: 232px;
bottom: 0;
right: 0;
padding: 64px;
background-color: var(--bgContent);
overflow-y: auto;
z-index: 1;
}
.blur-background {
filter: blur(5px);
overflow: hidden;
}
.d-none {
display: none;
}
@media screen and (max-width: 910px) {
.main-content {
padding: 32px;
left: 0;
bottom: 80px;
}
app-header {
left: 0;
}
app-sidebar-mobile {
display: unset;
}
}
@media screen and (max-width: 700px) {
.main-content {
top: 80px;
}
app-header {
height: 80px;
}
}
@media screen and (max-width: 450px) {
.main-content {
padding: 16px;
}
}

View file

@ -1,104 +1,14 @@
import { Component } from '@angular/core';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { HeaderComponent } from './shared/components/header/header.component';
import { SidebarComponent } from './shared/components/sidebar/sidebar.component';
import { LanguageService } from './services/language.service';
import { SidebarMobileComponent } from './shared/components/sidebar/sidebar-mobile/sidebar-mobile.component';
import { ContactEditNewComponent } from './components/contacts/contact-edit-new/contact-edit-new.component';
import { CommonModule } from '@angular/common';
import { SharedService } from './services/shared.service';
import { ContactDeleteComponent } from './components/contacts/contact-delete/contact-delete.component';
import { RouterOutlet } from '@angular/router';
import { OverlayComponent } from './shared/components/overlay/overlay.component';
import { FirebaseService } from './services/firebase.service';
import { filter } from 'rxjs';
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
HeaderComponent,
SidebarComponent,
SidebarMobileComponent,
ContactEditNewComponent,
ContactDeleteComponent,
CommonModule,
OverlayComponent,
],
imports: [RouterOutlet, OverlayComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'join';
isLoggedIn: string = '';
constructor(
public langService: LanguageService,
public sharedService: SharedService,
private firebaseService: FirebaseService,
private router: Router
) {
this.checkAndClearLocalStorage();
this.isLoggedIn = this.firebaseService.getCurrentUserId();
}
/**
* Lifecycle hook that is called after the component has been initialized.
*
* Checks if the user is logged in by verifying the `isLoggedIn` property.
*/
ngOnInit() {
if (this.isLoggedIn === null) {
this.checkPwResetRoute();
} else {
this.router.navigate(['/summary']);
this.sharedService.isBtnDisabled = false;
}
}
/**
* Clears the local storage after a certain time period (12h) has
* passed since the last login.
*/
checkAndClearLocalStorage() {
const startTime = localStorage.getItem('sessionTimeJOIN');
if (startTime) {
const startTimeMills = parseInt(startTime);
const currentTime = new Date().getTime();
const timeDifference = 12 * 60 * 60 * 1000; // 12h
if (currentTime - startTimeMills > timeDifference) {
localStorage.clear();
}
}
}
/**
* Listens to router events and checks if the current route is one of the
* allowed routes for password reset, registration, forgotten password,
* or login. If it is not, it navigates to the login page.
*/
checkPwResetRoute() {
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe(() => {
const urlTree = this.router.parseUrl(this.router.url);
const firstSegment = this.getFirstSegment(urlTree);
const allowedRoutes = ['pw-reset', 'register', 'forgot-pw', 'login'];
if (!allowedRoutes.includes(firstSegment)) {
this.router.navigate(['/login']);
}
});
}
/**
* Returns the first segment of the current URL or an empty string if it does not exist.
*
* @param urlTree The current URL tree.
* @returns The first segment of the current URL or an empty string if it does not exist.
*/
private getFirstSegment(urlTree: any): string {
return urlTree.root.children['primary']?.segments[0]?.path || '';
}
}

View file

@ -12,41 +12,42 @@ import { LoginComponent } from './components/login/login.component';
import { RegisterComponent } from './components/login/register/register.component';
import { ForgotPwComponent } from './components/login/forgot-pw/forgot-pw.component';
import { PwResetComponent } from './components/login/forgot-pw/pw-reset/pw-reset.component';
import { AuthGuard } from './auth.guard';
import { AuthenticatedGuard } from './guards/authenticated.guard';
import { RedirectIfAuthenticatedGuard } from './guards/redirect-if-authenticated.guard';
import { MainLayoutComponent } from './components/main-layout/main-layout.component';
export const routes: Routes = [
{ path: '', component: LoginComponent },
{ path: 'login', component: LoginComponent },
{ path: 'login/notice/:id', component: LoginComponent },
{ path: 'login/imprint', component: ImprintComponent },
{ path: 'login/privacy-policy', component: PrivacyPolicyComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'forgot-pw', component: ForgotPwComponent },
{ path: 'pw-reset', component: PwResetComponent },
{ path: 'summary', component: SummaryComponent, canActivate: [AuthGuard] },
{ path: 'add-task', component: AddTaskComponent, canActivate: [AuthGuard] },
// Parent route for all pages that do not require authenticated access
{
path: 'add-task/:id',
component: AddTaskComponent,
canActivate: [AuthGuard],
path: '',
canActivate: [RedirectIfAuthenticatedGuard],
children: [
{ path: '', component: LoginComponent, pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'login/notice/:id', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'forgot-pw', component: ForgotPwComponent },
{ path: 'pw-reset', component: PwResetComponent },
{ path: 'login/imprint', component: ImprintComponent },
{ path: 'login/privacy-policy', component: PrivacyPolicyComponent },
],
},
{ path: 'board', component: BoardComponent, canActivate: [AuthGuard] },
// Protected routes with MainLayout
{
path: 'task/:id',
component: TaskOverlayComponent,
canActivate: [AuthGuard],
},
{
path: 'task-edit/:id',
component: TaskEditOverlayComponent,
canActivate: [AuthGuard],
},
{ path: 'contacts', component: ContactsComponent, canActivate: [AuthGuard] },
{ path: 'help', component: HelpComponent, canActivate: [AuthGuard] },
{ path: 'imprint', component: ImprintComponent, canActivate: [AuthGuard] },
{
path: 'privacy-policy',
component: PrivacyPolicyComponent,
canActivate: [AuthGuard],
path: '',
component: MainLayoutComponent,
canActivate: [AuthenticatedGuard],
children: [
{ path: 'summary', component: SummaryComponent },
{ path: 'add-task', component: AddTaskComponent },
{ path: 'add-task/:id', component: AddTaskComponent },
{ path: 'board', component: BoardComponent },
{ path: 'task/:id', component: TaskOverlayComponent },
{ path: 'task-edit/:id', component: TaskEditOverlayComponent },
{ path: 'contacts', component: ContactsComponent },
{ path: 'help', component: HelpComponent },
{ path: 'imprint', component: ImprintComponent },
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
],
},
];

View file

@ -1,17 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import { authGuard } from './auth.guard';
describe('authGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

View file

@ -46,6 +46,9 @@ export class LoginComponent {
) {}
ngOnInit() {
if (this.loginService.checkAuthUser()) {
this.router.navigate(['/summary']);
}
this.routeParams();
}

View file

@ -0,0 +1,34 @@
<div
[ngClass]="{
'blur-background': sharedService.isAnyDialogOpen
}"
>
<div class="container">
<app-sidebar></app-sidebar>
<app-header></app-header>
<div class="main-content">
<router-outlet></router-outlet>
</div>
</div>
<app-sidebar-mobile></app-sidebar-mobile>
</div>
@if (sharedService.isNewContactDialogOpen) {
<app-contact-edit
[ngClass]="{
'blur-background': sharedService.isDeleteContactDialogOpen,
'd-none': sharedService.isDeleteContactDialogOpen
}"
></app-contact-edit>
} @if (sharedService.isEditContactDialogOpen) {
<app-contact-edit
[currentUserId]="sharedService.currentUserId"
[ngClass]="{
'blur-background': sharedService.isDeleteContactDialogOpen,
'd-none': sharedService.isDeleteContactDialogOpen
}"
></app-contact-edit>
} @if (sharedService.isDeleteContactDialogOpen) {
<app-contact-delete
[currentUserId]="sharedService.currentUserId"
></app-contact-delete>
}

View file

@ -0,0 +1,80 @@
.container {
display: flex;
}
app-header {
position: fixed;
top: 0;
left: 232px;
right: 0;
height: 96px;
width: 100%;
background-color: var(--white);
z-index: 2;
}
app-sidebar {
width: 232px;
height: 100vh;
}
app-sidebar-mobile {
display: none;
position: fixed;
bottom: 0;
width: 100%;
height: 80px;
}
.main-content {
position: fixed;
top: 96px;
left: 232px;
bottom: 0;
right: 0;
padding: 64px;
background-color: var(--bgContent);
overflow-y: auto;
z-index: 1;
}
.blur-background {
filter: blur(5px);
overflow: hidden;
}
.d-none {
display: none;
}
@media screen and (max-width: 910px) {
.main-content {
padding: 32px;
left: 0;
bottom: 80px;
}
app-header {
left: 0;
}
app-sidebar-mobile {
display: unset;
}
}
@media screen and (max-width: 700px) {
.main-content {
top: 80px;
}
app-header {
height: 80px;
}
}
@media screen and (max-width: 450px) {
.main-content {
padding: 16px;
}
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MainLayoutComponent } from './main-layout.component';
describe('MainLayoutComponent', () => {
let component: MainLayoutComponent;
let fixture: ComponentFixture<MainLayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MainLayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MainLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,101 @@
import { Component } from '@angular/core';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
import { filter } from 'rxjs';
import { FirebaseService } from '../../services/firebase.service';
import { LanguageService } from '../../services/language.service';
import { SharedService } from '../../services/shared.service';
import { SidebarMobileComponent } from '../../shared/components/sidebar/sidebar-mobile/sidebar-mobile.component';
import { SidebarComponent } from '../../shared/components/sidebar/sidebar.component';
import { ContactDeleteComponent } from '../contacts/contact-delete/contact-delete.component';
import { ContactEditNewComponent } from '../contacts/contact-edit-new/contact-edit-new.component';
import { HeaderComponent } from '../../shared/components/header/header.component';
@Component({
selector: 'app-main-layout',
standalone: true,
imports: [
RouterOutlet,
HeaderComponent,
SidebarComponent,
SidebarMobileComponent,
ContactEditNewComponent,
ContactDeleteComponent,
CommonModule,
],
templateUrl: './main-layout.component.html',
styleUrl: './main-layout.component.scss',
})
export class MainLayoutComponent {
isLoggedIn: string = '';
constructor(
public langService: LanguageService,
public sharedService: SharedService,
private firebaseService: FirebaseService,
private router: Router
) {
this.checkAndClearLocalStorage();
this.isLoggedIn = this.firebaseService.getCurrentUserId();
}
/**
* Lifecycle hook that is called after the component has been initialized.
*
* Checks if the user is logged in by verifying the `isLoggedIn` property.
*/
ngOnInit() {
if (this.isLoggedIn === null) {
this.checkPwResetRoute();
} else {
this.router.navigate(['/summary']);
this.sharedService.isBtnDisabled = false;
}
}
/**
* Clears the local storage after a certain time period (12h) has
* passed since the last login.
*/
checkAndClearLocalStorage() {
const startTime = localStorage.getItem('sessionTimeJOIN');
if (startTime) {
const startTimeMills = parseInt(startTime);
const currentTime = new Date().getTime();
const timeDifference = 12 * 60 * 60 * 1000; // 12h
if (currentTime - startTimeMills > timeDifference) {
localStorage.clear();
}
}
}
/**
* Listens to router events and checks if the current route is one of the
* allowed routes for password reset, registration, forgotten password,
* or login. If it is not, it navigates to the login page.
*/
checkPwResetRoute() {
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe(() => {
const urlTree = this.router.parseUrl(this.router.url);
const firstSegment = this.getFirstSegment(urlTree);
const allowedRoutes = ['pw-reset', 'register', 'forgot-pw', 'login'];
if (!allowedRoutes.includes(firstSegment)) {
this.router.navigate(['/login']);
}
});
}
/**
* Returns the first segment of the current URL or an empty string if it does not exist.
*
* @param urlTree The current URL tree.
* @returns The first segment of the current URL or an empty string if it does not exist.
*/
private getFirstSegment(urlTree: any): string {
return urlTree.root.children['primary']?.segments[0]?.path || '';
}
}

View file

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, map, Observable, of } from 'rxjs';
import { LoginService } from '../services/auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthenticatedGuard {
constructor(private loginService: LoginService, private router: Router) {}
canActivate(): Observable<boolean> | Promise<boolean> | boolean {
return this.loginService.checkAuthUser().pipe(
map((isAuthenticated) => {
if (isAuthenticated) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}),
catchError(() => {
this.router.navigate(['/login']);
return of(false);
})
);
}
}

View file

@ -2,32 +2,25 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { LoginService } from './services/auth.service';
import { LoginService } from '../services/auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard {
export class RedirectIfAuthenticatedGuard {
constructor(private loginService: LoginService, private router: Router) {}
/**
* The canActivate guard checks if the user is authenticated.
* If the user is authenticated, the guard returns true.
* If the user is not authenticated, the guard navigates to the root route and returns false.
* If an error occurs during the authentication check, the guard navigates to the root route and returns false.
*/
canActivate(): Observable<boolean> | Promise<boolean> | boolean {
return this.loginService.checkAuthUser().pipe(
map((isAuthenticated) => {
if (isAuthenticated) {
return true;
} else {
this.router.navigate(['/']);
this.router.navigate(['/summary']);
return false;
}
return true;
}),
catchError(() => {
this.router.navigate(['/']);
this.router.navigate(['/login']);
return of(false);
})
);

View file

@ -50,8 +50,16 @@ export class LoginService {
*/
checkAuthUser(): Observable<boolean> {
return this.firebaseService.getAuthUser().pipe(
map((user) => !!user),
catchError(() => of(false))
map((user) => {
if (user) {
return true;
} else {
return false;
}
}),
catchError(() => {
return of(false);
})
);
}