From d0e666418ce3d11bb93ad563bc7f9cd0cc9c56f3 Mon Sep 17 00:00:00 2001 From: Tomas Simko <72190589+TomassSimko@users.noreply.github.com> Date: Sat, 18 Nov 2023 23:40:22 +0100 Subject: [PATCH 1/5] progress work ui almost done --- Directory.Packages.props | 4 +- src/Infrastructure/DependencyInjection.cs | 9 +- src/Infrastructure/Infrastructure.csproj | 2 + src/UI/src/app/app-routing.module.ts | 8 + src/UI/src/app/app.component.spec.ts | 5 +- src/UI/src/app/app.component.ts | 8 +- .../auth/interceptors/auth.interceptor.ts | 32 +++ src/UI/src/app/core/auth/models/login.ts | 8 + .../auth/service/auth-can-activate.service.ts | 27 ++ .../core/auth/service/auth.service.spec.ts | 24 ++ .../src/app/core/auth/service/auth.service.ts | 76 ++++++ src/UI/src/app/core/core-routing.module.ts | 10 + src/UI/src/app/core/core.module.ts | 28 ++ .../app/modules/auth/auth-routing.module.ts | 24 ++ .../src/app/modules/auth/auth.component.html | 22 ++ .../src/app/modules/auth/auth.component.scss | 0 .../app/modules/auth/auth.component.spec.ts | 25 ++ src/UI/src/app/modules/auth/auth.component.ts | 15 ++ src/UI/src/app/modules/auth/auth.module.ts | 17 ++ .../auth/pages/login/login.component.html | 84 ++++++ .../auth/pages/login/login.component.scss | 0 .../auth/pages/login/login.component.spec.ts | 31 +++ .../auth/pages/login/login.component.ts | 103 +++++++ .../pages/register/register.component.html | 71 +++++ .../pages/register/register.component.scss | 0 .../pages/register/register.component.spec.ts | 23 ++ .../auth/pages/register/register.component.ts | 14 + .../dashboard/dashboard-routing.module.ts | 2 - .../components/navbar/navbar.component.html | 4 +- .../profile-menu/profile-menu.component.html | 2 +- .../profile-menu.component.spec.ts | 4 +- .../profile-menu/profile-menu.component.ts | 3 +- .../modules/layout/layout-routing.module.ts | 2 +- .../modules/layout/services/menu.service.ts | 2 +- src/UI/src/assets/images/ed.svg | 1 + src/UI/src/environments/environment.ts | 2 +- src/Web/DependencyInjection.cs | 2 - src/Web/Endpoints/WeatherForecasts.cs | 2 +- src/Web/Program.cs | 29 +- src/Web/Web.csproj | 9 +- src/Web/Web.http | 254 +++++++++--------- 41 files changed, 828 insertions(+), 160 deletions(-) create mode 100644 src/UI/src/app/core/auth/interceptors/auth.interceptor.ts create mode 100644 src/UI/src/app/core/auth/models/login.ts create mode 100644 src/UI/src/app/core/auth/service/auth-can-activate.service.ts create mode 100644 src/UI/src/app/core/auth/service/auth.service.spec.ts create mode 100644 src/UI/src/app/core/auth/service/auth.service.ts create mode 100644 src/UI/src/app/core/core-routing.module.ts create mode 100644 src/UI/src/app/core/core.module.ts create mode 100644 src/UI/src/app/modules/auth/auth-routing.module.ts create mode 100644 src/UI/src/app/modules/auth/auth.component.html create mode 100644 src/UI/src/app/modules/auth/auth.component.scss create mode 100644 src/UI/src/app/modules/auth/auth.component.spec.ts create mode 100644 src/UI/src/app/modules/auth/auth.component.ts create mode 100644 src/UI/src/app/modules/auth/auth.module.ts create mode 100644 src/UI/src/app/modules/auth/pages/login/login.component.html create mode 100644 src/UI/src/app/modules/auth/pages/login/login.component.scss create mode 100644 src/UI/src/app/modules/auth/pages/login/login.component.spec.ts create mode 100644 src/UI/src/app/modules/auth/pages/login/login.component.ts create mode 100644 src/UI/src/app/modules/auth/pages/register/register.component.html create mode 100644 src/UI/src/app/modules/auth/pages/register/register.component.scss create mode 100644 src/UI/src/app/modules/auth/pages/register/register.component.spec.ts create mode 100644 src/UI/src/app/modules/auth/pages/register/register.component.ts create mode 100644 src/UI/src/assets/images/ed.svg diff --git a/Directory.Packages.props b/Directory.Packages.props index f8b1ef5..f018964 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,11 +14,12 @@ + + - @@ -26,6 +27,7 @@ + diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 94637b6..15730fe 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -34,17 +34,22 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddScoped(provider => provider.GetRequiredService()); services.AddScoped(); - + services .AddDefaultIdentity() .AddRoles() .AddEntityFrameworkStores(); + services.AddSingleton(TimeProvider.System); services.AddTransient(); - + + services + .AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); + services.AddAuthorization(options => options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator))); + return services; } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index c14f149..5e6197b 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -6,12 +6,14 @@ + + diff --git a/src/UI/src/app/app-routing.module.ts b/src/UI/src/app/app-routing.module.ts index 2d17e75..f913023 100755 --- a/src/UI/src/app/app-routing.module.ts +++ b/src/UI/src/app/app-routing.module.ts @@ -1,7 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { LoginComponent } from './modules/auth/pages/login/login.component'; + const routes: Routes = [ + { + path: 'auth', + loadChildren: () => import('./modules/auth/auth.module').then((m) => m.AuthModule), + }, { path: '', loadChildren: () => import('./modules/layout/layout.module').then((m) => m.LayoutModule), @@ -10,6 +16,8 @@ const routes: Routes = [ path: 'management', loadChildren: () => import('./modules/management/management.module').then((m) => m.ManagementModule), }, + + { path: '**', redirectTo: 'error/404' }, ]; @NgModule({ diff --git a/src/UI/src/app/app.component.spec.ts b/src/UI/src/app/app.component.spec.ts index d99b4d5..bb11536 100755 --- a/src/UI/src/app/app.component.spec.ts +++ b/src/UI/src/app/app.component.spec.ts @@ -1,10 +1,11 @@ import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; +import { CommonModule } from '@angular/common'; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ - imports: [RouterTestingModule,AppComponent], + imports: [RouterTestingModule,AppComponent,CommonModule], })); it('should create the app', () => { @@ -16,6 +17,6 @@ describe('AppComponent', () => { it(`should have as title 'frontend'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('frontend'); + expect(app.title).toEqual('UI'); }); }); diff --git a/src/UI/src/app/app.component.ts b/src/UI/src/app/app.component.ts index e5902df..cba3314 100755 --- a/src/UI/src/app/app.component.ts +++ b/src/UI/src/app/app.component.ts @@ -1,21 +1,23 @@ import { Component, OnInit } from '@angular/core'; import { ThemeService } from './core/services/theme.service'; import { RouterOutlet } from '@angular/router'; -import { NgClass } from '@angular/common'; +import { CommonModule, NgClass, NgIf } from '@angular/common'; import { ResponsiveHelperComponent } from './shared/components/responsive-helper/responsive-helper.component'; import { AlertComponent } from './shared/component/alert/alert.component'; import { initFlowbite } from 'flowbite'; +import { ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, - imports: [NgClass, RouterOutlet, ResponsiveHelperComponent,AlertComponent], + imports: [NgClass, RouterOutlet, ResponsiveHelperComponent,AlertComponent,NgIf], }) export class AppComponent implements OnInit { - title = 'frontend'; + title = 'UI'; + isLoggedIn: boolean = false; constructor(public themeService: ThemeService) {} ngOnInit(): void { diff --git a/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..71db901 --- /dev/null +++ b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { environment } from '../../../../environments/environment'; +import { Observable } from 'rxjs'; +import { AuthService } from '../service/auth.service'; + + +// Adds Authorization header and token to HTTP requests when a token is available. +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + constructor(private authService: AuthService) {} + + intercept(req: HttpRequest, + next: HttpHandler): Observable> { + + // Don't add token if URL is not our API + if (!req.url.includes(environment.baseUrl)) + return next.handle(req); + + const token = this.authService.getUserToken(); + + if (!token) + return next.handle(req); + + const cloned = req.clone({ + headers: req.headers.set('Authorization', token) + }); + + return next.handle(cloned); + } +} diff --git a/src/UI/src/app/core/auth/models/login.ts b/src/UI/src/app/core/auth/models/login.ts new file mode 100644 index 0000000..ccce705 --- /dev/null +++ b/src/UI/src/app/core/auth/models/login.ts @@ -0,0 +1,8 @@ + +export interface AuthOk { + tokenType: string, + accessToken:string, + expiresIn: number, + refreshToken: string, + } + \ No newline at end of file diff --git a/src/UI/src/app/core/auth/service/auth-can-activate.service.ts b/src/UI/src/app/core/auth/service/auth-can-activate.service.ts new file mode 100644 index 0000000..411ad82 --- /dev/null +++ b/src/UI/src/app/core/auth/service/auth-can-activate.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthCanActivate implements CanActivate { + + constructor( + private authService: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean | UrlTree + { + if (this.authService.isSignedIn()) + return true; + + this.router.navigate(['/']); + return false; + } +} diff --git a/src/UI/src/app/core/auth/service/auth.service.spec.ts b/src/UI/src/app/core/auth/service/auth.service.spec.ts new file mode 100644 index 0000000..8591047 --- /dev/null +++ b/src/UI/src/app/core/auth/service/auth.service.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpClientModule } from '@angular/common/http'; +import { AuthService } from './auth.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('AuthService', () => { + let service: AuthService; + let fixture: ComponentFixture; + + beforeEach(() => { + + TestBed.configureTestingModule({ + imports: [HttpClientModule,HttpClientTestingModule,HttpClientModule ], // Include HttpClientModule here + + }); + service = TestBed.inject(AuthService); + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/UI/src/app/core/auth/service/auth.service.ts b/src/UI/src/app/core/auth/service/auth.service.ts new file mode 100644 index 0000000..b4f1cb2 --- /dev/null +++ b/src/UI/src/app/core/auth/service/auth.service.ts @@ -0,0 +1,76 @@ +import {Injectable, OnDestroy} from '@angular/core'; +import {HttpClient, HttpResponse} from '@angular/common/http'; +import {environment} from '../../../../environments/environment'; +import { shareReplay } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; +import {BehaviorSubject, Observable} from 'rxjs'; +import { AuthOk } from '../models/login'; + +@Injectable({ + providedIn: 'root' +}) + +export class AuthService { + + public signInState: Observable; + private _signInState = new BehaviorSubject(null); + + constructor(private _http: HttpClient) { + + this.signInState = this._signInState.asObservable(); + + const userData = this.getStoredUserData(); + if (userData != null) { + this._signInState.next(userData); + } + } + + public authenticate(email: string, password: string): Observable> { + const loginData = { + email: email, + password: password + }; + + return this._http.post(`${environment.baseUrl}/Users/login`, loginData, { observe: 'response' }) + .pipe( + tap(res => this.signIn(res.body)), + shareReplay() + ); + } + + + private signIn(data: AuthOk) { + console.log(data) + const expiresAt = new Date(); + expiresAt.setTime(Date.now() + (data.expiresIn * 1000)); + + localStorage.setItem('auth_userData', JSON.stringify(data)); + localStorage.setItem('auth_tokenString', `${data.tokenType} ${data.accessToken}`); + localStorage.setItem('auth_tokenExpiresAt', expiresAt.getTime().toString()); + this._signInState.next(data); + } + + public signOut() { + localStorage.removeItem('auth_userData'); + localStorage.removeItem('auth_tokenString'); + localStorage.removeItem('auth_tokenExpiresAt'); + + this._signInState.next(null); + } + + public isSignedIn() { + return this._signInState.value != null; + } + + public getUserToken() { + return localStorage.getItem('auth_tokenString'); + } + + public getValidityDays() { + return (+localStorage.getItem('auth_tokenExpiresAt') - Date.now()) / 1000 / (3600 * 24); + } + + private getStoredUserData(): AuthOk { + return JSON.parse(localStorage.getItem('auth_userData')) as AuthOk; + } +} diff --git a/src/UI/src/app/core/core-routing.module.ts b/src/UI/src/app/core/core-routing.module.ts new file mode 100644 index 0000000..1161494 --- /dev/null +++ b/src/UI/src/app/core/core-routing.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = []; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class CoreRoutingModule { } diff --git a/src/UI/src/app/core/core.module.ts b/src/UI/src/app/core/core.module.ts new file mode 100644 index 0000000..9110434 --- /dev/null +++ b/src/UI/src/app/core/core.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { CoreRoutingModule } from './core-routing.module'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { AuthInterceptor } from './auth/interceptors/auth.interceptor'; + + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + CoreRoutingModule, + ReactiveFormsModule, + RouterModule, + HttpClientModule, + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + }, + ] +}) +export class CoreModule { } diff --git a/src/UI/src/app/modules/auth/auth-routing.module.ts b/src/UI/src/app/modules/auth/auth-routing.module.ts new file mode 100644 index 0000000..298d5ba --- /dev/null +++ b/src/UI/src/app/modules/auth/auth-routing.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthComponent } from './auth.component'; +import { LoginComponent } from './pages/login/login.component'; +import { RegisterComponent } from './pages/register/register.component'; + +const routes: Routes = [ + { + path: '', + component: AuthComponent, + children: [ + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: 'login', component: LoginComponent, data: { returnUrl: window.location.pathname } }, + { path: 'register', component: RegisterComponent }, + { path: '**', redirectTo: 'login', pathMatch: 'full' }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AuthRoutingModule { } diff --git a/src/UI/src/app/modules/auth/auth.component.html b/src/UI/src/app/modules/auth/auth.component.html new file mode 100644 index 0000000..3d9cbc7 --- /dev/null +++ b/src/UI/src/app/modules/auth/auth.component.html @@ -0,0 +1,22 @@ +
+ +
+
+ +
+ + 🌐 + + SkillSphere +
+ + +
+
+
+ \ No newline at end of file diff --git a/src/UI/src/app/modules/auth/auth.component.scss b/src/UI/src/app/modules/auth/auth.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/UI/src/app/modules/auth/auth.component.spec.ts b/src/UI/src/app/modules/auth/auth.component.spec.ts new file mode 100644 index 0000000..222f04f --- /dev/null +++ b/src/UI/src/app/modules/auth/auth.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthComponent } from './auth.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('AuthComponent', () => { + let component: AuthComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AuthComponent] + }); + fixture = TestBed.createComponent(AuthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/UI/src/app/modules/auth/auth.component.ts b/src/UI/src/app/modules/auth/auth.component.ts new file mode 100644 index 0000000..53f5c8c --- /dev/null +++ b/src/UI/src/app/modules/auth/auth.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; + +@Component({ + selector: 'app-auth', + standalone: true, + imports: [CommonModule,RouterOutlet,HttpClientModule], + templateUrl: './auth.component.html', + styleUrls: ['./auth.component.scss'] +}) +export class AuthComponent { + +} diff --git a/src/UI/src/app/modules/auth/auth.module.ts b/src/UI/src/app/modules/auth/auth.module.ts new file mode 100644 index 0000000..c40a402 --- /dev/null +++ b/src/UI/src/app/modules/auth/auth.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { AuthRoutingModule } from './auth-routing.module'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { AuthService } from 'src/app/core/auth/service/auth.service'; + + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + AuthRoutingModule, + HttpClientModule + ], +}) +export class AuthModule { } diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.html b/src/UI/src/app/modules/auth/pages/login/login.component.html new file mode 100644 index 0000000..cd3feae --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/login/login.component.html @@ -0,0 +1,84 @@ + +
+
+

+ Hello Again ! +

+

Enter your credential to access your account.

+
+ + +
+
+
+ + +
+
+
Required field
+
Email must be an email address valid
+
+
+ +
+
+ + + + + + +
+
+
Required field
+
+
+
+ +
+
+ + +
+ + +
+ + + + + +
+ Not a Member yet? Register +
+
+ \ No newline at end of file diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.scss b/src/UI/src/app/modules/auth/pages/login/login.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts b/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts new file mode 100644 index 0000000..31e6251 --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + LoginComponent, + HttpClientModule, + RouterTestingModule, + ReactiveFormsModule, + HttpClientTestingModule, + ], + }); + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.ts b/src/UI/src/app/modules/auth/pages/login/login.component.ts new file mode 100644 index 0000000..99c7297 --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/login/login.component.ts @@ -0,0 +1,103 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule, NgClass, NgIf } from '@angular/common'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { Subscription, timer } from 'rxjs'; +import { AuthService } from 'src/app/core/auth/service/auth.service'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; + + +enum LocalLoginState { + None, + Waiting, + Success, + ErrorWrongData, + ErrorOther +} + +@Component({ + selector: 'app-login', + standalone: true, + imports: [ + FormsModule, + ReactiveFormsModule, + RouterLink, + HttpClientModule, + NgClass, + NgIf, + ], + providers: [AuthService,HttpClient], + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit, OnDestroy{ + form!: FormGroup; + submitted = false; + passwordTextType!: boolean; + formSubmitAttempt: boolean; + + private sub: Subscription; + + constructor(private readonly _formBuilder: FormBuilder,private authService: AuthService) {} + + + + + ngOnInit(): void { + this.form = this._formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + } + + + ngOnDestroy(): void { + if(this.sub) this.sub.unsubscribe(); + } + + + localLoginState = LocalLoginState.None; + get localLoginStates() { return LocalLoginState; } + + get f() { + return this.form.controls; + } + + togglePasswordTextType() { + this.passwordTextType = !this.passwordTextType; + } + + onSubmit() { + + const { email, password } = this.form.value; + + console.log(this.form.value) + this.formSubmitAttempt = true; + + if (this.form.invalid) { + return; + } + + this.localLoginState = LocalLoginState.Waiting; + + this.form.disable(); + + this.authService.authenticate(email, password).subscribe( + _ => { + this.localLoginState = LocalLoginState.Success; + timer(5000).subscribe(() => this.localLoginState = LocalLoginState.None); // In case user logs out without navigating elsewhere; the 'success' would still be visible. + this.form.enable(); + }, + err => { + + this.form.enable(); + + if (err.status == 401) + this.localLoginState = LocalLoginState.ErrorWrongData; + else + this.localLoginState = LocalLoginState.ErrorOther; + } + ); + // this._router.navigate(['/']); + } +} diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.html b/src/UI/src/app/modules/auth/pages/register/register.component.html new file mode 100644 index 0000000..4edba02 --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/register/register.component.html @@ -0,0 +1,71 @@ +
+
+

+ Register ! +

+
+ +
+
+ + +
+
+ + + + + +
+ +
+
+
+
+
+
+ Use 8 or more characters with a mix of letters, numbers & symbols. +
+ + +
+
+ +
+
+ + +
+
+ + + + + +
+ Already have an Account? Login +
+
+ \ No newline at end of file diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.scss b/src/UI/src/app/modules/auth/pages/register/register.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts b/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts new file mode 100644 index 0000000..008dca6 --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RegisterComponent,HttpClientModule,RouterTestingModule] + }); + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.ts b/src/UI/src/app/modules/auth/pages/register/register.component.ts new file mode 100644 index 0000000..4cb29fe --- /dev/null +++ b/src/UI/src/app/modules/auth/pages/register/register.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [CommonModule,RouterLink], + templateUrl: './register.component.html', + styleUrls: ['./register.component.scss'] +}) +export class RegisterComponent { + +} diff --git a/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts b/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts index 94a2446..971f5bb 100644 --- a/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts +++ b/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts @@ -2,8 +2,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './pages/dashboard/dashboard.component'; - - const routes: Routes = [ { path: '', diff --git a/src/UI/src/app/modules/layout/components/navbar/navbar.component.html b/src/UI/src/app/modules/layout/components/navbar/navbar.component.html index 8c961bd..4564c19 100644 --- a/src/UI/src/app/modules/layout/components/navbar/navbar.component.html +++ b/src/UI/src/app/modules/layout/components/navbar/navbar.component.html @@ -21,9 +21,9 @@ - + diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html index a30b388..0142604 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html @@ -46,7 +46,7 @@
  • - Sign out + Logout
  • diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts index ea99a36..8f6653b 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProfileMenuComponent } from './profile-menu.component'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; describe('ProfileMenuComponent', () => { let component: ProfileMenuComponent; @@ -8,7 +10,7 @@ describe('ProfileMenuComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ProfileMenuComponent] + imports: [ProfileMenuComponent,RouterTestingModule] }); fixture = TestBed.createComponent(ProfileMenuComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts index 997e627..4c6b6c4 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule, NgClass } from '@angular/common'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'app-profile-menu', standalone: true, - imports: [CommonModule,NgClass], + imports: [CommonModule,NgClass,RouterLink], templateUrl: './profile-menu.component.html', styleUrls: ['./profile-menu.component.scss'] }) diff --git a/src/UI/src/app/modules/layout/layout-routing.module.ts b/src/UI/src/app/modules/layout/layout-routing.module.ts index a2f855c..31bb3ce 100644 --- a/src/UI/src/app/modules/layout/layout-routing.module.ts +++ b/src/UI/src/app/modules/layout/layout-routing.module.ts @@ -4,12 +4,12 @@ import { LayoutComponent } from './layout.component'; import { ManagementComponent } from '../management/management.component'; const routes: Routes = [ + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', component: LayoutComponent, loadChildren: () => import('../dashboard/dashboard.module').then((m) => m.DashboardModule), }, - { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'management', component: LayoutComponent, diff --git a/src/UI/src/app/modules/layout/services/menu.service.ts b/src/UI/src/app/modules/layout/services/menu.service.ts index 8d4d6ee..4a0d0f3 100644 --- a/src/UI/src/app/modules/layout/services/menu.service.ts +++ b/src/UI/src/app/modules/layout/services/menu.service.ts @@ -85,6 +85,6 @@ export class MenuService implements OnDestroy { } ngOnDestroy(): void { - this._subscription.unsubscribe(); + if(this._subscription) this._subscription.unsubscribe(); } } \ No newline at end of file diff --git a/src/UI/src/assets/images/ed.svg b/src/UI/src/assets/images/ed.svg new file mode 100644 index 0000000..74f96dd --- /dev/null +++ b/src/UI/src/assets/images/ed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/UI/src/environments/environment.ts b/src/UI/src/environments/environment.ts index 8e32690..cccb676 100644 --- a/src/UI/src/environments/environment.ts +++ b/src/UI/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment = { production: false, - baseUrl: 'http://localhost:5000', + baseUrl: 'https://localhost:5001/api', }; /* diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 6bb1f9e..55362ea 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -23,8 +23,6 @@ public static IServiceCollection AddWebServices(this IServiceCollection services services.AddExceptionHandler(); - services.AddRazorPages(); - services.AddScoped(provider => { var validationRules = provider.GetService>(); diff --git a/src/Web/Endpoints/WeatherForecasts.cs b/src/Web/Endpoints/WeatherForecasts.cs index 8ae3d13..984915d 100644 --- a/src/Web/Endpoints/WeatherForecasts.cs +++ b/src/Web/Endpoints/WeatherForecasts.cs @@ -8,7 +8,7 @@ public class WeatherForecasts : EndpointGroupBase public override void Map(WebApplication app) { app.MapGroup(this) - .RequireAuthorization() + // .RequireAuthorization() .MapGet(GetWeatherForecasts); } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index c78402f..6f02490 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -13,6 +13,20 @@ builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(builder => + { + builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .Build(); + }); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -30,6 +44,7 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); + app.UseSwaggerUi3(settings => { settings.Path = "/api"; @@ -40,16 +55,22 @@ name: "default", pattern: "{controller}/{action=Index}/{id?}"); -app.MapRazorPages(); - -app.MapFallbackToFile("index.html"); - app.UseExceptionHandler(options => { }); app.Map("/", () => Results.Redirect("/api")); app.MapEndpoints(); + +app.UseCors(options => +{ + options.SetIsOriginAllowed(origin => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); +}); + + app.Run(); public partial class Program { } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 5ca5566..941f39a 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -25,14 +25,7 @@
    - - - <_ContentIncludedByDefault Remove="Pages\Error.cshtml" /> - <_ContentIncludedByDefault Remove="Pages\Shared\_LoginPartial.cshtml" /> - <_ContentIncludedByDefault Remove="Pages\_ViewImports.cshtml" /> - - - + OnBuildSuccess diff --git a/src/Web/Web.http b/src/Web/Web.http index ee1d903..c34c02e 100644 --- a/src/Web/Web.http +++ b/src/Web/Web.http @@ -1,127 +1,127 @@ -# For more info on HTTP files go to https://aka.ms/vs/httpfile -@Web_HostAddress = https://localhost:5001 -@AuthCookieName = .AspNetCore.Identity.Application -@AuthCookieValue = - -# GET Identity Account Login -# Get the @RequestVerificationToken necessary for logging in. -GET {{Web_HostAddress}}/Identity/Account/Login - -### - -# POST Identity Account Login -# Get the @AuthCookieValue necessary for authenticating requests. -@Email=administrator@localhost -@Password=Administrator1! -@RequestVerificationToken= -POST {{Web_HostAddress}}/Identity/Account/Login -Content-Type: application/x-www-form-urlencoded - -Input.Email={{Email}}&Input.Password={{Password}}&__RequestVerificationToken={{RequestVerificationToken}} - -### - -# GET WeatherForecast -GET {{Web_HostAddress}}/api/WeatherForecasts -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### - -# GET TodoLists -GET {{Web_HostAddress}}/api/TodoLists -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### - -#GET TodoList -GET {{Web_HostAddress}}/api/TodoLists/1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### - -# POST TodoLists -POST {{Web_HostAddress}}/api/TodoLists -Cookie: {{AuthCookieName}}={{AuthCookieValue}} -Content-Type: application/json - -// CreateTodoListCommand -{ - "Title": "Backlog" -} - -### - -# PUT TodoLists -PUT {{Web_HostAddress}}/api/TodoLists/1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} -Content-Type: application/json - -// UpdateTodoListCommand -{ - "Id": 1, - "Title": "Product Backlog" -} - -### - -# DELETE TodoLists -DELETE {{Web_HostAddress}}/api/TodoLists/1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### - -# GET TodoItems -@PageNumber = 1 -@PageSize = 10 -GET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}} -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### - -# POST TodoItems -POST {{Web_HostAddress}}/api/TodoItems -Cookie: {{AuthCookieName}}={{AuthCookieValue}} -Content-Type: application/json - -// CreateTodoItemCommand -{ - "ListId": 1, - "Title": "Eat a burrito 🌯" -} - -### - -#PUT TodoItems UpdateItemDetails -PUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} -Content-Type: application/json - -// UpdateTodoItemDetailCommand -{ - "Id": 1, - "ListId": 1, - "Priority": 3, - "Note": "This is a good idea!" -} - -### - -# PUT TodoItems -PUT {{Web_HostAddress}}/api/TodoItems/1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} -Content-Type: application/json - -// UpdateTodoItemCommand -{ - "Id": 1, - "Title": "Eat a yummy burrito 🌯", - "Done": true -} - -### - -# DELETE TodoItem -DELETE {{Web_HostAddress}}/api/TodoItems/1 -Cookie: {{AuthCookieName}}={{AuthCookieValue}} - -### \ No newline at end of file +## For more info on HTTP files go to https://aka.ms/vs/httpfile +#@Web_HostAddress = https://localhost:5001 +#@AuthCookieName = .AspNetCore.Identity.Application +#@AuthCookieValue = +# +## GET Identity Account Login +## Get the @RequestVerificationToken necessary for logging in. +#GET {{Web_HostAddress}}/Identity/Account/Login +# +#### +# +## POST Identity Account Login +## Get the @AuthCookieValue necessary for authenticating requests. +#@Email=administrator@localhost +#@Password=Administrator1! +#@RequestVerificationToken= +#POST {{Web_HostAddress}}/Identity/Account/Login +#Content-Type: application/x-www-form-urlencoded +# +#Input.Email={{Email}}&Input.Password={{Password}}&__RequestVerificationToken={{RequestVerificationToken}} +# +#### +# +## GET WeatherForecast +#GET {{Web_HostAddress}}/api/WeatherForecasts +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### +# +## GET TodoLists +#GET {{Web_HostAddress}}/api/TodoLists +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### +# +##GET TodoList +#GET {{Web_HostAddress}}/api/TodoLists/1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### +# +## POST TodoLists +#POST {{Web_HostAddress}}/api/TodoLists +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +#Content-Type: application/json +# +#// CreateTodoListCommand +#{ +# "Title": "Backlog" +#} +# +#### +# +## PUT TodoLists +#PUT {{Web_HostAddress}}/api/TodoLists/1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +#Content-Type: application/json +# +#// UpdateTodoListCommand +#{ +# "Id": 1, +# "Title": "Product Backlog" +#} +# +#### +# +## DELETE TodoLists +#DELETE {{Web_HostAddress}}/api/TodoLists/1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### +# +## GET TodoItems +#@PageNumber = 1 +#@PageSize = 10 +#GET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}} +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### +# +## POST TodoItems +#POST {{Web_HostAddress}}/api/TodoItems +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +#Content-Type: application/json +# +#// CreateTodoItemCommand +#{ +# "ListId": 1, +# "Title": "Eat a burrito 🌯" +#} +# +#### +# +##PUT TodoItems UpdateItemDetails +#PUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +#Content-Type: application/json +# +#// UpdateTodoItemDetailCommand +#{ +# "Id": 1, +# "ListId": 1, +# "Priority": 3, +# "Note": "This is a good idea!" +#} +# +#### +# +## PUT TodoItems +#PUT {{Web_HostAddress}}/api/TodoItems/1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +#Content-Type: application/json +# +#// UpdateTodoItemCommand +#{ +# "Id": 1, +# "Title": "Eat a yummy burrito 🌯", +# "Done": true +#} +# +#### +# +## DELETE TodoItem +#DELETE {{Web_HostAddress}}/api/TodoItems/1 +#Cookie: {{AuthCookieName}}={{AuthCookieValue}} +# +#### \ No newline at end of file From dd7a1da1a150fe1f1bcb5caeb46bd25fe4e35651 Mon Sep 17 00:00:00 2001 From: Tomas Simko <72190589+TomassSimko@users.noreply.github.com> Date: Sun, 19 Nov 2023 23:41:19 +0100 Subject: [PATCH 2/5] work on guards --- src/UI/src/app/app-routing.module.ts | 19 +++--- src/UI/src/app/app.component.ts | 4 +- .../auth/interceptors/auth.interceptor.ts | 61 +++++++++++++------ src/UI/src/app/core/auth/models/login.ts | 2 +- .../auth/service/auth-can-activate.service.ts | 27 -------- .../src/app/core/auth/service/auth-guard.ts | 15 +++++ .../core/auth/service/auth.service.spec.ts | 24 -------- .../src/app/core/auth/service/auth.service.ts | 33 ++++++---- src/UI/src/app/core/constant/menu.ts | 30 +++++++-- src/UI/src/app/core/core-routing.module.ts | 10 --- src/UI/src/app/core/core.module.ts | 28 --------- .../app/modules/auth/auth-routing.module.ts | 2 +- src/UI/src/app/modules/auth/auth.module.ts | 7 ++- .../auth/pages/login/login.component.html | 2 +- .../auth/pages/login/login.component.ts | 42 +++++-------- .../dashboard/dashboard-routing.module.ts | 6 +- .../dashboard/dashboard.component.spec.ts | 3 +- .../modules/dashboard/dashboard.component.ts | 1 + .../app/modules/dashboard/dashboard.module.ts | 12 ++-- .../pages/dashboard/dashboard.component.ts | 23 +++++-- .../service/dashboard-service.service.ts | 6 +- .../modules/layout/layout-routing.module.ts | 6 +- .../modules/layout/layout.component.spec.ts | 3 +- .../app/modules/layout/layout.component.ts | 4 +- .../src/app/modules/layout/layout.module.ts | 11 +++- .../management/management-routing.module.ts | 2 +- .../management/management.component.html | 1 + 27 files changed, 195 insertions(+), 189 deletions(-) delete mode 100644 src/UI/src/app/core/auth/service/auth-can-activate.service.ts create mode 100644 src/UI/src/app/core/auth/service/auth-guard.ts delete mode 100644 src/UI/src/app/core/auth/service/auth.service.spec.ts delete mode 100644 src/UI/src/app/core/core-routing.module.ts delete mode 100644 src/UI/src/app/core/core.module.ts diff --git a/src/UI/src/app/app-routing.module.ts b/src/UI/src/app/app-routing.module.ts index f913023..5d01237 100755 --- a/src/UI/src/app/app-routing.module.ts +++ b/src/UI/src/app/app-routing.module.ts @@ -1,23 +1,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { LoginComponent } from './modules/auth/pages/login/login.component'; +import { AuthGuard } from './core/auth/service/auth-guard'; +// Main routes const routes: Routes = [ - { - path: 'auth', - loadChildren: () => import('./modules/auth/auth.module').then((m) => m.AuthModule), - }, { path: '', loadChildren: () => import('./modules/layout/layout.module').then((m) => m.LayoutModule), + canActivate: [AuthGuard], }, { - path: 'management', - loadChildren: () => import('./modules/management/management.module').then((m) => m.ManagementModule), + path: 'auth', + loadChildren: () => import('./modules/auth/auth.module').then((m) => m.AuthModule), }, - + { + path: 'courses', + loadChildren: () => import('./modules/management/management.module').then((m) => m.ManagementModule), + + }, { path: '**', redirectTo: 'error/404' }, + ]; @NgModule({ diff --git a/src/UI/src/app/app.component.ts b/src/UI/src/app/app.component.ts index cba3314..c3b8c69 100755 --- a/src/UI/src/app/app.component.ts +++ b/src/UI/src/app/app.component.ts @@ -13,14 +13,16 @@ import { ReactiveFormsModule } from '@angular/forms'; styleUrls: ['./app.component.scss'], standalone: true, imports: [NgClass, RouterOutlet, ResponsiveHelperComponent,AlertComponent,NgIf], - + }) export class AppComponent implements OnInit { title = 'UI'; isLoggedIn: boolean = false; constructor(public themeService: ThemeService) {} + ngOnInit(): void { initFlowbite(); } + } diff --git a/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts index 71db901..48f079c 100644 --- a/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts +++ b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts @@ -1,32 +1,57 @@ -import { Injectable } from '@angular/core'; -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { environment } from '../../../../environments/environment'; -import { Observable } from 'rxjs'; +import { Observable, catchError, switchMap, throwError } from 'rxjs'; import { AuthService } from '../service/auth.service'; -// Adds Authorization header and token to HTTP requests when a token is available. -@Injectable() -export class AuthInterceptor implements HttpInterceptor { - constructor(private authService: AuthService) {} +@Injectable({ + providedIn: 'root' +}) - intercept(req: HttpRequest, - next: HttpHandler): Observable> { +export class AuthInterceptor implements HttpInterceptor { + authService = inject(AuthService); + refresh = false; + constructor() {} - // Don't add token if URL is not our API - if (!req.url.includes(environment.baseUrl)) - return next.handle(req); + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + + if (!req.url.includes(environment.baseUrl)) + return next.handle(req); + const token = this.authService.getUserToken(); if (!token) return next.handle(req); - const cloned = req.clone({ - headers: req.headers.set('Authorization', token) - }); - - return next.handle(cloned); - } + if(this.authService.isSignedIn){ + req = req.clone({ + headers: req.headers.set('Authorization', token) + }); + } + + return next.handle(req); + // return next.handle(cloned).pipe(catchError((err: HttpErrorResponse) => { + // if (err.status === 401 && !this.refresh) { + // this.refresh = true; + + // return this.http.post('http://localhost:5001/api/Users/refresh', {}, {withCredentials: true}).pipe( + // switchMap((res: any) => { + // this.authService.setUserToken(res.accessToken); + + // return next.handle(cloned.clone({ + // setHeaders: { + // Authorization: `Authorization ${token}` + // } + // })); + // }) + // ); + // } + // this.refresh = false; + // return throwError(() => err); + // })); +} } diff --git a/src/UI/src/app/core/auth/models/login.ts b/src/UI/src/app/core/auth/models/login.ts index ccce705..834f4d3 100644 --- a/src/UI/src/app/core/auth/models/login.ts +++ b/src/UI/src/app/core/auth/models/login.ts @@ -1,5 +1,5 @@ -export interface AuthOk { +export interface AuthResponse { tokenType: string, accessToken:string, expiresIn: number, diff --git a/src/UI/src/app/core/auth/service/auth-can-activate.service.ts b/src/UI/src/app/core/auth/service/auth-can-activate.service.ts deleted file mode 100644 index 411ad82..0000000 --- a/src/UI/src/app/core/auth/service/auth-can-activate.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@angular/core'; -import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; -import {Observable} from 'rxjs'; -import {AuthService} from './auth.service'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthCanActivate implements CanActivate { - - constructor( - private authService: AuthService, - private router: Router - ) {} - - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable | Promise | boolean | UrlTree - { - if (this.authService.isSignedIn()) - return true; - - this.router.navigate(['/']); - return false; - } -} diff --git a/src/UI/src/app/core/auth/service/auth-guard.ts b/src/UI/src/app/core/auth/service/auth-guard.ts new file mode 100644 index 0000000..ce5dfaa --- /dev/null +++ b/src/UI/src/app/core/auth/service/auth-guard.ts @@ -0,0 +1,15 @@ +import { inject } from "@angular/core"; +import { Router } from "@angular/router"; +import { AuthService } from "./auth.service"; + +export const AuthGuard = () => { + const currentUserService = inject(AuthService); + const router = inject(Router); + + if (!currentUserService.isSignedIn) { + return false; + } else { + router.navigate(['/auth/login']); + return true; + } +}; diff --git a/src/UI/src/app/core/auth/service/auth.service.spec.ts b/src/UI/src/app/core/auth/service/auth.service.spec.ts deleted file mode 100644 index 8591047..0000000 --- a/src/UI/src/app/core/auth/service/auth.service.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HttpClientModule } from '@angular/common/http'; -import { AuthService } from './auth.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -describe('AuthService', () => { - let service: AuthService; - let fixture: ComponentFixture; - - beforeEach(() => { - - TestBed.configureTestingModule({ - imports: [HttpClientModule,HttpClientTestingModule,HttpClientModule ], // Include HttpClientModule here - - }); - service = TestBed.inject(AuthService); - fixture.detectChanges(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); \ No newline at end of file diff --git a/src/UI/src/app/core/auth/service/auth.service.ts b/src/UI/src/app/core/auth/service/auth.service.ts index b4f1cb2..c1e77a4 100644 --- a/src/UI/src/app/core/auth/service/auth.service.ts +++ b/src/UI/src/app/core/auth/service/auth.service.ts @@ -1,22 +1,21 @@ -import {Injectable, OnDestroy} from '@angular/core'; +import {Injectable, OnDestroy, inject} from '@angular/core'; import {HttpClient, HttpResponse} from '@angular/common/http'; import {environment} from '../../../../environments/environment'; import { shareReplay } from 'rxjs/operators'; import { tap } from 'rxjs/operators'; import {BehaviorSubject, Observable} from 'rxjs'; -import { AuthOk } from '../models/login'; +import { AuthResponse } from '../models/login'; @Injectable({ providedIn: 'root' }) export class AuthService { - - public signInState: Observable; - private _signInState = new BehaviorSubject(null); + public signInState: Observable; + private _signInState = new BehaviorSubject(null); - constructor(private _http: HttpClient) { + constructor(private _http: HttpClient) { this.signInState = this._signInState.asObservable(); const userData = this.getStoredUserData(); @@ -25,13 +24,12 @@ export class AuthService { } } - public authenticate(email: string, password: string): Observable> { + public authenticate(email: string, password: string): Observable> { const loginData = { email: email, password: password }; - - return this._http.post(`${environment.baseUrl}/Users/login`, loginData, { observe: 'response' }) + return this._http.post(`${environment.baseUrl}/Users/login`, loginData, { observe: 'response' }) .pipe( tap(res => this.signIn(res.body)), shareReplay() @@ -39,8 +37,7 @@ export class AuthService { } - private signIn(data: AuthOk) { - console.log(data) + private signIn(data: AuthResponse) { const expiresAt = new Date(); expiresAt.setTime(Date.now() + (data.expiresIn * 1000)); @@ -62,6 +59,16 @@ export class AuthService { return this._signInState.value != null; } + public setUserToken(data: AuthResponse) { + const expiresAt = new Date(); + expiresAt.setTime(Date.now() + (data.expiresIn * 1000)); + + localStorage.setItem('auth_userData', JSON.stringify(data)); + localStorage.setItem('auth_tokenString', `${data.tokenType} ${data.accessToken}`); + localStorage.setItem('auth_tokenExpiresAt', expiresAt.getTime().toString()); + this._signInState.next(data); + } + public getUserToken() { return localStorage.getItem('auth_tokenString'); } @@ -70,7 +77,7 @@ export class AuthService { return (+localStorage.getItem('auth_tokenExpiresAt') - Date.now()) / 1000 / (3600 * 24); } - private getStoredUserData(): AuthOk { - return JSON.parse(localStorage.getItem('auth_userData')) as AuthOk; + private getStoredUserData(): AuthResponse { + return JSON.parse(localStorage.getItem('auth_userData')) as AuthResponse; } } diff --git a/src/UI/src/app/core/constant/menu.ts b/src/UI/src/app/core/constant/menu.ts index 7e49977..295f1e9 100644 --- a/src/UI/src/app/core/constant/menu.ts +++ b/src/UI/src/app/core/constant/menu.ts @@ -4,25 +4,43 @@ export class Menu { public static pages: MenuItem[] = [ { group: 'Pages', - separator: false, + separator: true, items: [ { - icon: 'heroBuildingStorefront', + icon: 'heroFolderOpen', label: 'Dashboard', route: '/dashboard', children: [ { label: 'Overview', route: '/dashboard' }, + { label: 'My Courses', route: '/courses/boxes' }, ], }, { - icon:'heroFolderOpen', - label: 'Management', - route: '/management', + icon:' heroBuildingStorefront', + label: 'Courses', + route: '/courses', children: [ - { label: 'Boxes Management', route: '/management/boxes' }, + { label: 'Course store', route: '/store' }, + { label: 'Saved courses', route: '/dashboard' }, ], }, ], }, + { + group: 'Config', + separator: false, + items: [ + { + icon: 'heroBuildingStorefront', + label: 'Settings', + route: '/settings', + }, + { + icon: 'heroBuildingStorefront', + label: 'Notifications', + route: '/gift', + }, + ], + }, ]; } \ No newline at end of file diff --git a/src/UI/src/app/core/core-routing.module.ts b/src/UI/src/app/core/core-routing.module.ts deleted file mode 100644 index 1161494..0000000 --- a/src/UI/src/app/core/core-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class CoreRoutingModule { } diff --git a/src/UI/src/app/core/core.module.ts b/src/UI/src/app/core/core.module.ts deleted file mode 100644 index 9110434..0000000 --- a/src/UI/src/app/core/core.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { CoreRoutingModule } from './core-routing.module'; -import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterModule } from '@angular/router'; -import { AuthInterceptor } from './auth/interceptors/auth.interceptor'; - - -@NgModule({ - declarations: [], - imports: [ - CommonModule, - CoreRoutingModule, - ReactiveFormsModule, - RouterModule, - HttpClientModule, - ], - providers: [ - { - provide: HTTP_INTERCEPTORS, - useClass: AuthInterceptor, - multi: true - }, - ] -}) -export class CoreModule { } diff --git a/src/UI/src/app/modules/auth/auth-routing.module.ts b/src/UI/src/app/modules/auth/auth-routing.module.ts index 298d5ba..bd4521c 100644 --- a/src/UI/src/app/modules/auth/auth-routing.module.ts +++ b/src/UI/src/app/modules/auth/auth-routing.module.ts @@ -12,7 +12,7 @@ const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'login', component: LoginComponent, data: { returnUrl: window.location.pathname } }, { path: 'register', component: RegisterComponent }, - { path: '**', redirectTo: 'login', pathMatch: 'full' }, + // { path: '**', redirectTo: 'login', pathMatch: 'full' }, ], }, ]; diff --git a/src/UI/src/app/modules/auth/auth.module.ts b/src/UI/src/app/modules/auth/auth.module.ts index c40a402..b71229e 100644 --- a/src/UI/src/app/modules/auth/auth.module.ts +++ b/src/UI/src/app/modules/auth/auth.module.ts @@ -11,7 +11,12 @@ import { AuthService } from 'src/app/core/auth/service/auth.service'; imports: [ CommonModule, AuthRoutingModule, - HttpClientModule + HttpClientModule, ], + providers: [ + HttpClientModule, + AuthService, + HttpClient + ] }) export class AuthModule { } diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.html b/src/UI/src/app/modules/auth/pages/login/login.component.html index cd3feae..39f6670 100644 --- a/src/UI/src/app/modules/auth/pages/login/login.component.html +++ b/src/UI/src/app/modules/auth/pages/login/login.component.html @@ -78,7 +78,7 @@

    - Not a Member yet? Register + Not a Member yet? Register
    \ No newline at end of file diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.ts b/src/UI/src/app/modules/auth/pages/login/login.component.ts index 99c7297..0e2c7c1 100644 --- a/src/UI/src/app/modules/auth/pages/login/login.component.ts +++ b/src/UI/src/app/modules/auth/pages/login/login.component.ts @@ -38,15 +38,13 @@ export class LoginComponent implements OnInit, OnDestroy{ private sub: Subscription; - constructor(private readonly _formBuilder: FormBuilder,private authService: AuthService) {} + constructor(private readonly _formBuilder: FormBuilder, private _router: Router,private authService: AuthService) {} - - ngOnInit(): void { this.form = this._formBuilder.group({ - email: ['', [Validators.required, Validators.email]], - password: ['', Validators.required], + email: ['test3@gmail.com', [Validators.required, Validators.email]], + password: ['Admin1!', Validators.required], }); } @@ -71,33 +69,23 @@ export class LoginComponent implements OnInit, OnDestroy{ const { email, password } = this.form.value; - console.log(this.form.value) - this.formSubmitAttempt = true; - if (this.form.invalid) { return; } - this.localLoginState = LocalLoginState.Waiting; - - this.form.disable(); - - this.authService.authenticate(email, password).subscribe( - _ => { - this.localLoginState = LocalLoginState.Success; - timer(5000).subscribe(() => this.localLoginState = LocalLoginState.None); // In case user logs out without navigating elsewhere; the 'success' would still be visible. - this.form.enable(); + this.authService.authenticate(email, password) + .subscribe({ + next: () => { + this._router.navigate(['/dashboard']); }, - err => { - - this.form.enable(); - - if (err.status == 401) - this.localLoginState = LocalLoginState.ErrorWrongData; - else - this.localLoginState = LocalLoginState.ErrorOther; + error: (err: any) => { + // Handle errors here + // 401 + // display alert } - ); - // this._router.navigate(['/']); + }); + + + } } diff --git a/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts b/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts index 971f5bb..232ff26 100644 --- a/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts +++ b/src/UI/src/app/modules/dashboard/dashboard-routing.module.ts @@ -2,14 +2,14 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './pages/dashboard/dashboard.component'; +// /dashboard => DashboardComponent + const routes: Routes = [ { path: '', component: DashboardComponent, - children: [ - // place other routes here later - ], }, + ]; @NgModule({ diff --git a/src/UI/src/app/modules/dashboard/dashboard.component.spec.ts b/src/UI/src/app/modules/dashboard/dashboard.component.spec.ts index 591c541..c6f82d3 100644 --- a/src/UI/src/app/modules/dashboard/dashboard.component.spec.ts +++ b/src/UI/src/app/modules/dashboard/dashboard.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DashboardComponent } from './dashboard.component'; import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('DashboardComponent', () => { let component: DashboardComponent; @@ -9,7 +10,7 @@ describe('DashboardComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [DashboardComponent,HttpClientModule] + imports: [DashboardComponent,HttpClientTestingModule] }); fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/dashboard/dashboard.component.ts b/src/UI/src/app/modules/dashboard/dashboard.component.ts index 2d4e808..621c4c7 100644 --- a/src/UI/src/app/modules/dashboard/dashboard.component.ts +++ b/src/UI/src/app/modules/dashboard/dashboard.component.ts @@ -1,3 +1,4 @@ +import { HttpClient, HttpClientModule } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; diff --git a/src/UI/src/app/modules/dashboard/dashboard.module.ts b/src/UI/src/app/modules/dashboard/dashboard.module.ts index 658dc68..2636020 100644 --- a/src/UI/src/app/modules/dashboard/dashboard.module.ts +++ b/src/UI/src/app/modules/dashboard/dashboard.module.ts @@ -2,13 +2,15 @@ import { NgModule } from '@angular/core'; import { DashboardRoutingModule } from './dashboard-routing.module'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { DashboardServiceService } from './service/dashboard-service.service'; - +import { AuthService } from 'src/app/core/auth/service/auth.service'; @NgModule({ - imports: [DashboardRoutingModule,HttpClientModule], + imports: [DashboardRoutingModule, HttpClientModule], providers: [ HttpClient, - DashboardServiceService - ] + DashboardServiceService, + AuthService, + HttpClientModule + ], }) -export class DashboardModule {} \ No newline at end of file +export class DashboardModule {} diff --git a/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts b/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts index 0aee070..25e4d34 100644 --- a/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts +++ b/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts @@ -1,17 +1,30 @@ import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output, TemplateRef, Type, ViewChild, ViewContainerRef, inject } from '@angular/core'; import { DashboardHeaderComponent } from '../../components/dashboard-header/dashboard-header.component'; -import { environment } from 'src/environments/environment.prod'; - - +import { environment } from 'src/environments/environment'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { DashboardServiceService } from '../../service/dashboard-service.service'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [CommonModule,DashboardHeaderComponent], + imports: [CommonModule,DashboardHeaderComponent,HttpClientModule], templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) -export class DashboardComponent { +export class DashboardComponent implements OnInit{ + // test = inject(DashboardServiceService) + + constructor() { } + + ngOnInit(): void { + + } + + // ngOnInit(): void { + // this.ds.getDashboardData().subscribe(data => { + // console.log(data); + // }); + // } } diff --git a/src/UI/src/app/modules/dashboard/service/dashboard-service.service.ts b/src/UI/src/app/modules/dashboard/service/dashboard-service.service.ts index dd2adb0..506fe4c 100644 --- a/src/UI/src/app/modules/dashboard/service/dashboard-service.service.ts +++ b/src/UI/src/app/modules/dashboard/service/dashboard-service.service.ts @@ -14,8 +14,10 @@ import { DashboardData, ResponseDto } from '../models/DashboardData'; export class DashboardServiceService { - constructor(private http: HttpClient,private state : State,private toastr: ToastrService) { - } + constructor(private http: HttpClient) {} + public getDashboardData(): Observable { + return this.http.get('https://localhost:5001/api/WeatherForecasts'); + } } diff --git a/src/UI/src/app/modules/layout/layout-routing.module.ts b/src/UI/src/app/modules/layout/layout-routing.module.ts index 31bb3ce..b0a5e67 100644 --- a/src/UI/src/app/modules/layout/layout-routing.module.ts +++ b/src/UI/src/app/modules/layout/layout-routing.module.ts @@ -2,19 +2,21 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LayoutComponent } from './layout.component'; import { ManagementComponent } from '../management/management.component'; +import { AuthGuard } from 'src/app/core/auth/service/auth-guard'; const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', component: LayoutComponent, - loadChildren: () => import('../dashboard/dashboard.module').then((m) => m.DashboardModule), + loadChildren: () => import('../dashboard/dashboard.module').then((m) => m.DashboardModule) }, { - path: 'management', + path: 'courses', component: LayoutComponent, loadChildren: () => import('../management/management.module').then((m) => m.ManagementModule), }, + { path: '**', redirectTo: 'error/404' }, ]; @NgModule({ diff --git a/src/UI/src/app/modules/layout/layout.component.spec.ts b/src/UI/src/app/modules/layout/layout.component.spec.ts index f71b51f..91e7b22 100644 --- a/src/UI/src/app/modules/layout/layout.component.spec.ts +++ b/src/UI/src/app/modules/layout/layout.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LayoutComponent } from './layout.component'; import { RouterTestingModule } from '@angular/router/testing'; import { ToastrModule } from 'ngx-toastr'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('LayoutComponent', () => { let component: LayoutComponent; @@ -10,7 +11,7 @@ describe('LayoutComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [LayoutComponent,RouterTestingModule, ToastrModule.forRoot()] + imports: [LayoutComponent,RouterTestingModule, ToastrModule.forRoot(),HttpClientTestingModule] }); fixture = TestBed.createComponent(LayoutComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/layout/layout.component.ts b/src/UI/src/app/modules/layout/layout.component.ts index 32ecbe8..9c9c55f 100644 --- a/src/UI/src/app/modules/layout/layout.component.ts +++ b/src/UI/src/app/modules/layout/layout.component.ts @@ -3,6 +3,7 @@ import { RouterOutlet } from '@angular/router'; import { NavbarComponent } from './components/navbar/navbar.component'; import { SidebarComponent } from './components/sidebar/sidebar.component'; import { AlertComponent } from 'src/app/shared/component/alert/alert.component'; +import { HttpClientModule } from '@angular/common/http'; @Component({ @@ -14,8 +15,7 @@ import { AlertComponent } from 'src/app/shared/component/alert/alert.component'; RouterOutlet, NavbarComponent, SidebarComponent, - AlertComponent, - + AlertComponent ], }) export class LayoutComponent implements OnInit { diff --git a/src/UI/src/app/modules/layout/layout.module.ts b/src/UI/src/app/modules/layout/layout.module.ts index e894f93..65a1fa9 100644 --- a/src/UI/src/app/modules/layout/layout.module.ts +++ b/src/UI/src/app/modules/layout/layout.module.ts @@ -1,13 +1,22 @@ -import { HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule, provideHttpClient, withInterceptors } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { LayoutRoutingModule } from './layout-routing.module'; import { SvgIconComponent, provideAngularSvgIcon } from 'angular-svg-icon'; import { NgxSmartModalModule } from 'ngx-smart-modal'; import { ToastrModule } from 'ngx-toastr'; +import { AuthInterceptor } from 'src/app/core/auth/interceptors/auth.interceptor'; @NgModule({ imports: [LayoutRoutingModule, HttpClientModule, NgxSmartModalModule.forRoot(), ToastrModule.forRoot()], + providers: [ + HttpClientModule, + HttpClient, + { provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + } + ], }) export class LayoutModule {} \ No newline at end of file diff --git a/src/UI/src/app/modules/management/management-routing.module.ts b/src/UI/src/app/modules/management/management-routing.module.ts index 7a3426e..1be1c57 100644 --- a/src/UI/src/app/modules/management/management-routing.module.ts +++ b/src/UI/src/app/modules/management/management-routing.module.ts @@ -9,7 +9,7 @@ const routes: Routes = [ path: '', component: ManagementComponent, children: [ - { path: 'boxes', component: BoxesComponent }, + { path: 'store', component: BoxesComponent }, { path: 'boxes/:id', component: BoxDetailComponent }, ], }, diff --git a/src/UI/src/app/modules/management/management.component.html b/src/UI/src/app/modules/management/management.component.html index 0680b43..4792537 100644 --- a/src/UI/src/app/modules/management/management.component.html +++ b/src/UI/src/app/modules/management/management.component.html @@ -1 +1,2 @@ + From fc43169e180a66bacaf42c7e4543d8c792bcce37 Mon Sep 17 00:00:00 2001 From: Tomas Simko <72190589+TomassSimko@users.noreply.github.com> Date: Tue, 21 Nov 2023 00:30:41 +0100 Subject: [PATCH 3/5] recovery --- src/Web/DependencyInjection.cs | 18 +- src/Web/Endpoints/Users.cs | 21 +- src/Web/Endpoints/WeatherForecasts.cs | 2 +- src/Web/Program.cs | 11 +- src/Web/wwwroot/api/specification.json | 636 +++---------------------- 5 files changed, 117 insertions(+), 571 deletions(-) diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 55362ea..74924ee 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -1,9 +1,11 @@ using Azure.Identity; using Microsoft.AspNetCore.Mvc; +using NSwag; +using NSwag.Generation.Processors.Security; +using SkillSphere.Application.Common.Interfaces; using skillSphere.Infrastructure.Data; using SkillSphere.Web.Infrastructure; using SkillSphere.Web.Services; -using SkillSphere.Application.Common.Interfaces; using ZymLabs.NSwag.FluentValidation; namespace SkillSphere.Web; @@ -23,6 +25,8 @@ public static IServiceCollection AddWebServices(this IServiceCollection services services.AddExceptionHandler(); + services.AddRazorPages(); + services.AddScoped(provider => { var validationRules = provider.GetService>(); @@ -48,6 +52,16 @@ public static IServiceCollection AddWebServices(this IServiceCollection services // BUG: SchemaProcessors is missing in NSwag 14 (https://github.com/RicoSuter/NSwag/issues/4524#issuecomment-1811897079) // configure.SchemaProcessors.Add(fluentValidationSchemaProcessor); + // Add JWT + configure.AddSecurity("JWT", Enumerable.Empty(), new OpenApiSecurityScheme + { + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Type = OpenApiSecuritySchemeType.ApiKey, + Description = "Please insert token: JWT {your JWT token}." + }); + + configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); }); return services; @@ -65,4 +79,6 @@ public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection return services; } + } + diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index ce24845..ad8a400 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -1,14 +1,33 @@ +using System.Diagnostics; +using System.Runtime.InteropServices.JavaScript; +using FluentValidation; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using SkillSphere.Application.Common.Interfaces; +using SkillSphere.Application.Common.Models; +using SkillSphere.Application.Features; +using SkillSphere.Application.TodoLists.Commands.CreateTodoList; +using SkillSphere.Domain.Entities; +using SkillSphere.Infrastructure.Authentication.Services; using SkillSphere.Web.Infrastructure; using SkillSphere.Infrastructure.Identity; namespace SkillSphere.Web.Endpoints; + public class Users : EndpointGroupBase { public override void Map(WebApplication app) { app.MapGroup(this) - .MapIdentityApi(); + .MapPost(Authenticate); + + } + + public async Task Authenticate(ISender sender,AuthUserCommand command) + { + return await sender.Send(command); } } diff --git a/src/Web/Endpoints/WeatherForecasts.cs b/src/Web/Endpoints/WeatherForecasts.cs index 984915d..c3a95fa 100644 --- a/src/Web/Endpoints/WeatherForecasts.cs +++ b/src/Web/Endpoints/WeatherForecasts.cs @@ -8,7 +8,7 @@ public class WeatherForecasts : EndpointGroupBase public override void Map(WebApplication app) { app.MapGroup(this) - // .RequireAuthorization() + .MapGet(GetWeatherForecasts); } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 6f02490..4d70f15 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,8 +1,8 @@ using SkillSphere.Application; using SkillSphere.Infrastructure; +using SkillSphere.Infrastructure.Data; using SkillSphere.Web; using SkillSphere.Web.Infrastructure; -using SkillSphere.Infrastructure.Data; var builder = WebApplication.CreateBuilder(args); @@ -13,7 +13,6 @@ builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); - builder.Services.AddCors(options => { options.AddDefaultPolicy(builder => @@ -44,10 +43,9 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseSwaggerUi3(settings => { - settings.Path = "/api"; + settings.Path = "/api"; settings.DocumentPath = "/api/specification.json"; }); @@ -55,13 +53,16 @@ name: "default", pattern: "{controller}/{action=Index}/{id?}"); +app.MapRazorPages(); + +app.MapFallbackToFile("index.html"); + app.UseExceptionHandler(options => { }); app.Map("/", () => Results.Redirect("/api")); app.MapEndpoints(); - app.UseCors(options => { options.SetIsOriginAllowed(origin => true) diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 1bcb144..141ab0f 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -55,7 +55,12 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] }, "post": { "tags": [ @@ -86,7 +91,12 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] } }, "/api/TodoItems/{id}": { @@ -123,7 +133,12 @@ "200": { "description": "" } - } + }, + "security": [ + { + "JWT": [] + } + ] }, "delete": { "tags": [ @@ -146,7 +161,12 @@ "200": { "description": "" } - } + }, + "security": [ + { + "JWT": [] + } + ] } }, "/api/TodoItems/UpdateDetail/{id}": { @@ -183,7 +203,12 @@ "200": { "description": "" } - } + }, + "security": [ + { + "JWT": [] + } + ] } }, "/api/TodoLists": { @@ -203,7 +228,12 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] }, "post": { "tags": [ @@ -234,7 +264,12 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] } }, "/api/TodoLists/{id}": { @@ -271,7 +306,12 @@ "200": { "description": "" } - } + }, + "security": [ + { + "JWT": [] + } + ] }, "delete": { "tags": [ @@ -294,349 +334,30 @@ "200": { "description": "" } - } - } - }, - "/api/Users/register": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRegister", - "requestBody": { - "x-name": "registration", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/login": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersLogin", - "parameters": [ - { - "name": "useCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "useSessionCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 3 - } - ], - "requestBody": { - "x-name": "login", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" - } - } - } - } - } - } - }, - "/api/Users/refresh": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRefresh", - "requestBody": { - "x-name": "refreshRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshRequest" - } - } - }, - "x-position": 1 }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" - } - } - } - } - } - } - }, - "/api/Users/confirmEmail": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersConfirmEmail", - "parameters": [ - { - "name": "userId", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, + "security": [ { - "name": "code", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "changedEmail", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 3 - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/resendConfirmationEmail": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResendConfirmationEmail", - "requestBody": { - "x-name": "resendRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResendConfirmationEmailRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/forgotPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersForgotPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForgotPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/resetPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResetPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResetPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } + "JWT": [] } - } + ] } }, - "/api/Users/manage/2fa": { + "/api/Users": { "post": { "tags": [ "Users" ], - "operationId": "PostApiUsersManage2fa", + "operationId": "Authenticate", "requestBody": { - "x-name": "tfaRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - } - } - }, - "/api/Users/manage/info": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersManageInfo", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - } - }, - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersManageInfo", - "requestBody": { - "x-name": "infoRequest", + "x-name": "command", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InfoRequest" + "$ref": "#/components/schemas/AuthUserCommand" } } }, + "required": true, "x-position": 1 }, "responses": { @@ -645,23 +366,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" + "type": "string" } } } - }, - "404": { - "description": "" } } } @@ -920,226 +628,15 @@ } } }, - "HttpValidationProblemDetails": { - "allOf": [ - { - "$ref": "#/components/schemas/ProblemDetails" - }, - { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "errors": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - ] - }, - "ProblemDetails": { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - } - }, - "RegisterRequest": { + "AuthUserCommand": { "type": "object", "additionalProperties": false, "properties": { "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "AccessTokenResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "tokenType": { - "type": "string" - }, - "accessToken": { - "type": "string" - }, - "expiresIn": { - "type": "integer", - "format": "int64" - }, - "refreshToken": { - "type": "string" - } - } - }, - "LoginRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "twoFactorCode": { "type": "string", "nullable": true }, - "twoFactorRecoveryCode": { - "type": "string", - "nullable": true - } - } - }, - "RefreshRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "refreshToken": { - "type": "string" - } - } - }, - "ResendConfirmationEmailRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ForgotPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ResetPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "resetCode": { - "type": "string" - }, - "newPassword": { - "type": "string" - } - } - }, - "TwoFactorResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "sharedKey": { - "type": "string" - }, - "recoveryCodesLeft": { - "type": "integer", - "format": "int32" - }, - "recoveryCodes": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "isTwoFactorEnabled": { - "type": "boolean" - }, - "isMachineRemembered": { - "type": "boolean" - } - } - }, - "TwoFactorRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "enable": { - "type": "boolean", - "nullable": true - }, - "twoFactorCode": { - "type": "string", - "nullable": true - }, - "resetSharedKey": { - "type": "boolean" - }, - "resetRecoveryCodes": { - "type": "boolean" - }, - "forgetMachine": { - "type": "boolean" - } - } - }, - "InfoResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "isEmailConfirmed": { - "type": "boolean" - } - } - }, - "InfoRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "newEmail": { - "type": "string", - "nullable": true - }, - "newPassword": { - "type": "string", - "nullable": true - }, - "oldPassword": { + "password": { "type": "string", "nullable": true } @@ -1167,6 +664,19 @@ } } } + }, + "securitySchemes": { + "JWT": { + "type": "apiKey", + "description": "Please insert token: JWT {your JWT token}.", + "name": "Authorization", + "in": "header" + } + } + }, + "security": [ + { + "JWT": [] } - } + ] } \ No newline at end of file From 1cf31bfb132035d3fa93b2d9a9000557fd609643 Mon Sep 17 00:00:00 2001 From: Tomas Simko <72190589+TomassSimko@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:15:23 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=94=91=20Login/Auth=20registration=20?= =?UTF-8?q?with=20protected=20routes=20and=20JWT=20token=20gen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/Commands/AuthorizeUser.cs | 40 ++++ src/Application/Auth/Commands/RegisterUser.cs | 35 ++++ .../Common/Interfaces/IIdentityService.cs | 4 +- src/Application/Common/Models/AuthResult.cs | 11 ++ .../GetWeatherForecastsQuery.cs | 7 +- .../Authentication/JwtTokenConfig.cs | 15 ++ .../Authentication/Model/TokenModel.cs | 15 ++ .../Authentication/Services/JwtGenerator.cs | 60 ++++++ .../Data/ApplicationDbContextInitialiser.cs | 1 + src/Infrastructure/Data/InitialData.cs | 29 +++ src/Infrastructure/DependencyInjection.cs | 56 +++++- .../Identity/IdentityResultExtensions.cs | 6 + .../Identity/IdentityService.cs | 38 +++- src/Infrastructure/Identity/Roles.cs | 6 + src/UI/package-lock.json | 9 + src/UI/package.json | 1 + src/UI/src/app/app-routing.module.ts | 1 + .../auth/interceptors/auth.interceptor.ts | 73 +++---- src/UI/src/app/core/auth/models/login.ts | 7 +- src/UI/src/app/core/auth/models/register.ts | 7 + .../src/app/core/auth/service/auth-guard.ts | 11 +- .../src/app/core/auth/service/auth.service.ts | 42 ++-- src/UI/src/app/modules/auth/auth.module.ts | 2 +- .../auth/pages/login/login.component.html | 8 +- .../auth/pages/login/login.component.spec.ts | 2 + .../auth/pages/login/login.component.ts | 26 +-- .../pages/register/register.component.html | 43 ++-- .../pages/register/register.component.spec.ts | 3 +- .../auth/pages/register/register.component.ts | 76 ++++++- .../pages/dashboard/dashboard.component.ts | 19 +- .../navbar/navbar.component.spec.ts | 3 +- .../profile-menu/profile-menu.component.html | 3 +- .../profile-menu.component.spec.ts | 3 +- .../profile-menu/profile-menu.component.ts | 36 +++- .../modules/layout/layout-routing.module.ts | 3 +- .../shared/service/alert-service.service.ts | 1 + src/UI/src/main.ts | 3 +- src/UI/yarn.lock | 5 + src/Web/DependencyInjection.cs | 6 +- .../Endpoints/{Users.cs => AuthorizeUser.cs} | 19 +- src/Web/Endpoints/WeatherForecasts.cs | 2 +- src/Web/wwwroot/api/specification.json | 187 +++++++++++++----- tests/Application.FunctionalTests/Testing.cs | 1 + 43 files changed, 708 insertions(+), 217 deletions(-) create mode 100644 src/Application/Auth/Commands/AuthorizeUser.cs create mode 100644 src/Application/Auth/Commands/RegisterUser.cs create mode 100644 src/Application/Common/Models/AuthResult.cs create mode 100644 src/Infrastructure/Authentication/JwtTokenConfig.cs create mode 100644 src/Infrastructure/Authentication/Model/TokenModel.cs create mode 100644 src/Infrastructure/Authentication/Services/JwtGenerator.cs create mode 100644 src/Infrastructure/Data/InitialData.cs create mode 100644 src/Infrastructure/Identity/Roles.cs create mode 100644 src/UI/src/app/core/auth/models/register.ts rename src/Web/Endpoints/{Users.cs => AuthorizeUser.cs} (60%) diff --git a/src/Application/Auth/Commands/AuthorizeUser.cs b/src/Application/Auth/Commands/AuthorizeUser.cs new file mode 100644 index 0000000..dfe6005 --- /dev/null +++ b/src/Application/Auth/Commands/AuthorizeUser.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using SkillSphere.Application.Common.Interfaces; +using SkillSphere.Application.Common.Models; + +namespace SkillSphere.Application.Auth.Commands; + +public record AuthUserCommand : IRequest +{ + public string? Email { get; init; } + public string? Password { get; init; } +} + +public class AuthenticateCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IIdentityService _userService; + + public AuthenticateCommandHandler(IApplicationDbContext context,IIdentityService userService) + { + _context = context; + _userService = userService; + } + public async Task Handle(AuthUserCommand request, CancellationToken cancellationToken) + { + // Assuming request.Email and request.Password are provided + + Guard.Against.NullOrEmpty(request.Email, nameof(request.Email)); + Guard.Against.NullOrEmpty(request.Password, nameof(request.Password)); + + return await _userService.AuthenticateAsync(request.Email, request.Password); + //return await _userService.AuthenticateAsync(request.Email, request.Password); + + + } + + +} + + + diff --git a/src/Application/Auth/Commands/RegisterUser.cs b/src/Application/Auth/Commands/RegisterUser.cs new file mode 100644 index 0000000..753ae0d --- /dev/null +++ b/src/Application/Auth/Commands/RegisterUser.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using SkillSphere.Application.Common.Interfaces; +using SkillSphere.Application.Common.Models; + +namespace SkillSphere.Application.Auth.Commands; + +public record RegisterUserCommand : IRequest +{ + public string? Email { get; init; } + public string? Password { get; init; } +} + +public class RegisterUserCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IIdentityService _userService; + + public RegisterUserCommandHandler(IApplicationDbContext context,IIdentityService userService) + { + _context = context; + _userService = userService; + } + public async Task Handle(RegisterUserCommand request, CancellationToken cancellationToken) + { + Guard.Against.NullOrEmpty(request.Email, nameof(request.Email)); + Guard.Against.NullOrEmpty(request.Password, nameof(request.Password)); + + var result = await _userService.CreateUserAsync(request.Email, request.Password); + return result; + } + +} + + + diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index e14e00e..5e86ec1 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -10,7 +10,9 @@ public interface IIdentityService Task AuthorizeAsync(string userId, string policyName); - Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password); + Task CreateUserAsync(string userName, string password); Task DeleteUserAsync(string userId); + + Task AuthenticateAsync(string requestEmail, string requestPassword); } diff --git a/src/Application/Common/Models/AuthResult.cs b/src/Application/Common/Models/AuthResult.cs new file mode 100644 index 0000000..c0baef8 --- /dev/null +++ b/src/Application/Common/Models/AuthResult.cs @@ -0,0 +1,11 @@ +namespace SkillSphere.Application.Common.Models; + +public class AuthResult +{ + public string? Token { get; init; } + + public int ExpiresIn { get; init; } + public string? UserId { get; init; } + public string? Email { get; init; } + +} diff --git a/src/Application/Features/WeatherForecasts/Queries/GetWeatherForecasts/GetWeatherForecastsQuery.cs b/src/Application/Features/WeatherForecasts/Queries/GetWeatherForecasts/GetWeatherForecastsQuery.cs index 57aba5b..efdd8b9 100644 --- a/src/Application/Features/WeatherForecasts/Queries/GetWeatherForecasts/GetWeatherForecastsQuery.cs +++ b/src/Application/Features/WeatherForecasts/Queries/GetWeatherForecasts/GetWeatherForecastsQuery.cs @@ -1,7 +1,12 @@ -namespace SkillSphere.Application.WeatherForecasts.Queries.GetWeatherForecasts; +using SkillSphere.Application.Common.Security; +using SkillSphere.Domain.Constants; +namespace SkillSphere.Application.WeatherForecasts.Queries.GetWeatherForecasts; + +[Authorize(Roles = Roles.Administrator)] public record GetWeatherForecastsQuery : IRequest>; + public class GetWeatherForecastsQueryHandler : IRequestHandler> { private static readonly string[] Summaries = new[] diff --git a/src/Infrastructure/Authentication/JwtTokenConfig.cs b/src/Infrastructure/Authentication/JwtTokenConfig.cs new file mode 100644 index 0000000..c13fb25 --- /dev/null +++ b/src/Infrastructure/Authentication/JwtTokenConfig.cs @@ -0,0 +1,15 @@ + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace SkillSphere.Infrastructure.Authentication +{ + + public class JwtTokenConfig + { + public string Secret { get; init; } = ""; + } +} + + + diff --git a/src/Infrastructure/Authentication/Model/TokenModel.cs b/src/Infrastructure/Authentication/Model/TokenModel.cs new file mode 100644 index 0000000..181461b --- /dev/null +++ b/src/Infrastructure/Authentication/Model/TokenModel.cs @@ -0,0 +1,15 @@ +namespace SkillSphere.Infrastructure.Authentication.Model; + +public record TokenModel +{ + public string TokenType { get; } + public string AccessToken { get; } + public DateTime ExpiresAt { get; } + + public TokenModel(string tokenType, string accessToken, DateTime expiresAt) + => (TokenType, AccessToken, ExpiresAt) = (tokenType, accessToken, expiresAt); + + public int GetRemainingLifetimeSeconds() + => Math.Max(0, (int)(ExpiresAt - DateTime.Now).TotalSeconds); +} + diff --git a/src/Infrastructure/Authentication/Services/JwtGenerator.cs b/src/Infrastructure/Authentication/Services/JwtGenerator.cs new file mode 100644 index 0000000..445df50 --- /dev/null +++ b/src/Infrastructure/Authentication/Services/JwtGenerator.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using SkillSphere.Infrastructure.Authentication.Model; +using SkillSphere.Infrastructure.Identity; + +namespace SkillSphere.Infrastructure.Authentication.Services; + + + public interface IJwtTokenGen + { + ValueTask CreateToken(ApplicationUser user, CancellationToken cancellationToken = default); + } + + public class JwtGenerator : IJwtTokenGen + { + private readonly JwtTokenConfig _authSettings; + + public JwtGenerator(JwtTokenConfig authSettings) + { + _authSettings = authSettings; + } + + public ValueTask CreateToken(ApplicationUser user, CancellationToken cancellationToken = default) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + Debug.Assert(_authSettings.Secret != null, "_authSettings.Secret != null"); + var key = Encoding.UTF8.GetBytes(_authSettings.Secret); + + Debug.Assert(user.Email != null, "user.Email != null"); + Debug.Assert(user.UserName != null, "user.UserName != null"); + + var claimList = new List + { + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim(JwtRegisteredClaimNames.Sub, user.Id), + new Claim(JwtRegisteredClaimNames.Name, user.UserName) + }; + + // claimList.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Audience = null, + Issuer = null, + Subject = new ClaimsIdentity(claimList), + Expires = DateTime.UtcNow.AddSeconds(20), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + var jwtToken = tokenHandler.WriteToken(token); + + return new ValueTask(jwtToken); + } + } + diff --git a/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs b/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs index b17017b..06be3a3 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using skillSphere.Infrastructure.Data; +using Roles = SkillSphere.Domain.Constants.Roles; namespace SkillSphere.Infrastructure.Data; diff --git a/src/Infrastructure/Data/InitialData.cs b/src/Infrastructure/Data/InitialData.cs new file mode 100644 index 0000000..877a832 --- /dev/null +++ b/src/Infrastructure/Data/InitialData.cs @@ -0,0 +1,29 @@ +using SkillSphere.Infrastructure.Identity; + +namespace SkillSphere.Infrastructure.Data; + +public class InitialData +{ + public static List Users { get; } + + static InitialData() + { + Users = new List + { + new ApplicationUser + { + Id = Guid.NewGuid().ToString(), + UserName = "admin", + Email = "admin@gmail.com", + SecurityStamp = Guid.NewGuid().ToString() + }, + new ApplicationUser + { + Id = Guid.NewGuid().ToString(), + UserName = "developer", + Email = "developer@gmail.com", + SecurityStamp = Guid.NewGuid().ToString() + } + }; + } +} diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 15730fe..43f7901 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -1,15 +1,21 @@ -using Microsoft.AspNetCore.Identity; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; using SkillSphere.Application.Common.Interfaces; using SkillSphere.Domain.Constants; +using SkillSphere.Infrastructure.Authentication; +using SkillSphere.Infrastructure.Authentication.Services; using skillSphere.Infrastructure.Data; using SkillSphere.Infrastructure.Data; using SkillSphere.Infrastructure.Data.Interceptors; using SkillSphere.Infrastructure.Identity; using testSphere.Infrastructure.Data.Interceptors; +using Roles = SkillSphere.Domain.Constants.Roles; namespace SkillSphere.Infrastructure; @@ -35,20 +41,58 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddScoped(); + // Setting up Identity services .AddDefaultIdentity() .AddRoles() .AddEntityFrameworkStores(); - + // Set up Time services.AddSingleton(TimeProvider.System); + + // Register Identity Service and its Interfaces services.AddTransient(); + + services.AddTransient(); - services - .AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); + // Add Authentication schema for JWT + + var jwtOptions = new JwtTokenConfig(); + services.AddSingleton(jwtOptions); + + // Setting up API JWT Authentication + services.AddAuthorization().AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + ValidateIssuer = false, + ValidateAudience = false, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)), + ClockSkew = TimeSpan.Zero + }; + }); + + + + // services + // .AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); + + + + + // => Adding policy for role Administrator - services.AddAuthorization(options => - options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator))); + // services.AddAuthorization(options => + // options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator))); return services; diff --git a/src/Infrastructure/Identity/IdentityResultExtensions.cs b/src/Infrastructure/Identity/IdentityResultExtensions.cs index df7e02c..a31cd6e 100644 --- a/src/Infrastructure/Identity/IdentityResultExtensions.cs +++ b/src/Infrastructure/Identity/IdentityResultExtensions.cs @@ -11,4 +11,10 @@ public static Result ToApplicationResult(this IdentityResult result) ? Result.Success() : Result.Failure(result.Errors.Select(e => e.Description)); } + public static Result ToResult(this IdentityResult result) + { + return result.Succeeded + ? Result.Success() + : Result.Failure(result.Errors.Select(e => e.Description)); + } } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 0e6a0e6..5926cb9 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,8 +1,11 @@ -using SkillSphere.Application.Common.Interfaces; +using System.Collections.Immutable; +using System.Security.Authentication; +using SkillSphere.Application.Common.Interfaces; using SkillSphere.Application.Common.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using SkillSphere.Infrastructure.Authentication.Services; namespace SkillSphere.Infrastructure.Identity; @@ -11,15 +14,17 @@ public class IdentityService : IIdentityService private readonly UserManager _userManager; private readonly IUserClaimsPrincipalFactory _userClaimsPrincipalFactory; private readonly IAuthorizationService _authorizationService; + private readonly IJwtTokenGen _jwtTokenGen; public IdentityService( UserManager userManager, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, IJwtTokenGen jwtTokenGen) { _userManager = userManager; _userClaimsPrincipalFactory = userClaimsPrincipalFactory; _authorizationService = authorizationService; + _jwtTokenGen = jwtTokenGen; } public async Task GetUserNameAsync(string userId) @@ -29,17 +34,17 @@ public IdentityService( return user.UserName; } - public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password) + public async Task CreateUserAsync(string email, string password) { var user = new ApplicationUser { - UserName = userName, - Email = userName, + UserName = email, + Email = email, }; var result = await _userManager.CreateAsync(user, password); - return (result.ToApplicationResult(), user.Id); + return result.ToApplicationResult(); } public async Task IsInRoleAsync(string userId, string role) @@ -72,6 +77,27 @@ public async Task DeleteUserAsync(string userId) return user != null ? await DeleteUserAsync(user) : Result.Success(); } + public async Task AuthenticateAsync(string requestEmail, string requestPassword) + { + var user = await _userManager.FindByEmailAsync(requestEmail); + if (user == null) throw new UnauthorizedAccessException(); + + + var login = await _userManager.CheckPasswordAsync(user, requestPassword); + if (!login) throw new UnauthorizedAccessException(); + + var token = _jwtTokenGen.CreateToken(user); + + return new AuthResult + { + UserId = user.Id, + Email = user.Email, + Token = token.Result, + ExpiresIn = 20, + }; + } + + public async Task DeleteUserAsync(ApplicationUser user) { var result = await _userManager.DeleteAsync(user); diff --git a/src/Infrastructure/Identity/Roles.cs b/src/Infrastructure/Identity/Roles.cs new file mode 100644 index 0000000..d6e3392 --- /dev/null +++ b/src/Infrastructure/Identity/Roles.cs @@ -0,0 +1,6 @@ +namespace SkillSphere.Infrastructure.Identity; + +public abstract class Roles +{ + public const string Administrator = nameof(Administrator); +} \ No newline at end of file diff --git a/src/UI/package-lock.json b/src/UI/package-lock.json index adcf53f..717fc1d 100755 --- a/src/UI/package-lock.json +++ b/src/UI/package-lock.json @@ -27,6 +27,7 @@ "dotenv": "^16.3.1", "flowbite": "^1.8.1", "i": "^0.3.7", + "jwt-decode": "^4.0.0", "ngx-smart-modal": "^14.0.2", "ngx-toastr": "^17.0.2", "npm": "^10.1.0", @@ -8052,6 +8053,14 @@ "node >= 0.2.0" ] }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/karma": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", diff --git a/src/UI/package.json b/src/UI/package.json index c2089fa..e56bc1d 100755 --- a/src/UI/package.json +++ b/src/UI/package.json @@ -33,6 +33,7 @@ "dotenv": "^16.3.1", "flowbite": "^1.8.1", "i": "^0.3.7", + "jwt-decode": "^4.0.0", "ngx-smart-modal": "^14.0.2", "ngx-toastr": "^17.0.2", "npm": "^10.1.0", diff --git a/src/UI/src/app/app-routing.module.ts b/src/UI/src/app/app-routing.module.ts index 5d01237..948a8d0 100755 --- a/src/UI/src/app/app-routing.module.ts +++ b/src/UI/src/app/app-routing.module.ts @@ -17,6 +17,7 @@ const routes: Routes = [ { path: 'courses', loadChildren: () => import('./modules/management/management.module').then((m) => m.ManagementModule), + canActivate: [AuthGuard], }, { path: '**', redirectTo: 'error/404' }, diff --git a/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts index 48f079c..6a18ce6 100644 --- a/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts +++ b/src/UI/src/app/core/auth/interceptors/auth.interceptor.ts @@ -1,57 +1,46 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; import { environment } from '../../../../environments/environment'; import { Observable, catchError, switchMap, throwError } from 'rxjs'; import { AuthService } from '../service/auth.service'; - - +import { Router } from '@angular/router'; +import { jwtDecode } from "jwt-decode"; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) - export class AuthInterceptor implements HttpInterceptor { - authService = inject(AuthService); - refresh = false; - constructor() {} + constructor(private authService: AuthService, private router: Router) {} - + intercept(req: HttpRequest, next: HttpHandler): any { + if (!req.url.includes(environment.baseUrl)) { + return next.handle(req); + } - intercept(req: HttpRequest, next: HttpHandler): Observable> { - - if (!req.url.includes(environment.baseUrl)) - return next.handle(req); - const token = this.authService.getUserToken(); - if (!token) - return next.handle(req); - - if(this.authService.isSignedIn){ - req = req.clone({ - headers: req.headers.set('Authorization', token) + if (token && this.authService.isSignedIn()) { + req = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${token}`), }); } - return next.handle(req); - // return next.handle(cloned).pipe(catchError((err: HttpErrorResponse) => { - // if (err.status === 401 && !this.refresh) { - // this.refresh = true; - - // return this.http.post('http://localhost:5001/api/Users/refresh', {}, {withCredentials: true}).pipe( - // switchMap((res: any) => { - // this.authService.setUserToken(res.accessToken); - - // return next.handle(cloned.clone({ - // setHeaders: { - // Authorization: `Authorization ${token}` - // } - // })); - // }) - // ); - // } - // this.refresh = false; - // return throwError(() => err); - // })); -} -} + return next.handle(req).pipe( + catchError((err: HttpErrorResponse) => { + if (err.status === 0) { + this.authService.signOut(); + this.router.navigate(['/auth/login']); + } + + return throwError(() => err); + }) + ); + } +} \ No newline at end of file diff --git a/src/UI/src/app/core/auth/models/login.ts b/src/UI/src/app/core/auth/models/login.ts index 834f4d3..6a98bfe 100644 --- a/src/UI/src/app/core/auth/models/login.ts +++ b/src/UI/src/app/core/auth/models/login.ts @@ -1,8 +1,9 @@ export interface AuthResponse { - tokenType: string, - accessToken:string, + token: string, + email: string, + userId: string, expiresIn: number, - refreshToken: string, + // refreshToken: string, } \ No newline at end of file diff --git a/src/UI/src/app/core/auth/models/register.ts b/src/UI/src/app/core/auth/models/register.ts new file mode 100644 index 0000000..9f2f373 --- /dev/null +++ b/src/UI/src/app/core/auth/models/register.ts @@ -0,0 +1,7 @@ + +export interface Register { + succeeded: boolean, + errors: string[] + + } + \ No newline at end of file diff --git a/src/UI/src/app/core/auth/service/auth-guard.ts b/src/UI/src/app/core/auth/service/auth-guard.ts index ce5dfaa..fd8327e 100644 --- a/src/UI/src/app/core/auth/service/auth-guard.ts +++ b/src/UI/src/app/core/auth/service/auth-guard.ts @@ -1,15 +1,16 @@ import { inject } from "@angular/core"; -import { Router } from "@angular/router"; +import { Router, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; export const AuthGuard = () => { const currentUserService = inject(AuthService); const router = inject(Router); - if (!currentUserService.isSignedIn) { - return false; - } else { - router.navigate(['/auth/login']); + if (currentUserService.isSignedIn()) { return true; } + + router.navigate(['/auth/login']); + + return false; }; diff --git a/src/UI/src/app/core/auth/service/auth.service.ts b/src/UI/src/app/core/auth/service/auth.service.ts index c1e77a4..6b0f764 100644 --- a/src/UI/src/app/core/auth/service/auth.service.ts +++ b/src/UI/src/app/core/auth/service/auth.service.ts @@ -5,23 +5,27 @@ import { shareReplay } from 'rxjs/operators'; import { tap } from 'rxjs/operators'; import {BehaviorSubject, Observable} from 'rxjs'; import { AuthResponse } from '../models/login'; +import { Register } from '../models/register'; @Injectable({ providedIn: 'root' }) export class AuthService { + + public signInState: Observable; private _signInState = new BehaviorSubject(null); constructor(private _http: HttpClient) { this.signInState = this._signInState.asObservable(); - const userData = this.getStoredUserData(); + if (userData != null) { this._signInState.next(userData); } + } public authenticate(email: string, password: string): Observable> { @@ -29,27 +33,37 @@ export class AuthService { email: email, password: password }; - return this._http.post(`${environment.baseUrl}/Users/login`, loginData, { observe: 'response' }) + return this._http.post(`${environment.baseUrl}/auth/authenticate`, loginData, { observe: 'response' }) .pipe( tap(res => this.signIn(res.body)), shareReplay() ); } + public register(email: string, password: string): Observable { + const registrationData = { + email: email, + password: password + }; + return this._http.post(`${environment.baseUrl}/auth/register`, registrationData); + + } + private signIn(data: AuthResponse) { const expiresAt = new Date(); expiresAt.setTime(Date.now() + (data.expiresIn * 1000)); localStorage.setItem('auth_userData', JSON.stringify(data)); - localStorage.setItem('auth_tokenString', `${data.tokenType} ${data.accessToken}`); + localStorage.setItem('auth_jwt', `${data.token}`); localStorage.setItem('auth_tokenExpiresAt', expiresAt.getTime().toString()); + this._signInState.next(data); } public signOut() { + localStorage.removeItem('auth_jwt'); localStorage.removeItem('auth_userData'); - localStorage.removeItem('auth_tokenString'); localStorage.removeItem('auth_tokenExpiresAt'); this._signInState.next(null); @@ -59,23 +73,17 @@ export class AuthService { return this._signInState.value != null; } - public setUserToken(data: AuthResponse) { - const expiresAt = new Date(); - expiresAt.setTime(Date.now() + (data.expiresIn * 1000)); - - localStorage.setItem('auth_userData', JSON.stringify(data)); - localStorage.setItem('auth_tokenString', `${data.tokenType} ${data.accessToken}`); - localStorage.setItem('auth_tokenExpiresAt', expiresAt.getTime().toString()); - this._signInState.next(data); - } - public getUserToken() { - return localStorage.getItem('auth_tokenString'); + return localStorage.getItem('auth_jwt'); } - public getValidityDays() { - return (+localStorage.getItem('auth_tokenExpiresAt') - Date.now()) / 1000 / (3600 * 24); + public isTokenValid(): boolean { + const expiresAt = +localStorage.getItem('auth_tokenExpiresAt'); + const currentTime = Date.now(); + + return expiresAt > currentTime; } + private getStoredUserData(): AuthResponse { return JSON.parse(localStorage.getItem('auth_userData')) as AuthResponse; diff --git a/src/UI/src/app/modules/auth/auth.module.ts b/src/UI/src/app/modules/auth/auth.module.ts index b71229e..79bbf1f 100644 --- a/src/UI/src/app/modules/auth/auth.module.ts +++ b/src/UI/src/app/modules/auth/auth.module.ts @@ -12,10 +12,10 @@ import { AuthService } from 'src/app/core/auth/service/auth.service'; CommonModule, AuthRoutingModule, HttpClientModule, + ], providers: [ HttpClientModule, - AuthService, HttpClient ] }) diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.html b/src/UI/src/app/modules/auth/pages/login/login.component.html index 39f6670..3177abc 100644 --- a/src/UI/src/app/modules/auth/pages/login/login.component.html +++ b/src/UI/src/app/modules/auth/pages/login/login.component.html @@ -47,8 +47,6 @@

    - -
    @@ -58,11 +56,11 @@

    -
    +
    - Not a Member yet? Register + Not a Member yet? Register
    \ No newline at end of file diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts b/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts index 31e6251..fd1ca62 100644 --- a/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts +++ b/src/UI/src/app/modules/auth/pages/login/login.component.spec.ts @@ -5,6 +5,7 @@ import { HttpClientModule } from '@angular/common/http'; import { RouterTestingModule } from '@angular/router/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ToastrModule } from 'ngx-toastr'; describe('LoginComponent', () => { let component: LoginComponent; @@ -18,6 +19,7 @@ describe('LoginComponent', () => { RouterTestingModule, ReactiveFormsModule, HttpClientTestingModule, + ToastrModule.forRoot() ], }); fixture = TestBed.createComponent(LoginComponent); diff --git a/src/UI/src/app/modules/auth/pages/login/login.component.ts b/src/UI/src/app/modules/auth/pages/login/login.component.ts index 0e2c7c1..3be4c27 100644 --- a/src/UI/src/app/modules/auth/pages/login/login.component.ts +++ b/src/UI/src/app/modules/auth/pages/login/login.component.ts @@ -5,15 +5,9 @@ import { Router, RouterLink } from '@angular/router'; import { Subscription, timer } from 'rxjs'; import { AuthService } from 'src/app/core/auth/service/auth.service'; import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { ToastrService } from 'ngx-toastr'; -enum LocalLoginState { - None, - Waiting, - Success, - ErrorWrongData, - ErrorOther -} @Component({ selector: 'app-login', @@ -26,7 +20,7 @@ enum LocalLoginState { NgClass, NgIf, ], - providers: [AuthService,HttpClient], + providers: [HttpClient], templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) @@ -34,29 +28,22 @@ export class LoginComponent implements OnInit, OnDestroy{ form!: FormGroup; submitted = false; passwordTextType!: boolean; - formSubmitAttempt: boolean; private sub: Subscription; - constructor(private readonly _formBuilder: FormBuilder, private _router: Router,private authService: AuthService) {} + constructor(private readonly _formBuilder: FormBuilder, private _router: Router,private authService: AuthService, private toastr: ToastrService) {} - ngOnInit(): void { this.form = this._formBuilder.group({ - email: ['test3@gmail.com', [Validators.required, Validators.email]], - password: ['Admin1!', Validators.required], + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], }); } - ngOnDestroy(): void { if(this.sub) this.sub.unsubscribe(); } - - localLoginState = LocalLoginState.None; - get localLoginStates() { return LocalLoginState; } - get f() { return this.form.controls; } @@ -79,8 +66,9 @@ export class LoginComponent implements OnInit, OnDestroy{ this._router.navigate(['/dashboard']); }, error: (err: any) => { + this.toastr.error(JSON.stringify(err)); // Handle errors here - // 401 + // 4013 // display alert } }); diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.html b/src/UI/src/app/modules/auth/pages/register/register.component.html index 4edba02..9ab30ab 100644 --- a/src/UI/src/app/modules/auth/pages/register/register.component.html +++ b/src/UI/src/app/modules/auth/pages/register/register.component.html @@ -1,4 +1,4 @@ -
    +

    Register ! @@ -7,58 +7,51 @@

    - + +
    +
    Required field
    +
    Email must be an email address valid
    +
    - + +
    +
    Required field
    +
    - -
    -
    -
    -
    -
    -
    - Use 8 or more characters with a mix of letters, numbers & symbols. -
    - - -
    +
    -
    diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts b/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts index 008dca6..5a8b95b 100644 --- a/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts +++ b/src/UI/src/app/modules/auth/pages/register/register.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RegisterComponent } from './register.component'; import { HttpClientModule } from '@angular/common/http'; import { RouterTestingModule } from '@angular/router/testing'; +import { ToastrModule } from 'ngx-toastr'; describe('RegisterComponent', () => { let component: RegisterComponent; @@ -10,7 +11,7 @@ describe('RegisterComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [RegisterComponent,HttpClientModule,RouterTestingModule] + imports: [RegisterComponent,HttpClientModule,RouterTestingModule,ToastrModule.forRoot()] }); fixture = TestBed.createComponent(RegisterComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/auth/pages/register/register.component.ts b/src/UI/src/app/modules/auth/pages/register/register.component.ts index 4cb29fe..69f75f1 100644 --- a/src/UI/src/app/modules/auth/pages/register/register.component.ts +++ b/src/UI/src/app/modules/auth/pages/register/register.component.ts @@ -1,14 +1,80 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { AuthService } from 'src/app/core/auth/service/auth.service'; +import { AlertServiceService } from 'src/app/shared/service/alert-service.service'; +import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-register', standalone: true, - imports: [CommonModule,RouterLink], + imports: [ + CommonModule, + RouterLink, + FormsModule, + HttpClientModule, + RouterLink, + ReactiveFormsModule, + ], templateUrl: './register.component.html', - styleUrls: ['./register.component.scss'] + styleUrls: ['./register.component.scss'], }) -export class RegisterComponent { +export class RegisterComponent implements OnInit { + form!: FormGroup; + submitted = false; + passwordTextType!: boolean; + constructor( + private readonly _formBuilder: FormBuilder, + private _router: Router, + private authService: AuthService, + private toastr: ToastrService + ) {} + + ngOnInit(): void { + this.form = this._formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + } + + get f() { + return this.form.controls; + } + + onRegister() { + const { email, password } = this.form.value; + + if (this.form.invalid) { + return; + } + + this.authService.register(email, password).subscribe({ + next: (response: any) => { + if (response.succeeded) { + this.toastr.success('Registration successful. Please login'); + this._router.navigate(['/auth/login']); + } else { + // Handle registration failure + if (response.errors && response.errors.length > 0) { + const errorMessage = response.errors[0]; + this.toastr.error(errorMessage); + } else { + this.toastr.error('Registration failed: unknown error'); + } + } + }, + error: (error: any) => { + this.toastr.error(error); + }, + }); + } } diff --git a/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts b/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts index 25e4d34..2193303 100644 --- a/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts +++ b/src/UI/src/app/modules/dashboard/pages/dashboard/dashboard.component.ts @@ -4,6 +4,8 @@ import { DashboardHeaderComponent } from '../../components/dashboard-header/dash import { environment } from 'src/environments/environment'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { DashboardServiceService } from '../../service/dashboard-service.service'; +import { AlertServiceService } from 'src/app/shared/service/alert-service.service'; +import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-dashboard', @@ -13,18 +15,19 @@ import { DashboardServiceService } from '../../service/dashboard-service.service styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements OnInit{ - // test = inject(DashboardServiceService) + ds = inject(DashboardServiceService) constructor() { } + + ngOnInit(): void { - - } + this.ds.getDashboardData().subscribe(data => { + + + }); + - // ngOnInit(): void { - // this.ds.getDashboardData().subscribe(data => { - // console.log(data); - // }); - // } + } } diff --git a/src/UI/src/app/modules/layout/components/navbar/navbar.component.spec.ts b/src/UI/src/app/modules/layout/components/navbar/navbar.component.spec.ts index c772328..068b168 100644 --- a/src/UI/src/app/modules/layout/components/navbar/navbar.component.spec.ts +++ b/src/UI/src/app/modules/layout/components/navbar/navbar.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NavbarComponent } from './navbar.component'; import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('NavbarComponent', () => { let component: NavbarComponent; @@ -9,7 +10,7 @@ describe('NavbarComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NavbarComponent,RouterTestingModule] + imports: [NavbarComponent,RouterTestingModule,HttpClientTestingModule] }); fixture = TestBed.createComponent(NavbarComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html index 0142604..ad6dea1 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.html @@ -28,7 +28,7 @@

    Mr.Simpson -

    simpson@boxfactory.com

    +

    {{authResponse?.email}}

    @@ -44,6 +44,7 @@ Settings
  • Logout diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts index 8f6653b..b12adac 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProfileMenuComponent } from './profile-menu.component'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('ProfileMenuComponent', () => { let component: ProfileMenuComponent; @@ -10,7 +11,7 @@ describe('ProfileMenuComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ProfileMenuComponent,RouterTestingModule] + imports: [ProfileMenuComponent,RouterTestingModule,HttpClientTestingModule] }); fixture = TestBed.createComponent(ProfileMenuComponent); component = fixture.componentInstance; diff --git a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts index 4c6b6c4..bd67640 100644 --- a/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts +++ b/src/UI/src/app/modules/layout/components/navbar/profile-menu/profile-menu.component.ts @@ -1,24 +1,44 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule, NgClass } from '@angular/common'; -import { RouterLink } from '@angular/router'; - +import { RouterLink } from '@angular/router'; +import { AuthService } from 'src/app/core/auth/service/auth.service'; +import { Subscription } from 'rxjs'; +import { AuthResponse } from 'src/app/core/auth/models/login'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-profile-menu', standalone: true, - imports: [CommonModule,NgClass,RouterLink], + imports: [CommonModule, NgClass, RouterLink], templateUrl: './profile-menu.component.html', - styleUrls: ['./profile-menu.component.scss'] + styleUrls: ['./profile-menu.component.scss'], }) - export class ProfileMenuComponent implements OnInit { public isMenuOpen = false; + private signInStateSubscription: Subscription; + public authResponse: AuthResponse; - constructor() {} + constructor(private authService: AuthService) {} - ngOnInit(): void {} + ngOnInit() { + this.signInStateSubscription = this.authService.signInState.subscribe( + (authResponse: AuthResponse) => { + this.authResponse = authResponse; + } + ); + } + + ngOnDestroy() { + if (this.signInStateSubscription) { + this.signInStateSubscription.unsubscribe(); + } + } public toggleMenu(): void { this.isMenuOpen = !this.isMenuOpen; } -} \ No newline at end of file + + public logout(): void { + this.authService.signOut(); + } +} diff --git a/src/UI/src/app/modules/layout/layout-routing.module.ts b/src/UI/src/app/modules/layout/layout-routing.module.ts index b0a5e67..16bc945 100644 --- a/src/UI/src/app/modules/layout/layout-routing.module.ts +++ b/src/UI/src/app/modules/layout/layout-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LayoutComponent } from './layout.component'; -import { ManagementComponent } from '../management/management.component'; import { AuthGuard } from 'src/app/core/auth/service/auth-guard'; const routes: Routes = [ @@ -9,7 +8,7 @@ const routes: Routes = [ { path: 'dashboard', component: LayoutComponent, - loadChildren: () => import('../dashboard/dashboard.module').then((m) => m.DashboardModule) + loadChildren: () => import('../dashboard/dashboard.module').then((m) => m.DashboardModule), }, { path: 'courses', diff --git a/src/UI/src/app/shared/service/alert-service.service.ts b/src/UI/src/app/shared/service/alert-service.service.ts index 4f489e1..cccdf12 100644 --- a/src/UI/src/app/shared/service/alert-service.service.ts +++ b/src/UI/src/app/shared/service/alert-service.service.ts @@ -7,6 +7,7 @@ import { Observable, Subject } from 'rxjs'; export class AlertServiceService { private alertSubject = new Subject(); + showSuccess(message: string) { this.alertSubject.next({ type: 'success', message }); } diff --git a/src/UI/src/main.ts b/src/UI/src/main.ts index c63e2fe..cf62266 100755 --- a/src/UI/src/main.ts +++ b/src/UI/src/main.ts @@ -8,6 +8,7 @@ import { AppComponent } from './app/app.component'; import { AppRoutingModule } from './app/app-routing.module'; import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; import { environment } from './environments/environment'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; if (environment.production) { @@ -19,7 +20,7 @@ if (environment.production) { } bootstrapApplication(AppComponent, { - providers: [importProvidersFrom(BrowserModule, AppRoutingModule), + providers: [importProvidersFrom(BrowserModule, AppRoutingModule,HttpClient,HttpClientModule), provideDialogConfig( { enableClose: false, diff --git a/src/UI/yarn.lock b/src/UI/yarn.lock index 5f1fc3c..7ba961b 100644 --- a/src/UI/yarn.lock +++ b/src/UI/yarn.lock @@ -4552,6 +4552,11 @@ just-diff-apply@^5.2.0: just-diff@^6.0.0: version "6.0.2" +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + karma-chrome-launcher@~3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz" diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 74924ee..79522e4 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -44,7 +44,9 @@ public static IServiceCollection AddWebServices(this IServiceCollection services services.AddOpenApiDocument((configure, sp) => { configure.Title = "SkillSphere API"; + configure.Description = "SkillSphere API"; + configure.Version = "v1"; // Add the fluent validations schema processor var fluentValidationSchemaProcessor = sp.CreateScope().ServiceProvider.GetRequiredService(); @@ -58,9 +60,9 @@ public static IServiceCollection AddWebServices(this IServiceCollection services Name = "Authorization", In = OpenApiSecurityApiKeyLocation.Header, Type = OpenApiSecuritySchemeType.ApiKey, - Description = "Please insert token: JWT {your JWT token}." + Description = "Please insert token: Bearer {your JWT token}." }); - + configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); }); diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/AuthorizeUser.cs similarity index 60% rename from src/Web/Endpoints/Users.cs rename to src/Web/Endpoints/AuthorizeUser.cs index ad8a400..836a204 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/AuthorizeUser.cs @@ -4,9 +4,10 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using SkillSphere.Application.Auth; +using SkillSphere.Application.Auth.Commands; using SkillSphere.Application.Common.Interfaces; using SkillSphere.Application.Common.Models; -using SkillSphere.Application.Features; using SkillSphere.Application.TodoLists.Commands.CreateTodoList; using SkillSphere.Domain.Entities; using SkillSphere.Infrastructure.Authentication.Services; @@ -16,18 +17,24 @@ namespace SkillSphere.Web.Endpoints; -public class Users : EndpointGroupBase +public class Auth : EndpointGroupBase { public override void Map(WebApplication app) { app.MapGroup(this) - .MapPost(Authenticate); - + .MapPost(Authenticate, "/authenticate") + .MapPost(Register, "/register"); } - public async Task Authenticate(ISender sender,AuthUserCommand command) + public async Task Authenticate(ISender sender,AuthUserCommand command) { - return await sender.Send(command); + return await sender.Send(command); + } + + public async Task Register(ISender sender,RegisterUserCommand command) + { + return await sender.Send(command); } } + diff --git a/src/Web/Endpoints/WeatherForecasts.cs b/src/Web/Endpoints/WeatherForecasts.cs index c3a95fa..8ae3d13 100644 --- a/src/Web/Endpoints/WeatherForecasts.cs +++ b/src/Web/Endpoints/WeatherForecasts.cs @@ -8,7 +8,7 @@ public class WeatherForecasts : EndpointGroupBase public override void Map(WebApplication app) { app.MapGroup(this) - + .RequireAuthorization() .MapGet(GetWeatherForecasts); } diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 141ab0f..92ba875 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -3,9 +3,74 @@ "openapi": "3.0.0", "info": { "title": "SkillSphere API", - "version": "1.0.0" + "description": "SkillSphere API", + "version": "v1" }, "paths": { + "/api/Auth/authenticate": { + "post": { + "tags": [ + "Auth" + ], + "operationId": "Authenticate", + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthUserCommand" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResult" + } + } + } + } + } + } + }, + "/api/Auth/register": { + "post": { + "tags": [ + "Auth" + ], + "operationId": "Register", + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterUserCommand" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Result" + } + } + } + } + } + } + }, "/api/TodoItems": { "get": { "tags": [ @@ -342,38 +407,6 @@ ] } }, - "/api/Users": { - "post": { - "tags": [ - "Users" - ], - "operationId": "Authenticate", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthUserCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, "/api/WeatherForecasts": { "get": { "tags": [ @@ -394,12 +427,82 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] } } }, "components": { "schemas": { + "AuthResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "token": { + "type": "string", + "nullable": true + }, + "expiresIn": { + "type": "integer", + "format": "int32" + }, + "userId": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + } + } + }, + "AuthUserCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + } + } + }, + "Result": { + "type": "object", + "additionalProperties": false, + "properties": { + "succeeded": { + "type": "boolean" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RegisterUserCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + } + } + }, "PaginatedListOfTodoItemBriefDto": { "type": "object", "additionalProperties": false, @@ -628,20 +731,6 @@ } } }, - "AuthUserCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string", - "nullable": true - }, - "password": { - "type": "string", - "nullable": true - } - } - }, "WeatherForecast": { "type": "object", "additionalProperties": false, @@ -668,7 +757,7 @@ "securitySchemes": { "JWT": { "type": "apiKey", - "description": "Please insert token: JWT {your JWT token}.", + "description": "Please insert token: Bearer {your JWT token}.", "name": "Authorization", "in": "header" } diff --git a/tests/Application.FunctionalTests/Testing.cs b/tests/Application.FunctionalTests/Testing.cs index e5d1099..bf4a687 100644 --- a/tests/Application.FunctionalTests/Testing.cs +++ b/tests/Application.FunctionalTests/Testing.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using SkillSphere.Domain.Constants; using skillSphere.Infrastructure.Data; +using Roles = SkillSphere.Infrastructure.Identity.Roles; namespace SkillSphere.Application.FunctionalTests; From eb9a68c7470b54f09bf7c600df77bcabba6c284f Mon Sep 17 00:00:00 2001 From: Tomas Simko <72190589+TomassSimko@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:22:34 +0100 Subject: [PATCH 5/5] added policy --- src/Infrastructure/DependencyInjection.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 43f7901..347fce5 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -80,19 +80,11 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti ClockSkew = TimeSpan.Zero }; }); - - - - // services - // .AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); - - - + // => Adding policy for role Administrator - - // services.AddAuthorization(options => - // options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator))); + services.AddAuthorization(options => + options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator))); return services;