feat: implement authentication flow and dynamic API configuration #5
This commit is contained in:
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
namespace Api.Controllers
|
namespace Api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
//[Authorize]
|
[Authorize]
|
||||||
[Route("api/product")]
|
[Route("api/product")]
|
||||||
public class ProductController : ControllerBase
|
public class ProductController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace Api
|
|||||||
{
|
{
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Api.Models;
|
using Api.Models;
|
||||||
|
using Microsoft.AspNetCore.Rewrite;
|
||||||
|
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
@@ -22,12 +24,7 @@ namespace Api
|
|||||||
})
|
})
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
// options.Events = new JwtBearerEvents
|
|
||||||
// {
|
|
||||||
// OnTokenValidated = context => Task.CompletedTask,
|
|
||||||
// OnChallenge = context => Task.CompletedTask
|
|
||||||
// };
|
|
||||||
|
|
||||||
options.Authority = builder.Configuration["Authentication:PocketId:Authority"];
|
options.Authority = builder.Configuration["Authentication:PocketId:Authority"];
|
||||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
|
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
|
||||||
{
|
{
|
||||||
@@ -61,47 +58,43 @@ namespace Api
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
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())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseHttpsRedirection();
|
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<string> { "index.html" }
|
||||||
|
});
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(staticFilePath),
|
||||||
|
RequestPath = ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
app.UseDefaultFiles(); // Uses wwwroot by default
|
app.UseDefaultFiles(); // Uses wwwroot by default
|
||||||
app.UseStaticFiles();
|
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.UseCors("AllowAll");
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
3
Web/public/config.json
Normal file
3
Web/public/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"apiEndpoint": "https://localhost:5001"
|
||||||
|
}
|
||||||
@@ -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 { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
import { provideOAuthClient } from 'angular-oauth2-oidc';
|
import { provideOAuthClient } from 'angular-oauth2-oidc';
|
||||||
|
import { AppConfigService } from './services/config.service';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
|
provideAppInitializer(() => inject(AppConfigService).loadConfig()),
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideOAuthClient({
|
provideOAuthClient({
|
||||||
resourceServer: {
|
resourceServer: {
|
||||||
|
|||||||
9
Web/src/app/app.route.guard.ts
Normal file
9
Web/src/app/app.route.guard.ts
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { Login } from './login/login';
|
import { Login } from './login/login';
|
||||||
|
import { authGuard } from './app.route.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
component: Login
|
component: Login,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'content',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () => import('./content/content').then(m => m.Content),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
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 { Router, RouterOutlet } from '@angular/router';
|
||||||
import { DefaultOAuthInterceptor, OAuthService } from 'angular-oauth2-oidc';
|
import { DefaultOAuthInterceptor, OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
import { AppConfigService } from './services/config.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet],
|
||||||
providers: [OAuthService, {
|
providers: [
|
||||||
provide: HTTP_INTERCEPTORS,
|
OAuthService,
|
||||||
useClass: DefaultOAuthInterceptor,
|
{
|
||||||
multi: true,
|
provide: HTTP_INTERCEPTORS,
|
||||||
}],
|
useClass: DefaultOAuthInterceptor,
|
||||||
|
multi: true,
|
||||||
|
}],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
})
|
})
|
||||||
@@ -28,6 +31,6 @@ export class App implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.as.loadDiscoveryDocumentAndLogin();
|
this.as.loadDiscoveryDocumentAndLogin().then(() => this.router.navigate(['login']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
Web/src/app/content/content.html
Normal file
1
Web/src/app/content/content.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<span>{{data()|json}}</span>
|
||||||
0
Web/src/app/content/content.scss
Normal file
0
Web/src/app/content/content.scss
Normal file
23
Web/src/app/content/content.spec.ts
Normal file
23
Web/src/app/content/content.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Content } from './content';
|
||||||
|
|
||||||
|
describe('Content', () => {
|
||||||
|
let component: Content;
|
||||||
|
let fixture: ComponentFixture<Content>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Content]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Content);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
23
Web/src/app/content/content.ts
Normal file
23
Web/src/app/content/content.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
<p>login works!</p>
|
|
||||||
|
Logging in...
|
||||||
@@ -1,21 +1,33 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
imports: [],
|
|
||||||
templateUrl: './login.html',
|
templateUrl: './login.html',
|
||||||
styleUrl: './login.scss'
|
styleUrl: './login.scss'
|
||||||
})
|
})
|
||||||
export class Login implements OnInit {
|
export class Login implements OnInit {
|
||||||
constructor(private readonly as: OAuthService, private readonly httpClient: HttpClient) {
|
|
||||||
|
|
||||||
}
|
constructor(readonly as: OAuthService, readonly router: Router) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.httpClient.get('https://localhost:5001/api/product', {
|
if (this.as.hasValidAccessToken() && this.as.hasValidIdToken()) {
|
||||||
headers: new HttpHeaders({ Authorization: `Bearer ${this.as.getAccessToken()}` }).append('Content-Type', 'application/json')
|
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']);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
Web/src/app/services/config.service.ts
Normal file
19
Web/src/app/services/config.service.ts
Normal file
@@ -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<any> {
|
||||||
|
return this.http.get('/config.json')
|
||||||
|
.pipe(tap(data => this.config = data))
|
||||||
|
}
|
||||||
|
|
||||||
|
get setting() {
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user