diff --git a/src/app/app.component.html b/src/app/app.component.html index d1b27e6..d324a65 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,40 +1 @@ -@if (this.isLoggedIn === null) { - - -} @else { -
-
- - -
- -
-
- -
-@if (sharedService.isNewContactDialogOpen) { - -} @if (sharedService.isEditContactDialogOpen) { - -} @if (sharedService.isDeleteContactDialogOpen) { - -} - -} + diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 37d9f9f..e69de29 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -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; - } -} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 35e73d0..05cffae 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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 || ''; - } } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 9da3b6e..41d58d9 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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 }, + ], }, ]; diff --git a/src/app/auth.guard.spec.ts b/src/app/auth.guard.spec.ts deleted file mode 100644 index 4ae275e..0000000 --- a/src/app/auth.guard.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts index 6973b44..ce9ab51 100644 --- a/src/app/components/login/login.component.ts +++ b/src/app/components/login/login.component.ts @@ -46,6 +46,9 @@ export class LoginComponent { ) {} ngOnInit() { + if (this.loginService.checkAuthUser()) { + this.router.navigate(['/summary']); + } this.routeParams(); } diff --git a/src/app/components/main-layout/main-layout.component.html b/src/app/components/main-layout/main-layout.component.html new file mode 100644 index 0000000..2a0e3e4 --- /dev/null +++ b/src/app/components/main-layout/main-layout.component.html @@ -0,0 +1,34 @@ +
+
+ + +
+ +
+
+ +
+@if (sharedService.isNewContactDialogOpen) { + +} @if (sharedService.isEditContactDialogOpen) { + +} @if (sharedService.isDeleteContactDialogOpen) { + +} diff --git a/src/app/components/main-layout/main-layout.component.scss b/src/app/components/main-layout/main-layout.component.scss new file mode 100644 index 0000000..37d9f9f --- /dev/null +++ b/src/app/components/main-layout/main-layout.component.scss @@ -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; + } +} diff --git a/src/app/components/main-layout/main-layout.component.spec.ts b/src/app/components/main-layout/main-layout.component.spec.ts new file mode 100644 index 0000000..c577470 --- /dev/null +++ b/src/app/components/main-layout/main-layout.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MainLayoutComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MainLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/main-layout/main-layout.component.ts b/src/app/components/main-layout/main-layout.component.ts new file mode 100644 index 0000000..0deaaa0 --- /dev/null +++ b/src/app/components/main-layout/main-layout.component.ts @@ -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 || ''; + } +} diff --git a/src/app/guards/authenticated.guard.ts b/src/app/guards/authenticated.guard.ts new file mode 100644 index 0000000..6c1fe01 --- /dev/null +++ b/src/app/guards/authenticated.guard.ts @@ -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 | Promise | 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); + }) + ); + } +} diff --git a/src/app/auth.guard.ts b/src/app/guards/redirect-if-authenticated.guard.ts similarity index 52% rename from src/app/auth.guard.ts rename to src/app/guards/redirect-if-authenticated.guard.ts index 104699a..3ddb4e0 100644 --- a/src/app/auth.guard.ts +++ b/src/app/guards/redirect-if-authenticated.guard.ts @@ -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 | Promise | 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); }) ); diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 5479199..bbdfd1d 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -50,8 +50,16 @@ export class LoginService { */ checkAuthUser(): Observable { return this.firebaseService.getAuthUser().pipe( - map((user) => !!user), - catchError(() => of(false)) + map((user) => { + if (user) { + return true; + } else { + return false; + } + }), + catchError(() => { + return of(false); + }) ); }