diff --git a/Api/Controllers/ProductController.cs b/Api/Controllers/ProductController.cs index 422cbfe..b5eea82 100644 --- a/Api/Controllers/ProductController.cs +++ b/Api/Controllers/ProductController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authorization; namespace Api.Controllers { [ApiController] - //[Authorize] + [Authorize] [Route("api/product")] public class ProductController : ControllerBase { diff --git a/Api/Program.cs b/Api/Program.cs index 185c8fd..1740ed1 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -8,6 +8,8 @@ namespace Api { using Microsoft.EntityFrameworkCore; using Api.Models; + using Microsoft.AspNetCore.Rewrite; + public static class Program { public static void Main(string[] args) @@ -22,12 +24,7 @@ namespace Api }) .AddJwtBearer(options => { - // options.Events = new JwtBearerEvents - // { - // OnTokenValidated = context => Task.CompletedTask, - // OnChallenge = context => Task.CompletedTask - // }; - + options.Authority = builder.Configuration["Authentication:PocketId:Authority"]; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() { @@ -61,47 +58,43 @@ namespace Api var app = builder.Build(); - // Configure the HTTP request pipeline. app.UseSwagger(); app.UseSwaggerUI(); - // app.Use(async (context, next) => - // { - // if (context.Request.Method == HttpMethods.Options) - // { - // context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); - // context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - // context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type"); - // context.Response.StatusCode = StatusCodes.Status204NoContent; - // return; - // } - // await next(); - // }); if (!app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); } - if (!app.Environment.IsDevelopment()) + var routes = new[] { "api", "swagger" }; + var rewriteString = String.Join("|", routes); + var rewriteOptions = new RewriteOptions() + .AddRewrite(@$"^(?!.*?\b({rewriteString}))^(?!.*?\.\b(jpg|jpeg|png|svg|ttf|woff|woff2|html|js|json|css|ico))", "index.html", false); + app.UseRewriter(rewriteOptions); + + + if (app.Environment.IsDevelopment()) + { + var currentDirectory = Directory.GetCurrentDirectory(); + var staticFilePath = Path.Combine(currentDirectory,"../Web/dist/Web/browser"); + app.UseDefaultFiles(new DefaultFilesOptions + { + FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(staticFilePath), + DefaultFileNames = new List { "index.html" } + }); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(staticFilePath), + RequestPath = "" + }); + } + else { app.UseDefaultFiles(); // Uses wwwroot by default app.UseStaticFiles(); - // Angular routing fallback for production - app.Use(async (context, next) => - { - await next(); - var path = context.Request.Path.Value ?? string.Empty; - if (context.Response.StatusCode == 404 && - !System.IO.Path.HasExtension(path) && - !path.StartsWith("/api")) - { - context.Request.Path = "/index.html"; - await next(); - } - }); } - + app.UseCors("AllowAll"); app.UseAuthentication(); app.UseAuthorization(); diff --git a/Web/public/config.json b/Web/public/config.json new file mode 100644 index 0000000..d231463 --- /dev/null +++ b/Web/public/config.json @@ -0,0 +1,3 @@ +{ + "apiEndpoint": "https://localhost:5001" +} \ No newline at end of file diff --git a/Web/src/app/app.config.ts b/Web/src/app/app.config.ts index d65a489..3f08b26 100644 --- a/Web/src/app/app.config.ts +++ b/Web/src/app/app.config.ts @@ -1,15 +1,17 @@ -import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core'; +import { ApplicationConfig, inject, provideAppInitializer, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; import { provideOAuthClient } from 'angular-oauth2-oidc'; +import { AppConfigService } from './services/config.service'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideZonelessChangeDetection(), provideRouter(routes), + provideAppInitializer(() => inject(AppConfigService).loadConfig()), provideHttpClient(), provideOAuthClient({ resourceServer: { diff --git a/Web/src/app/app.route.guard.ts b/Web/src/app/app.route.guard.ts new file mode 100644 index 0000000..af1e8a5 --- /dev/null +++ b/Web/src/app/app.route.guard.ts @@ -0,0 +1,9 @@ +import { CanActivateFn } from '@angular/router'; +import { inject } from '@angular/core'; +import { OAuthService } from 'angular-oauth2-oidc'; + + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(OAuthService); + return authService.hasValidAccessToken(); // returns boolean, Promise, or Observable +}; diff --git a/Web/src/app/app.routes.ts b/Web/src/app/app.routes.ts index 20f679e..3cbc7ea 100644 --- a/Web/src/app/app.routes.ts +++ b/Web/src/app/app.routes.ts @@ -1,9 +1,20 @@ import { Routes } from '@angular/router'; import { Login } from './login/login'; +import { authGuard } from './app.route.guard'; export const routes: Routes = [ { path: 'login', - component: Login + component: Login, + }, + { + path: 'content', + children: [ + { + path: '', + loadComponent: () => import('./content/content').then(m => m.Content), + canActivate: [authGuard], + } + ], } ]; diff --git a/Web/src/app/app.ts b/Web/src/app/app.ts index f984c32..fc01e6e 100644 --- a/Web/src/app/app.ts +++ b/Web/src/app/app.ts @@ -1,16 +1,19 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { Component, OnInit } from '@angular/core'; +import { APP_INITIALIZER, Component, inject, OnInit, provideAppInitializer } from '@angular/core'; import { Router, RouterOutlet } from '@angular/router'; import { DefaultOAuthInterceptor, OAuthService } from 'angular-oauth2-oidc'; +import { AppConfigService } from './services/config.service'; @Component({ selector: 'app-root', imports: [RouterOutlet], - providers: [OAuthService, { - provide: HTTP_INTERCEPTORS, - useClass: DefaultOAuthInterceptor, - multi: true, - }], + providers: [ + OAuthService, + { + provide: HTTP_INTERCEPTORS, + useClass: DefaultOAuthInterceptor, + multi: true, + }], templateUrl: './app.html', styleUrl: './app.scss' }) @@ -28,6 +31,6 @@ export class App implements OnInit { }); } ngOnInit(): void { - this.as.loadDiscoveryDocumentAndLogin(); + this.as.loadDiscoveryDocumentAndLogin().then(() => this.router.navigate(['login'])); } } diff --git a/Web/src/app/content/content.html b/Web/src/app/content/content.html new file mode 100644 index 0000000..fc44e62 --- /dev/null +++ b/Web/src/app/content/content.html @@ -0,0 +1 @@ +{{data()|json}} \ No newline at end of file diff --git a/Web/src/app/content/content.scss b/Web/src/app/content/content.scss new file mode 100644 index 0000000..e69de29 diff --git a/Web/src/app/content/content.spec.ts b/Web/src/app/content/content.spec.ts new file mode 100644 index 0000000..8250188 --- /dev/null +++ b/Web/src/app/content/content.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Content } from './content'; + +describe('Content', () => { + let component: Content; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Content] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Content); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Web/src/app/content/content.ts b/Web/src/app/content/content.ts new file mode 100644 index 0000000..c74aaf3 --- /dev/null +++ b/Web/src/app/content/content.ts @@ -0,0 +1,23 @@ +import { JsonPipe } from '@angular/common'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Component, isDevMode, signal } from '@angular/core'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { AppConfigService } from '../services/config.service'; + +@Component({ + selector: 'app-content', + imports: [JsonPipe], + templateUrl: './content.html', + styleUrl: './content.scss' +}) +export class Content { + data = signal({}); + + constructor(httpClient: HttpClient, readonly as: OAuthService, readonly cs: AppConfigService) { + httpClient.get((isDevMode() ? cs.setting.apiEndpoint : '') + '/api/product', { + headers: new HttpHeaders({ Authorization: `Bearer ${as.getAccessToken()}` }).append('Content-Type', 'application/json') + }).subscribe(data => { + this.data.set(data); + }); + } +} diff --git a/Web/src/app/login/login.html b/Web/src/app/login/login.html index 147cfc4..117eb00 100644 --- a/Web/src/app/login/login.html +++ b/Web/src/app/login/login.html @@ -1 +1,2 @@ -

login works!

+ +Logging in... \ No newline at end of file diff --git a/Web/src/app/login/login.ts b/Web/src/app/login/login.ts index d83f23e..e420c52 100644 --- a/Web/src/app/login/login.ts +++ b/Web/src/app/login/login.ts @@ -1,21 +1,33 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { OAuthService } from 'angular-oauth2-oidc'; @Component({ selector: 'app-login', - imports: [], templateUrl: './login.html', styleUrl: './login.scss' }) export class Login implements OnInit { - constructor(private readonly as: OAuthService, private readonly httpClient: HttpClient) { - } + constructor(readonly as: OAuthService, readonly router: Router) { } + ngOnInit(): void { - this.httpClient.get('https://localhost:5001/api/product', { - headers: new HttpHeaders({ Authorization: `Bearer ${this.as.getAccessToken()}` }).append('Content-Type', 'application/json') + if (this.as.hasValidAccessToken() && this.as.hasValidIdToken()) { + this.getUserInfo(); + } else { + this.as.events.subscribe(event => { + if (event.type === 'token_received') + if (this.as.hasValidIdToken()) { + this.getUserInfo(); + } + }) } - ).subscribe(console.warn); + } + + getUserInfo(): void { + this.as.loadUserProfile().then(value => { + console.log('User profile loaded:', value); + this.router.navigate(['/content']); + }); } } diff --git a/Web/src/app/services/config.service.ts b/Web/src/app/services/config.service.ts new file mode 100644 index 0000000..ea23318 --- /dev/null +++ b/Web/src/app/services/config.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, tap } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class AppConfigService { + private config: any; + + constructor(private readonly http: HttpClient) { } + + loadConfig(): Observable { + return this.http.get('/config.json') + .pipe(tap(data => this.config = data)) + } + + get setting() { + return this.config; + } +}