Add SPA sample using BFF and Spring Cloud Gateway

This commit is contained in:
Joe Grandja
2024-11-08 14:29:41 -05:00
parent 3a63bf6dd2
commit a6d9c19daa
30 changed files with 14035 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<div>
<nav class="navbar navbar-expand-lg bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="/spring-security.svg" width="40" height="32">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li *ngIf="isAuthenticated" class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Authorize</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" (click)="authorizeMessages()">Messages</a></li>
</ul>
</li>
</ul>
<div class="d-flex">
@if (isAuthenticated) {
<div>
<span class="fs-6 px-3">{{ userName }}</span>
<button class="btn btn-outline-dark" (click)="logout()">Logout</button>
</div>
} @else {
<div>
<button class="btn btn-outline-dark" (click)="login()">Login</button>
</div>
}
</div>
</div>
</div>
</nav>
</div>
<div class="container">
<div *ngIf="messages.length > 0" class="row py-5 justify-content-start">
<div class="col">
<table class="table table-striped caption-top">
<caption>Messages</caption>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Message</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let message of messages; let index = index">
<th scope="row">{{ index + 1 }}</th>
<td>{{ message }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,75 @@
import {Component, OnInit} from '@angular/core';
import {NgIf, NgForOf} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {catchError, of} from 'rxjs';
import {environment} from "./environment";
@Component({
selector: 'app-root',
standalone: true,
imports: [NgIf, NgForOf],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit {
isAuthenticated: boolean = false;
userName: string = '';
messages: string[] = [];
constructor(private http: HttpClient) {
}
ngOnInit(): void {
this.getUserInfo();
this.getMessages();
}
login(): void {
// The Backend is configured to trigger login when unauthenticated
window.location.href = environment.backendBaseUrl;
}
logout(): void {
this.http.post('/logout', null)
.pipe(catchError((error) => {
console.error(error);
return of(null);
}))
.subscribe(() => {
this.isAuthenticated = false;
this.userName = '';
this.messages = [];
});
}
getUserInfo(): void {
this.http.get<any>('/userinfo')
.pipe(catchError((error) => {
console.error(error);
return of(null);
}))
.subscribe((userInfo) => {
if (userInfo) {
this.isAuthenticated = true;
this.userName = userInfo.sub;
}
});
}
authorizeMessages(): void {
// Trigger the Backend to perform the authorization_code grant flow.
// After authorization is complete, the Backend will redirect back to this app.
window.location.href = environment.backendBaseUrl + "/oauth2/authorization/messaging-client-authorization-code";
}
getMessages(): void {
this.http.get<string[]>('/messages')
.pipe(catchError((error) => {
console.error(error);
return of([]);
}))
.subscribe((messages) => {
this.messages = messages;
});
}
}

View File

@@ -0,0 +1,14 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import {provideHttpClient, withInterceptors} from '@angular/common/http';
import { routes } from './app.routes';
import { withCredentialsInterceptor } from './http.interceptors';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptors([withCredentialsInterceptor]))
]
};

View File

@@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View File

@@ -0,0 +1,3 @@
export const environment = {
backendBaseUrl: 'http://127.0.0.1:8080',
};

View File

@@ -0,0 +1,24 @@
import {HttpRequest, HttpHandlerFn, HttpEvent} from '@angular/common/http';
import {Observable} from 'rxjs';
import {environment} from "./environment";
/*
IMPORTANT:
By default, the HttpClient passes the CSRF token via the X-XSRF-TOKEN header using its built-in interceptor.
However, this DOES NOT WORK when absolute URLs are used in HttpClient calls.
Hence, the reason for this interceptor, as it prepends the Backend base URL to the relative URL.
Ensure you only use relative URLs in HttpClient calls for mutating requests (e.g. POST),
otherwise operations such as /logout will not work.
See the reference for further information:
https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security
*/
export function withCredentialsInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
request = request.clone({
url: environment.backendBaseUrl + request.url,
withCredentials: true // This is required to ensure the Session Cookie is passed in every request to the Backend
});
return next(request);
}

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SPA with Backend and Spring Cloud Gateway</title>
<base href="/">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body>
<app-root></app-root>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script>
</body>
</html>

View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */