Add SPA sample using BFF and Spring Cloud Gateway
This commit is contained in:
58
samples/spa-client/src/app/app.component.html
Normal file
58
samples/spa-client/src/app/app.component.html
Normal 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>
|
||||
0
samples/spa-client/src/app/app.component.scss
Normal file
0
samples/spa-client/src/app/app.component.scss
Normal file
75
samples/spa-client/src/app/app.component.ts
Normal file
75
samples/spa-client/src/app/app.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
14
samples/spa-client/src/app/app.config.ts
Normal file
14
samples/spa-client/src/app/app.config.ts
Normal 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]))
|
||||
]
|
||||
};
|
||||
3
samples/spa-client/src/app/app.routes.ts
Normal file
3
samples/spa-client/src/app/app.routes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
3
samples/spa-client/src/app/environment.ts
Normal file
3
samples/spa-client/src/app/environment.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
backendBaseUrl: 'http://127.0.0.1:8080',
|
||||
};
|
||||
24
samples/spa-client/src/app/http.interceptors.ts
Normal file
24
samples/spa-client/src/app/http.interceptors.ts
Normal 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);
|
||||
}
|
||||
15
samples/spa-client/src/index.html
Normal file
15
samples/spa-client/src/index.html
Normal 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>
|
||||
6
samples/spa-client/src/main.ts
Normal file
6
samples/spa-client/src/main.ts
Normal 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));
|
||||
1
samples/spa-client/src/styles.scss
Normal file
1
samples/spa-client/src/styles.scss
Normal file
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
Reference in New Issue
Block a user