diff --git a/.idea/.idea.DotNetAngular/.idea/workspace.xml b/.idea/.idea.DotNetAngular/.idea/workspace.xml index 176ecf5..2f5cbdf 100644 --- a/.idea/.idea.DotNetAngular/.idea/workspace.xml +++ b/.idea/.idea.DotNetAngular/.idea/workspace.xml @@ -13,11 +13,41 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - @@ -69,7 +99,7 @@ "RunOnceActivity.git.unshallow": "true", "RunOnceActivity.typescript.service.memoryLimit.init": "true", "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", - "git-widget-placeholder": "feature/haeder", + "git-widget-placeholder": "feature/guards", "junie.onboarding.icon.badge.shown": "true", "node.js.detected.package.eslint": "true", "node.js.detected.package.tslint": "true", @@ -221,7 +251,9 @@ - + + + @@ -279,7 +311,15 @@ 1773598259701 - + + + 1773601314558 + + + + 1773601314558 + + @@ -322,7 +362,8 @@ - + + diff --git a/src/ClientApp/src/app/app.config.ts b/src/ClientApp/src/app/app.config.ts index a1e7d6f..87b4ef7 100644 --- a/src/ClientApp/src/app/app.config.ts +++ b/src/ClientApp/src/app/app.config.ts @@ -2,7 +2,12 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import {provideHttpClient, withFetch, withInterceptors} from "@angular/common/http"; +import {tokenInterceptor} from "./presentation/interceptors/token.interceptor"; +import {provideClientHydration, withEventReplay} from "@angular/platform-browser"; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), + provideHttpClient(withFetch(), withInterceptors([tokenInterceptor + ])), provideClientHydration(withEventReplay())] }; diff --git a/src/ClientApp/src/app/app.routes.ts b/src/ClientApp/src/app/app.routes.ts index b6cbab6..5fe5290 100644 --- a/src/ClientApp/src/app/app.routes.ts +++ b/src/ClientApp/src/app/app.routes.ts @@ -2,10 +2,21 @@ import { Routes } from '@angular/router'; import {RegisterComponent} from "./presentation/authentication/register/register.component"; import {LoginComponent} from "./presentation/authentication/login/login.component"; import {StartpageComponent} from "./presentation/components/startpage/startpage.component"; +import {AdminDashboardComponent} from "./presentation/components/admin-dashboard/admin-dashboard.component"; +import {UserDashboardComponent} from "./presentation/components/user-dashboard/user-dashboard.component"; +import {UnauthorizedComponent} from "./presentation/components/unauthorized/unauthorized.component"; +import {guestGuard} from "./presentation/guards/guest.guard"; +import {AuthenticationGuard} from "./presentation/guards/authentication.guard"; +import {adminGuard} from "./presentation/guards/admin.guard"; +import {UserTableComponent} from "./presentation/components/user-table/user-table.component"; export const routes: Routes = [ - {path: '', redirectTo: 'login', pathMatch: 'full'}, - {path: 'register', component: RegisterComponent}, - {path: 'login', component: LoginComponent}, - {path: 'startpage', component: StartpageComponent} + {path: '', redirectTo: 'startpage', pathMatch: 'full'}, + {path: 'register', component: RegisterComponent, canActivate: [guestGuard]}, + {path: 'login', component: LoginComponent, canActivate: [guestGuard]}, + {path: 'startpage', component: StartpageComponent, canActivate: [guestGuard]}, + {path: 'user-table', component: UserTableComponent, canActivate: [adminGuard]}, + {path: 'admin-dashboard', component: AdminDashboardComponent, canActivate: [adminGuard]}, + {path: 'user-dashboard', component: UserDashboardComponent, canActivate: [AuthenticationGuard]}, + {path: 'unauthorized', component: UnauthorizedComponent, canActivate: [AuthenticationGuard]} ]; diff --git a/src/ClientApp/src/app/domain/entities/user.ts b/src/ClientApp/src/app/domain/entities/user.ts index 330f3ff..dba0c2e 100644 --- a/src/ClientApp/src/app/domain/entities/user.ts +++ b/src/ClientApp/src/app/domain/entities/user.ts @@ -1,4 +1,3 @@ -import {RssFeed} from "./rss-feed"; import {UserRole} from "./user-role"; export interface User { @@ -8,9 +7,5 @@ export interface User { password: string; lastLogin: Date; - rssFeedId: number; - rssFeed: RssFeed; - UserRoles: UserRole[]; - rssFeeds: RssFeed []; } diff --git a/src/ClientApp/src/app/presentation/authentication/login/login.component.ts b/src/ClientApp/src/app/presentation/authentication/login/login.component.ts index b18df35..0feccf5 100644 --- a/src/ClientApp/src/app/presentation/authentication/login/login.component.ts +++ b/src/ClientApp/src/app/presentation/authentication/login/login.component.ts @@ -66,7 +66,8 @@ export class LoginComponent { this.userStore.setEmailForStore(tokenPayload.email); this.userStore.setRoleForStore(tokenPayload.role); this.loadingService.hide() - this.router.navigate(['startpage']).then(success => { + // TODO redirect to... + this.router.navigate(['user-dashboard']).then(success => { if (success) { this.loginForm.reset(); } diff --git a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.html b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.html new file mode 100644 index 0000000..2743f2b --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.html @@ -0,0 +1,10 @@ + + + + + + + Users + + + diff --git a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.scss b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts new file mode 100644 index 0000000..5884066 --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminDashboardComponent } from './admin-dashboard.component'; + +describe('AdminDashboardComponent', () => { + let component: AdminDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.ts b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.ts new file mode 100644 index 0000000..98b9e16 --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import {RouterLink} from "@angular/router"; + +@Component({ + selector: 'app-admin-dashboard', + imports: [ + RouterLink + ], + templateUrl: './admin-dashboard.component.html', + styleUrl: './admin-dashboard.component.scss', + standalone: true +}) +export class AdminDashboardComponent { + +} diff --git a/src/ClientApp/src/app/presentation/components/header/header.component.html b/src/ClientApp/src/app/presentation/components/header/header.component.html index 4f41f86..5f11de4 100644 --- a/src/ClientApp/src/app/presentation/components/header/header.component.html +++ b/src/ClientApp/src/app/presentation/components/header/header.component.html @@ -1,28 +1,11 @@ - @if (showOverviewTools){ - - - - - - } - - - - - - + @if (!showDropdown){ diff --git a/src/ClientApp/src/app/presentation/components/header/header.component.ts b/src/ClientApp/src/app/presentation/components/header/header.component.ts index ad1d68a..5db7458 100644 --- a/src/ClientApp/src/app/presentation/components/header/header.component.ts +++ b/src/ClientApp/src/app/presentation/components/header/header.component.ts @@ -50,8 +50,7 @@ export class HeaderComponent implements OnInit { // Header-Logic // TODO hide dropdown for login register and legal const headerRoutes = ['/login', '/register', '/legal', '/startpage']; - this.showDropdown = headerRoutes.some(route => event.urlAfterRedirects.startsWith(route)); - this.showOverviewTools = event.urlAfterRedirects.startsWith('/rss-feed-overview'); + this.showDropdown = headerRoutes.some(route => event.urlAfterRedirects.startsWith(route)) }); } diff --git a/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.html b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.html new file mode 100644 index 0000000..10fe24b --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.html @@ -0,0 +1 @@ +unauthorized works! diff --git a/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.scss b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.spec.ts b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.spec.ts new file mode 100644 index 0000000..bab5ad8 --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UnauthorizedComponent } from './unauthorized.component'; + +describe('UnauthorizedComponent', () => { + let component: UnauthorizedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnauthorizedComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UnauthorizedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.ts b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.ts new file mode 100644 index 0000000..eee1267 --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-unauthorized', + imports: [], + templateUrl: './unauthorized.component.html', + styleUrl: './unauthorized.component.scss', +}) +export class UnauthorizedComponent { + +} diff --git a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.html b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.html new file mode 100644 index 0000000..9d7a79d --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.html @@ -0,0 +1,73 @@ + + + + + + + + + + + {{username}} + + + + + + + + + + Personal Data + + + Username: {{username}} + E-Mail: {{email}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.scss b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts new file mode 100644 index 0000000..138e3af --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDashboardComponent } from './user-dashboard.component'; + +describe('UserDashboardComponent', () => { + let component: UserDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.ts b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.ts new file mode 100644 index 0000000..d9b655b --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.ts @@ -0,0 +1,38 @@ +import {Component, inject} from '@angular/core'; +import {Title} from "@angular/platform-browser"; +import {UserStoreService} from "../../../infrastructure/services/user-store.service"; +import {AuthService} from "../../../infrastructure/services/auth-service"; + +@Component({ + selector: 'app-user-dashboard', + imports: [], + templateUrl: './user-dashboard.component.html', + styleUrl: './user-dashboard.component.scss', +}) +export class UserDashboardComponent { + public username!: string; + public email!: string; + public userId!: number; + userStore = inject(UserStoreService); + private authenticateService = inject(AuthService); + title = inject(Title) + + constructor() { + //TODO App Name + this.title.setTitle('User Dashboard | App Name'); + } + ngOnInit(): void { + this.userStore.getUsernameFromStore() + .subscribe(val => { + const usernameFromToken = this.authenticateService.getUsernameFromToken(); + this.username = val || usernameFromToken + }); + + this.userStore.getEmailFromStore() + .subscribe(val => { + const emailFromToken = this.authenticateService.getEmailFromToken(); + this.email = val || emailFromToken + }); + + } +} diff --git a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.html b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.html new file mode 100644 index 0000000..212898d --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.html @@ -0,0 +1,98 @@ + + + User-List + + + + + # + + Username + + + + E-Mail + + + + Last Login + + + Action + + + + + @for (user of users; track user.id; let i = $index;) { + + {{ i + 1 }} + {{ user.username }} + {{ user.email }} + {{ user.lastLogin | date:'dd.MM.yyyy HH:mm:ss':'Europe/Berlin' }} + + + + + } + + + + + + + + + + + « + + + + @for (Page of [].constructor(totalPages); track Page ;let i = $index;) { + + + {{ i + 1 }} + + + } + + + + + » + + + + + + + Page {{ pageNumber }} of {{ totalPages }} + + + + + + Do you really want to delete + {{ selectedUser?.username }} ? + diff --git a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.scss b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts new file mode 100644 index 0000000..1e1eb4f --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserTableComponent } from './user-table.component'; + +describe('UserTableComponent', () => { + let component: UserTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserTableComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.ts b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.ts new file mode 100644 index 0000000..89e40ab --- /dev/null +++ b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.ts @@ -0,0 +1,120 @@ +import {Component, inject} from '@angular/core'; +import {User} from "../../../domain/entities/user"; +import {UserService} from "../../../infrastructure/services/user.service"; +import {ToastService} from "../../../infrastructure/services/toast.service"; +import {Title} from "@angular/platform-browser"; +import {PopupModalComponent} from "../../shared/popup-modal/popup-modal.component"; +import {DatePipe, NgClass} from "@angular/common"; + +@Component({ + selector: 'app-user-table', + imports: [ + PopupModalComponent, + DatePipe, + NgClass + ], + templateUrl: './user-table.component.html', + styleUrl: './user-table.component.scss', + standalone: true +}) +export class UserTableComponent { + public users: User[] = []; + selectedUser: User | null = null; + + pageNumber: number = 1; + pageSize: number = 10; + totalPages: number = 0; + + sortColumn: string = ''; + sortDirection: 'asc' | 'desc' = 'asc'; + + private apiService = inject(UserService) + toastService = inject(ToastService); + title = inject(Title) + + constructor() { + this.title.setTitle('User Table | RSS Feed Reader'); + } + + ngOnInit(): void { + this.getAllUsers() + } + getAllUsers(): void { + this.apiService.getAllUsers(this.pageNumber, this.pageSize).subscribe(result => { + this.users = result.value.items; + this.totalPages = result.value.totalPages; + this.sort(this.sortColumn); + }); + } + nextPage(): void { + if (this.pageNumber < this.totalPages) { + this.pageNumber++; + this.getAllUsers(); + } + } + + previousPage(): void { + if (this.pageNumber > 1) { + this.pageNumber--; + this.getAllUsers(); + } + } + goToPage(page: number): void { + if (page >= 1 && page <= this.totalPages) { + this.pageNumber = page; + this.getAllUsers(); + } + } + // Sortiermethode + sort(column: string): void { + if (this.sortColumn === column) { + // Wenn die Spalte bereits sortiert ist, wechsle die Richtung + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + // Andernfalls aufsteigend sortieren + this.sortDirection = 'asc'; + this.sortColumn = column; + } + + // Sortiere die Tabelle basierend auf Spalte und Richtung + this.users.sort((a: any, b: any) => { + const valueA = a[column]; + const valueB = b[column]; + if (valueA < valueB) { + return this.sortDirection === 'asc' ? -1 : 1; + } + if (valueA > valueB) { + return this.sortDirection === 'asc' ? 1 : -1; + } + return 0; + }); + } + + deleteUser(id: number): void { + this.apiService.deleteUser(id).subscribe({ + next: (result) => { + console.log('delete',result); + this.toastService.show(result.value, { + classname: 'bg-success text-light', + delay: 3000 + }) + this.users = this.users.filter(user => user.id !== id); + }, + error: (err) => { + console.error(err.error.error.message); + this.toastService.show(err.error.error.message, { + classname: 'bg-danger text-light', + delay: 3000 + }); + } + }); + } + + selectUser(user: User): void { + this.selectedUser = user; + console.log('selectedUser', user); + } + handleConfirm() { + this.deleteUser(this.selectedUser!.id); + } +} diff --git a/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts new file mode 100644 index 0000000..0ddaaa9 --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts @@ -0,0 +1,17 @@ +import {TestBed} from '@angular/core/testing'; +import {CanActivateFn} from '@angular/router'; + +import {adminGuard} from './admin.guard'; + +describe('adminGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => adminGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/guards/admin.guard.ts b/src/ClientApp/src/app/presentation/guards/admin.guard.ts new file mode 100644 index 0000000..40751fe --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/admin.guard.ts @@ -0,0 +1,43 @@ +import {CanActivateFn, Router} from '@angular/router'; +import {inject} from "@angular/core"; +import {AuthService} from "../../infrastructure/services/auth-service"; +import {ToastService} from "../../infrastructure/services/toast.service"; +import {UserStoreService} from "../../infrastructure/services/user-store.service"; +import {catchError, map} from 'rxjs/operators'; +import {of} from 'rxjs'; + +export const adminGuard: CanActivateFn = (route, state) => { + + + const userStore = inject(UserStoreService); + const authenticateService = inject(AuthService); + const toastService = inject(ToastService); + const router = inject(Router); + + return userStore.getRoleFromStore().pipe( + map(val => { + const roleFromToken = authenticateService.getRoleFromToken(); + const role = val || roleFromToken; + + if (role === "Admin" || role === "SuperAdmin") { + return true; + } else { + toastService.show("Access denied. Admins only.", { + classname: 'bg-warning text-dark', + delay: 3000 + }); + void router.navigate(['/unauthorized']); + return false; + } + }), + catchError(() => { + toastService.show("An error occurred. Please try again later.", { + classname: 'bg-danger text-light', + delay: 2000 + }); + void router.navigate(['/login']); + return of(false); + }) + ); +}; + diff --git a/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts new file mode 100644 index 0000000..71fed82 --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts @@ -0,0 +1,24 @@ +import {TestBed} from '@angular/core/testing'; + +import {AuthenticationGuard} from './authentication.guard'; +import {provideHttpClient} from "@angular/common/http"; +import {ActivatedRoute} from "@angular/router"; +import {of} from "rxjs"; + +describe('AuthenticationGuard', () => { + let guard: AuthenticationGuard; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), + { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }, + ] + }); + guard = TestBed.inject(AuthenticationGuard); + + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/guards/authentication.guard.ts b/src/ClientApp/src/app/presentation/guards/authentication.guard.ts new file mode 100644 index 0000000..15da467 --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/authentication.guard.ts @@ -0,0 +1,29 @@ +import {inject, Injectable} from '@angular/core'; +import {CanActivate, Router} from '@angular/router'; +import {AuthService} from "../../infrastructure/services/auth-service"; +import {ToastService} from "../../infrastructure/services/toast.service"; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationGuard implements CanActivate { + toastService = inject(ToastService); + private authenticateService = inject(AuthService) + private router = inject(Router) + + canActivate(): boolean { + if (this.authenticateService.isLoggedIn()) { + return true; + } else { + this.toastService.show("Please Login first", { + classname: 'bg-warning text-dark', + delay: 2000 + }); + this.router.navigate(['/login']).catch(error => { + console.error('Navigation error:', error); + }) + return false; + } + } + +} diff --git a/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts new file mode 100644 index 0000000..6fdaf10 --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { CanActivateFn } from '@angular/router'; + +import { guestGuard } from './guest.guard'; + +describe('guestGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => guestGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/guards/guest.guard.ts b/src/ClientApp/src/app/presentation/guards/guest.guard.ts new file mode 100644 index 0000000..4e17981 --- /dev/null +++ b/src/ClientApp/src/app/presentation/guards/guest.guard.ts @@ -0,0 +1,16 @@ +import {CanActivateFn, Router} from '@angular/router'; +import {inject} from "@angular/core"; +import {AuthService} from "../../infrastructure/services/auth-service"; + + +export const guestGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isLoggedIn()){ + return true; + } + // TODO rout + void router.navigate(['/user-dashboard']); + return false; +}; diff --git a/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts new file mode 100644 index 0000000..e98338f --- /dev/null +++ b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts @@ -0,0 +1,17 @@ +import {TestBed} from '@angular/core/testing'; +import {HttpInterceptorFn} from '@angular/common/http'; + +import {tokenInterceptor} from './token.interceptor'; + +describe('tokenInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => tokenInterceptor(req, next)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/interceptors/token.interceptor.ts b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.ts new file mode 100644 index 0000000..de9d38c --- /dev/null +++ b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.ts @@ -0,0 +1,75 @@ +import {HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest} from '@angular/common/http'; +import {inject} from "@angular/core"; +import {AuthService} from "../../infrastructure/services/auth-service"; +import {catchError, Observable, switchMap, throwError} from "rxjs"; +import {ToastService} from "../../infrastructure/services/toast.service"; +import {Router} from '@angular/router'; +import {RefreshTokenRequest} from "../models/refresh-token-request"; + +export const tokenInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const token = authService.getToken(); + const toastService = inject(ToastService); + const router = inject(Router); + + if (token) { + req = req.clone({ + setHeaders: {Authorization: `Bearer ${token}`} + }); + } + + return next(req).pipe( + catchError((err: any) => { + if (err instanceof HttpErrorResponse) { + if (err.status === 401) { + return handleUnAuthorizedError(req, next, authService, router, toastService); + } + } + console.log("interceptor error",err.error?.error?.message); + return throwError(() => err); + + }) + ); +}; + +function handleUnAuthorizedError( + req: HttpRequest, + next: HttpHandlerFn, + authenticateService: AuthService, + router: Router, + toastService: ToastService +): Observable> { + + const refreshTokenRequest = new RefreshTokenRequest(); + refreshTokenRequest.refreshToken = authenticateService.getRefreshToken()!; + refreshTokenRequest.userId = authenticateService.getUserIdFromToken()!; + + return authenticateService.renewToken(refreshTokenRequest).pipe( + switchMap((data) => { + const tokenResponse = data.value; + authenticateService.storeRefreshToken(tokenResponse.refreshToken); + authenticateService.storeToken(tokenResponse.accessToken); + + req = req.clone({ + setHeaders: {Authorization: `Bearer ${tokenResponse.accessToken}`} + }); + + return next(req); + }), + catchError((err) => { + return throwError(() => { + authenticateService.signOut(); + router.navigate(['login']).then(success => { + if (success) { + toastService.show(err.error?.error?.message ||'Token is expired, login again!', { + classname: 'bg-warning text-light', + delay: 2000 + }); + } + }) + }); + }) + ); +} + + diff --git a/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.html b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.html new file mode 100644 index 0000000..c17628d --- /dev/null +++ b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.html @@ -0,0 +1,19 @@ + + + + + {{ title }} + + + + @if (body) { + {{ body }} + } + + + + + diff --git a/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.scss b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.spec.ts b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.spec.ts new file mode 100644 index 0000000..592d4d6 --- /dev/null +++ b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupModalComponent } from './popup-modal.component'; + +describe('PopupModalComponent', () => { + let component: PopupModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopupModalComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PopupModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.ts b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.ts new file mode 100644 index 0000000..7cbbfe8 --- /dev/null +++ b/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.ts @@ -0,0 +1,26 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {NgClass} from "@angular/common"; + +@Component({ + selector: 'app-popup-modal', + imports: [ + NgClass + ], + templateUrl: './popup-modal.component.html', + styleUrl: './popup-modal.component.scss', + standalone: true +}) +export class PopupModalComponent { + + + @Input() headerClass: string = 'bg-light'; + @Input() title: string = ''; + @Input() body: string = ''; + + @Output() confirm = new EventEmitter(); + @Output() cancel = new EventEmitter(); + + onConfirm() { + this.confirm.emit(); + } +}
unauthorized works!
Username: {{username}}
E-Mail: {{email}}
+ Do you really want to delete + {{ selectedUser?.username }} ? +
{{ body }}