Merge pull request #15777 from ch4mpy/BAEL-7380
BAEL-7380 : OAuth2 BFF with `spring-addons-starter-oidc`
This commit is contained in:
commit
27465b037e
|
@ -0,0 +1,10 @@
|
|||
**/*.pem
|
||||
**/*.crt
|
||||
**/.env.*
|
||||
**/target/
|
||||
**/build/
|
||||
**/.metadata
|
||||
**/*.project
|
||||
**/*.settings
|
||||
**/*.classpath
|
||||
**/*.factorypath
|
|
@ -0,0 +1,16 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,42 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-ui": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"baseHref": "/angular-ui/",
|
||||
"outputPath": "dist/angular-ui",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-ui:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-ui:build:development"
|
||||
},
|
||||
"local": {
|
||||
"buildTarget": "angular-ui:build:development",
|
||||
"host": "0.0.0.0",
|
||||
"port": 4201
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-ui:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "23e97199-7e93-4604-8730-91fe13971aa4"
|
||||
}
|
||||
}
|
12944
spring-security-modules/spring-security-oauth2-bff/angular-ui/package-lock.json
generated
Normal file
12944
spring-security-modules/spring-security-oauth2-bff/angular-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "angular-ui",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve -c local",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/compiler": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"@angular/platform-browser": "^17.0.0",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||
"@angular/router": "^17.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.0.10",
|
||||
"@angular/cli": "^17.0.10",
|
||||
"@angular/compiler-cli": "^17.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { NavigationComponent } from './navigation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [NavigationComponent],
|
||||
template: `<app-navigation
|
||||
[destination]="['']"
|
||||
label="HOME"
|
||||
></app-navigation>
|
||||
<p>
|
||||
This application is a show-case for an Angular app consuming a REST API
|
||||
through an OAuth2 BFF.
|
||||
</p>`,
|
||||
styles: ``,
|
||||
})
|
||||
export class AboutView {}
|
|
@ -0,0 +1,27 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { AuthenticationComponent } from './auth/authentication.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
HttpClientModule,
|
||||
AuthenticationComponent,
|
||||
],
|
||||
template: `<div style="display: flex;">
|
||||
<div style="margin: auto;"></div>
|
||||
<h1>Angular UI</h1>
|
||||
<div style="margin: auto;"></div>
|
||||
<app-authentication style="margin: auto 1em;"></app-authentication>
|
||||
</div>
|
||||
<div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {}
|
|
@ -0,0 +1,12 @@
|
|||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes), provideHttpClient()],
|
||||
};
|
||||
|
||||
export const reverseProxyUri = 'http://localhost:7080';
|
||||
export const baseUri = `${reverseProxyUri}/angular-ui/`;
|
|
@ -0,0 +1,9 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { AboutView } from './about.view';
|
||||
import { HomeView } from './home.view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeView },
|
||||
{ path: 'about', component: AboutView },
|
||||
{ path: '**', redirectTo: '/' },
|
||||
];
|
|
@ -0,0 +1,27 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { baseUri, reverseProxyUri } from '../app.config';
|
||||
import { UserService } from './user.service';
|
||||
import { LoginComponent } from './login.component';
|
||||
import { LogoutComponent } from './logout.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-authentication',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, LoginComponent, LogoutComponent],
|
||||
template: `<span>
|
||||
<app-login *ngIf="!isAuthenticated"></app-login>
|
||||
<app-logout *ngIf="isAuthenticated"></app-logout>
|
||||
</span>`,
|
||||
styles: ``,
|
||||
})
|
||||
export class AuthenticationComponent {
|
||||
constructor(private user: UserService) {}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return this.user.current.isAuthenticated;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { UserService } from './user.service';
|
||||
import { baseUri } from '../app.config';
|
||||
import { Router } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
enum LoginExperience {
|
||||
IFRAME,
|
||||
DEFAULT,
|
||||
}
|
||||
|
||||
interface LoginOptionDto {
|
||||
label: string;
|
||||
loginUri: string;
|
||||
isSameAuthority: boolean;
|
||||
}
|
||||
|
||||
function loginOptions(http: HttpClient): Observable<Array<LoginOptionDto>> {
|
||||
return http
|
||||
.get('/bff/login-options')
|
||||
.pipe(map((dto: any) => dto as LoginOptionDto[]));
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
template: `<span>
|
||||
<select *ngIf="loginExperiences.length > 1" [formControl]="selectedLoginExperience">
|
||||
<option *ngFor="let le of loginExperiences">
|
||||
{{ loginExperienceLabel(le) }}
|
||||
</option>
|
||||
</select>
|
||||
<button (click)="login()" [disabled]="!isLoginEnabled">Login</button>
|
||||
</span>
|
||||
<div
|
||||
class="modal-overlay"
|
||||
*ngIf="isLoginModalDisplayed && !isAuthenticated"
|
||||
(click)="isLoginModalDisplayed = false"
|
||||
>
|
||||
<div class="modal">
|
||||
<iframe
|
||||
[src]="iframeSrc"
|
||||
frameborder="0"
|
||||
(load)="onIframeLoad($event)"
|
||||
></iframe>
|
||||
<button class="close-button" (click)="isLoginModalDisplayed = false">
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>`,
|
||||
styles: `.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: none;
|
||||
}`,
|
||||
})
|
||||
export class LoginComponent {
|
||||
isLoginModalDisplayed = false;
|
||||
iframeSrc?: SafeUrl;
|
||||
loginExperiences: LoginExperience[] = [];
|
||||
selectedLoginExperience = new FormControl<LoginExperience | null>(null, [
|
||||
Validators.required,
|
||||
]);
|
||||
|
||||
private loginUri?: string;
|
||||
|
||||
constructor(
|
||||
http: HttpClient,
|
||||
private user: UserService,
|
||||
private router: Router,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
loginOptions(http).subscribe((opts) => {
|
||||
if (opts.length) {
|
||||
this.loginUri = opts[0].loginUri;
|
||||
if (opts[0].isSameAuthority) {
|
||||
this.loginExperiences.push(LoginExperience.IFRAME);
|
||||
}
|
||||
this.loginExperiences.push(LoginExperience.DEFAULT);
|
||||
this.selectedLoginExperience.patchValue(this.loginExperiences[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isLoginEnabled(): boolean {
|
||||
return (
|
||||
this.selectedLoginExperience.valid && !this.user.current.isAuthenticated
|
||||
);
|
||||
}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return this.user.current.isAuthenticated;
|
||||
}
|
||||
|
||||
login() {
|
||||
if (!this.loginUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(this.loginUri);
|
||||
url.searchParams.append(
|
||||
'post_login_success_uri',
|
||||
`${baseUri}${this.router.url}`
|
||||
);
|
||||
const loginUrl = url.toString();
|
||||
|
||||
if (this.selectedLoginExperience.value === LoginExperience.IFRAME) {
|
||||
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(loginUrl);
|
||||
this.isLoginModalDisplayed = true;
|
||||
} else {
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
}
|
||||
|
||||
onIframeLoad(event: any) {
|
||||
if (!!event.currentTarget.src) {
|
||||
this.user.refresh();
|
||||
this.isLoginModalDisplayed = !this.user.current.isAuthenticated;
|
||||
}
|
||||
}
|
||||
|
||||
loginExperienceLabel(le: LoginExperience) {
|
||||
return LoginExperience[le].toLowerCase()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { baseUri } from '../app.config';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logout',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
template: `
|
||||
<button (click)="logout()">Logout</button>
|
||||
`,
|
||||
styles: ``
|
||||
})
|
||||
export class LogoutComponent {
|
||||
|
||||
constructor(private http: HttpClient, private user: UserService) {}
|
||||
|
||||
logout() {
|
||||
lastValueFrom(
|
||||
this.http.post('/bff/logout', null, {
|
||||
headers: {
|
||||
'X-POST-LOGOUT-SUCCESS-URI': baseUri,
|
||||
},
|
||||
observe: 'response',
|
||||
})
|
||||
)
|
||||
.then((resp) => {
|
||||
const logoutUri = resp.headers.get('Location');
|
||||
if (!!logoutUri) {
|
||||
window.location.href = logoutUri;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.user.refresh();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription, interval } from 'rxjs';
|
||||
|
||||
interface UserinfoDto {
|
||||
username: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export class User {
|
||||
static readonly ANONYMOUS = new User('', '', []);
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly email: string,
|
||||
readonly roles: string[]
|
||||
) {}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return !!this.name;
|
||||
}
|
||||
|
||||
hasAnyRole(...roles: string[]): boolean {
|
||||
for (const r of roles) {
|
||||
if (this.roles.includes(r)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
private user$ = new BehaviorSubject<User>(User.ANONYMOUS);
|
||||
private refreshSub?: Subscription;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.refreshSub?.unsubscribe();
|
||||
this.http.get('/bff/api/me').subscribe({
|
||||
next: (dto: any) => {
|
||||
const user = dto as UserinfoDto;
|
||||
if (
|
||||
user.username !== this.user$.value.name ||
|
||||
user.email !== this.user$.value.email ||
|
||||
(user.roles || []).toString() !== this.user$.value.roles.toString()
|
||||
) {
|
||||
this.user$.next(
|
||||
user.username
|
||||
? new User(
|
||||
user.username || '',
|
||||
user.email || '',
|
||||
user.roles || []
|
||||
)
|
||||
: User.ANONYMOUS
|
||||
);
|
||||
}
|
||||
if (!!user.exp) {
|
||||
const now = Date.now();
|
||||
const delay = (1000 * user.exp - now) * 0.8;
|
||||
if (delay > 2000) {
|
||||
this.refreshSub = interval(delay).subscribe(() => this.refresh());
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.warn(error);
|
||||
this.user$.next(User.ANONYMOUS);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get valueChanges(): Observable<User> {
|
||||
return this.user$;
|
||||
}
|
||||
|
||||
get current(): User {
|
||||
return this.user$.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { NavigationComponent } from './navigation.component';
|
||||
import { User, UserService } from './auth/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [NavigationComponent],
|
||||
template: `<app-navigation
|
||||
[destination]="['about']"
|
||||
label="About"
|
||||
></app-navigation>
|
||||
<p>{{ message }}</p>`,
|
||||
styles: [],
|
||||
})
|
||||
export class HomeView {
|
||||
message = '';
|
||||
|
||||
private userSubscription?: Subscription;
|
||||
|
||||
constructor(user: UserService) {
|
||||
this.userSubscription = user.valueChanges.subscribe((u) => {
|
||||
this.message = u.isAuthenticated
|
||||
? `Hi ${u.name}, you are granted with ${HomeView.rolesStr(u)}.`
|
||||
: 'You are not authenticated.';
|
||||
});
|
||||
}
|
||||
|
||||
static rolesStr(user: User) {
|
||||
if(!user?.roles?.length) {
|
||||
return '[]'
|
||||
}
|
||||
return `["${user.roles.join('", "')}"]`
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.userSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
template: `<button (click)="navigate()">{{ label }}</button>`,
|
||||
styles: ``,
|
||||
})
|
||||
export class NavigationComponent {
|
||||
@Input()
|
||||
label!: string;
|
||||
|
||||
@Input()
|
||||
destination!: string[];
|
||||
|
||||
private userSubscription?: Subscription;
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.userSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
navigate() {
|
||||
this.router.navigate(this.destination);
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AngularUi</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -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));
|
|
@ -0,0 +1 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
|
@ -0,0 +1,14 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
BIN
spring-security-modules/spring-security-oauth2-bff/backend/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
spring-security-modules/spring-security-oauth2-bff/backend/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<pmd>
|
||||
<useProjectRuleSet>true</useProjectRuleSet>
|
||||
<ruleSetFile>D:\workspaces\baeldung\tutorials\spring-security-modules\spring-security-oauth2-bff\backend\bff\.pmdruleset.xml</ruleSetFile>
|
||||
<includeDerivedFiles>false</includeDerivedFiles>
|
||||
<violationsAsErrors>true</violationsAsErrors>
|
||||
<fullBuildEnabled>true</fullBuildEnabled>
|
||||
</pmd>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
name="M2Eclipse PMD RuleSet"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
<description>M2Eclipse PMD RuleSet</description>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/bff/target.*</exclude-pattern>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/bff/target/generated-sources.*</exclude-pattern>
|
||||
</ruleset>
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.baeldung.bff</groupId>
|
||||
<artifactId>backend-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>bff</artifactId>
|
||||
<name>BFF</name>
|
||||
<description>Backend For Frontend for the OAuth2 BFF article</description>
|
||||
|
||||
<properties>
|
||||
<tutorialsproject.basedir>../../../..</tutorialsproject.basedir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,13 @@
|
|||
package com.baeldung.bff;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class BffApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BffApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.baeldung.bff;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
public class LoginOptionsController {
|
||||
private final List<LoginOptionDto> loginOptions;
|
||||
|
||||
public LoginOptionsController(OAuth2ClientProperties clientProps, SpringAddonsOidcProperties addonsProperties) {
|
||||
final var clientAuthority = addonsProperties.getClient()
|
||||
.getClientUri()
|
||||
.getAuthority();
|
||||
this.loginOptions = clientProps.getRegistration()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> "authorization_code".equals(e.getValue()
|
||||
.getAuthorizationGrantType()))
|
||||
.map(e -> {
|
||||
final var label = e.getValue()
|
||||
.getProvider();
|
||||
final var loginUri = "%s/oauth2/authorization/%s".formatted(addonsProperties.getClient()
|
||||
.getClientUri(), e.getKey());
|
||||
final var providerId = clientProps.getRegistration()
|
||||
.get(e.getKey())
|
||||
.getProvider();
|
||||
final var providerIssuerAuthority = URI.create(clientProps.getProvider()
|
||||
.get(providerId)
|
||||
.getIssuerUri())
|
||||
.getAuthority();
|
||||
return new LoginOptionDto(label, loginUri, Objects.equals(clientAuthority, providerIssuerAuthority));
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping(path = "/login-options", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Mono<List<LoginOptionDto>> getLoginOptions() throws URISyntaxException {
|
||||
return Mono.just(this.loginOptions);
|
||||
}
|
||||
|
||||
public static record LoginOptionDto(@NotEmpty String label, @NotEmpty String loginUri, boolean isSameAuthority) {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
{"properties": [{
|
||||
"name": "scheme",
|
||||
"type": "java.lang.String",
|
||||
"description": "Scheme to use for backend services"
|
||||
},{
|
||||
"name": "hostname",
|
||||
"type": "java.lang.String",
|
||||
"description": "Name of the host on which the backend services run"
|
||||
},{
|
||||
"name": "reverse-proxy-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port used by the reverse proxy"
|
||||
},{
|
||||
"name": "reverse-proxy-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Public URI for the reverse proxy"
|
||||
},{
|
||||
"name": "angular-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Angular app is served"
|
||||
},{
|
||||
"name": "angular-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Angular assets"
|
||||
},{
|
||||
"name": "angular-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Angular assets"
|
||||
},{
|
||||
"name": "vue-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Vue app is served"
|
||||
},{
|
||||
"name": "vue-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Vue assets"
|
||||
},{
|
||||
"name": "vue-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Vue assets"
|
||||
},{
|
||||
"name": "react-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the React app is served"
|
||||
},{
|
||||
"name": "react-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves React assets"
|
||||
},{
|
||||
"name": "react-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to React assets"
|
||||
},{
|
||||
"name": "authorization-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the authorization server listens"
|
||||
},{
|
||||
"name": "authorization-server-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to the authorization server"
|
||||
},{
|
||||
"name": "authorization-server-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal base URI for the authorization server"
|
||||
},{
|
||||
"name": "issuer",
|
||||
"type": "java.net.URI",
|
||||
"description": "Exact value of the issuer claim in tokens"
|
||||
},{
|
||||
"name": "client-id",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-id"
|
||||
},{
|
||||
"name": "client-secret",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-secret"
|
||||
},{
|
||||
"name": "username-claim-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as user name"
|
||||
},{
|
||||
"name": "authorities-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as Spring authorities source"
|
||||
},{
|
||||
"name": "audience",
|
||||
"type": "java.lang.String",
|
||||
"description": "Some OpenID providers (Auth0) require the client to provide the 'audience' he's going to use access token for"
|
||||
},{
|
||||
"name": "bff-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the BFF listens"
|
||||
},{
|
||||
"name": "bff-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route REST requests to the BFF"
|
||||
},{
|
||||
"name": "bff-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to the BFF"
|
||||
},{
|
||||
"name": "resource-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the stateless REST API listens"
|
||||
}]}
|
|
@ -0,0 +1,165 @@
|
|||
# Custom properties to ease configuration overrides
|
||||
# on command-line or IDE launch configurations
|
||||
scheme: http
|
||||
hostname: localhost
|
||||
reverse-proxy-port: 7080
|
||||
reverse-proxy-uri: ${scheme}://${hostname}:${reverse-proxy-port}
|
||||
authorization-server-prefix: /auth
|
||||
issuer: ${reverse-proxy-uri}${authorization-server-prefix}/realms/baeldung
|
||||
client-id: baeldung-confidential
|
||||
client-secret: change-me
|
||||
username-claim-json-path: $.preferred_username
|
||||
authorities-json-path: $.realm_access.roles
|
||||
bff-port: 7081
|
||||
bff-prefix: /bff
|
||||
resource-server-port: 7084
|
||||
audience:
|
||||
|
||||
server:
|
||||
port: ${bff-port}
|
||||
ssl:
|
||||
enabled: false
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: bff
|
||||
uri: ${scheme}://${hostname}:${resource-server-port}
|
||||
predicates:
|
||||
- Path=/api/**
|
||||
filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
- TokenRelay=
|
||||
- SaveSession
|
||||
- StripPrefix=1
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
provider:
|
||||
baeldung:
|
||||
issuer-uri: ${issuer}
|
||||
registration:
|
||||
baeldung:
|
||||
provider: baeldung
|
||||
authorization-grant-type: authorization_code
|
||||
client-id: ${client-id}
|
||||
client-secret: ${client-secret}
|
||||
scope: openid,profile,email,offline_access
|
||||
|
||||
com:
|
||||
c4-soft:
|
||||
springaddons:
|
||||
oidc:
|
||||
ops:
|
||||
- iss: ${issuer}
|
||||
authorities:
|
||||
- path: ${authorities-json-path}
|
||||
aud: ${audience}
|
||||
# SecurityFilterChain with oauth2Login() (sessions and CSRF protection enabled)
|
||||
client:
|
||||
client-uri: ${reverse-proxy-uri}${bff-prefix}
|
||||
security-matchers:
|
||||
- /api/**
|
||||
- /login/**
|
||||
- /oauth2/**
|
||||
- /logout
|
||||
permit-all:
|
||||
- /api/**
|
||||
- /login/**
|
||||
- /oauth2/**
|
||||
csrf: cookie-accessible-from-js
|
||||
oauth2-redirections:
|
||||
rp-initiated-logout: ACCEPTED
|
||||
back-channel-logout:
|
||||
enabled: true
|
||||
# SecurityFilterChain with oauth2ResourceServer() (sessions and CSRF protection disabled)
|
||||
resourceserver:
|
||||
permit-all:
|
||||
- /login-options
|
||||
- /error
|
||||
- /v3/api-docs/**
|
||||
- /swagger-ui/**
|
||||
- /actuator/health/readiness
|
||||
- /actuator/health/liveness
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
probes:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
health:
|
||||
livenessstate:
|
||||
enabled: true
|
||||
readinessstate:
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org:
|
||||
springframework:
|
||||
boot: INFO
|
||||
security: INFO
|
||||
web: INFO
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: ssl
|
||||
server:
|
||||
ssl:
|
||||
enabled: true
|
||||
scheme: https
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: cognito
|
||||
issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
|
||||
client-id: 12olioff63qklfe9nio746es9f
|
||||
client-secret: change-me
|
||||
username-claim-json-path: username
|
||||
authorities-json-path: $.cognito:groups
|
||||
com:
|
||||
c4-soft:
|
||||
springaddons:
|
||||
oidc:
|
||||
client:
|
||||
oauth2-logout:
|
||||
baeldung:
|
||||
uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout
|
||||
client-id-request-param: client_id
|
||||
post-logout-uri-request-param: logout_uri
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: auth0
|
||||
issuer: https://dev-ch4mpy.eu.auth0.com/
|
||||
client-id: yWgZDRJLAksXta8BoudYfkF5kus2zv2Q
|
||||
client-secret: change-me
|
||||
username-claim-json-path: $['https://c4-soft.com/user']['name']
|
||||
authorities-json-path: $['https://c4-soft.com/user']['roles']
|
||||
audience: bff.baeldung.com
|
||||
com:
|
||||
c4-soft:
|
||||
springaddons:
|
||||
oidc:
|
||||
client:
|
||||
authorization-request-params:
|
||||
baeldung:
|
||||
- name: audience
|
||||
value: ${audience}
|
||||
oauth2-logout:
|
||||
baeldung:
|
||||
uri: ${issuer}v2/logout
|
||||
client-id-request-param: client_id
|
||||
post-logout-uri-request-param: returnTo
|
|
@ -0,0 +1,308 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
|
@ -0,0 +1,205 @@
|
|||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.baeldung</groupId>
|
||||
<artifactId>parent-boot-3</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>../../../parent-boot-3</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>com.baeldung.bff</groupId>
|
||||
<artifactId>backend-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<description>Parent pom for the backend services in the OAuth2 BFF article</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<tutorialsproject.basedir>../../..</tutorialsproject.basedir>
|
||||
<spring-addons.version>7.5.3</spring-addons.version>
|
||||
<spring-boot.version>3.2.2</spring-boot.version>
|
||||
<spring-cloud.version>2023.0.0</spring-cloud.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>reverse-proxy</module>
|
||||
<module>bff</module>
|
||||
<module>resource-server</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring-cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc</artifactId>
|
||||
<version>${spring-addons.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc-test</artifactId>
|
||||
<version>${spring-addons.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<testEnvironment>true</testEnvironment>
|
||||
</systemPropertyVariables>
|
||||
<argLine>-Dspring.profiles.active=no-ssl</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<pmd>
|
||||
<useProjectRuleSet>true</useProjectRuleSet>
|
||||
<ruleSetFile>D:\workspaces\baeldung\tutorials\spring-security-modules\spring-security-oauth2-bff\backend\resource-server\.pmdruleset.xml</ruleSetFile>
|
||||
<includeDerivedFiles>false</includeDerivedFiles>
|
||||
<violationsAsErrors>true</violationsAsErrors>
|
||||
<fullBuildEnabled>true</fullBuildEnabled>
|
||||
</pmd>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
name="M2Eclipse PMD RuleSet"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
<description>M2Eclipse PMD RuleSet</description>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/resource-server/target/generated-sources.*</exclude-pattern>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/resource-server/target.*</exclude-pattern>
|
||||
</ruleset>
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.baeldung.bff</groupId>
|
||||
<artifactId>backend-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>resource-server</artifactId>
|
||||
<name>resource-server</name>
|
||||
<description>Stateless REST API for the OAuth2 BFF article</description>
|
||||
|
||||
<properties>
|
||||
<tutorialsproject.basedir>../../../..</tutorialsproject.basedir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.c4-soft.springaddons</groupId>
|
||||
<artifactId>spring-addons-starter-oidc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,55 @@
|
|||
package com.baeldung.bff;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class MeController {
|
||||
|
||||
@GetMapping("/me")
|
||||
public UserInfoDto getMe(Authentication auth) {
|
||||
if (auth instanceof JwtAuthenticationToken jwtAuth) {
|
||||
final var email = (String) jwtAuth.getTokenAttributes()
|
||||
.getOrDefault(StandardClaimNames.EMAIL, "");
|
||||
final var roles = auth.getAuthorities()
|
||||
.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.toList();
|
||||
final var exp = Optional.ofNullable(jwtAuth.getTokenAttributes()
|
||||
.get(JwtClaimNames.EXP)).map(expClaim -> {
|
||||
if(expClaim instanceof Long lexp) {
|
||||
return lexp;
|
||||
}
|
||||
if(expClaim instanceof Instant iexp) {
|
||||
return iexp.getEpochSecond();
|
||||
}
|
||||
if(expClaim instanceof Date dexp) {
|
||||
return dexp.toInstant().getEpochSecond();
|
||||
}
|
||||
return Long.MAX_VALUE;
|
||||
}).orElse(Long.MAX_VALUE);
|
||||
return new UserInfoDto(auth.getName(), email, roles, exp);
|
||||
}
|
||||
return UserInfoDto.ANONYMOUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username a unique identifier for the resource owner in the token (sub claim by default)
|
||||
* @param email OpenID email claim
|
||||
* @param roles Spring authorities resolved for the authentication in the security context
|
||||
* @param exp seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time when the access token expires
|
||||
*/
|
||||
public static record UserInfoDto(String username, String email, List<String> roles, Long exp) {
|
||||
public static final UserInfoDto ANONYMOUS = new UserInfoDto("", "", List.of(), Long.MAX_VALUE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.baeldung.bff;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ResourceServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ResourceServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
{"properties": [{
|
||||
"name": "scheme",
|
||||
"type": "java.lang.String",
|
||||
"description": "Scheme to use for backend services"
|
||||
},{
|
||||
"name": "hostname",
|
||||
"type": "java.lang.String",
|
||||
"description": "Name of the host on which the backend services run"
|
||||
},{
|
||||
"name": "reverse-proxy-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port used by the reverse proxy"
|
||||
},{
|
||||
"name": "angular-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Angular app is served"
|
||||
},{
|
||||
"name": "reverse-proxy-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Public URI for the reverse proxy"
|
||||
},{
|
||||
"name": "angular-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Angular assets"
|
||||
},{
|
||||
"name": "angular-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Angular assets"
|
||||
},{
|
||||
"name": "vue-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Vue app is served"
|
||||
},{
|
||||
"name": "vue-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Vue assets"
|
||||
},{
|
||||
"name": "vue-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Vue assets"
|
||||
},{
|
||||
"name": "react-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the React app is served"
|
||||
},{
|
||||
"name": "react-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves React assets"
|
||||
},{
|
||||
"name": "react-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to React assets"
|
||||
},{
|
||||
"name": "authorization-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the authorization server listens"
|
||||
},{
|
||||
"name": "authorization-server-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to the authorization server"
|
||||
},{
|
||||
"name": "authorization-server-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal base URI for the authorization server"
|
||||
},{
|
||||
"name": "issuer",
|
||||
"type": "java.net.URI",
|
||||
"description": "Exact value of the issuer claim in tokens"
|
||||
},{
|
||||
"name": "client-id",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-id"
|
||||
},{
|
||||
"name": "client-secret",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-secret"
|
||||
},{
|
||||
"name": "username-claim-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as user name"
|
||||
},{
|
||||
"name": "authorities-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as Spring authorities source"
|
||||
},{
|
||||
"name": "audience",
|
||||
"type": "java.lang.String",
|
||||
"description": "Some OpenID providers (Auth0) require the client to provide the 'audience' he's going to use access token for"
|
||||
},{
|
||||
"name": "bff-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the BFF listens"
|
||||
},{
|
||||
"name": "bff-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route REST requests to the BFF"
|
||||
},{
|
||||
"name": "bff-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to the BFF"
|
||||
},{
|
||||
"name": "resource-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the stateless REST API listens"
|
||||
}]}
|
|
@ -0,0 +1,86 @@
|
|||
scheme: http
|
||||
hostname: localhost
|
||||
reverse-proxy-port: 7080
|
||||
reverse-proxy-uri: ${scheme}://${hostname}:${reverse-proxy-port}
|
||||
authorization-server-prefix: /auth
|
||||
issuer: ${reverse-proxy-uri}${authorization-server-prefix}/realms/baeldung
|
||||
username-claim-json-path: $.preferred_username
|
||||
authorities-json-path: $.realm_access.roles
|
||||
resource-server-port: 7084
|
||||
audience:
|
||||
|
||||
server:
|
||||
port: ${resource-server-port}
|
||||
ssl:
|
||||
enabled: false
|
||||
|
||||
com:
|
||||
c4-soft:
|
||||
springaddons:
|
||||
oidc:
|
||||
ops:
|
||||
- iss: ${issuer}
|
||||
username-claim: ${username-claim-json-path}
|
||||
authorities:
|
||||
- path: ${authorities-json-path}
|
||||
aud: ${audience}
|
||||
resourceserver:
|
||||
permit-all:
|
||||
- /me
|
||||
- /v3/api-docs/**
|
||||
- /swagger-ui/**
|
||||
- /actuator/health/readiness
|
||||
- /actuator/health/liveness
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
probes:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
health:
|
||||
livenessstate:
|
||||
enabled: true
|
||||
readinessstate:
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org:
|
||||
springframework:
|
||||
boot: INFO
|
||||
security: INFO
|
||||
web: INFO
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: ssl
|
||||
server:
|
||||
ssl:
|
||||
enabled: true
|
||||
scheme: https
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: cognito
|
||||
issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
|
||||
username-claim-json-path: username
|
||||
authorities-json-path: $.cognito:groups
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: auth0
|
||||
issuer: https://dev-ch4mpy.eu.auth0.com/
|
||||
username-claim-json-path: $['https://c4-soft.com/user']['name']
|
||||
authorities-json-path: $['https://c4-soft.com/user']['roles']
|
||||
audience: bff.baeldung.com
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<pmd>
|
||||
<useProjectRuleSet>true</useProjectRuleSet>
|
||||
<ruleSetFile>D:\workspaces\baeldung\tutorials\spring-security-modules\spring-security-oauth2-bff\backend\reverse-proxy\.pmdruleset.xml</ruleSetFile>
|
||||
<includeDerivedFiles>false</includeDerivedFiles>
|
||||
<violationsAsErrors>true</violationsAsErrors>
|
||||
<fullBuildEnabled>true</fullBuildEnabled>
|
||||
</pmd>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
name="M2Eclipse PMD RuleSet"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
<description>M2Eclipse PMD RuleSet</description>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/reverse-proxy/target.*</exclude-pattern>
|
||||
<exclude-pattern>.*D:/workspaces/baeldung/tutorials/spring-security-modules/spring-security-oauth2-bff/backend/reverse-proxy/target/generated-sources.*</exclude-pattern>
|
||||
</ruleset>
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.baeldung.bff</groupId>
|
||||
<artifactId>backend-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>reverse-proxy</artifactId>
|
||||
<name>reverse-proxy</name>
|
||||
<description>Reverse proxy for the OAuth2 BFF article</description>
|
||||
|
||||
<properties>
|
||||
<tutorialsproject.basedir>../../../..</tutorialsproject.basedir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,13 @@
|
|||
package com.baeldung.bff;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ReverseProxyApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ReverseProxyApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
{"properties": [{
|
||||
"name": "scheme",
|
||||
"type": "java.lang.String",
|
||||
"description": "Scheme to use for backend services"
|
||||
},{
|
||||
"name": "hostname",
|
||||
"type": "java.lang.String",
|
||||
"description": "Name of the host on which the backend services run"
|
||||
},{
|
||||
"name": "reverse-proxy-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port used by the reverse proxy"
|
||||
},{
|
||||
"name": "angular-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Angular app is served"
|
||||
},{
|
||||
"name": "reverse-proxy-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Public URI for the reverse proxy"
|
||||
},{
|
||||
"name": "angular-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Angular assets"
|
||||
},{
|
||||
"name": "angular-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Angular assets"
|
||||
},{
|
||||
"name": "vue-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the Vue app is served"
|
||||
},{
|
||||
"name": "vue-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves Vue assets"
|
||||
},{
|
||||
"name": "vue-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to Vue assets"
|
||||
},{
|
||||
"name": "react-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port from which the React app is served"
|
||||
},{
|
||||
"name": "react-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to whatever serves React assets"
|
||||
},{
|
||||
"name": "react-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to React assets"
|
||||
},{
|
||||
"name": "authorization-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the authorization server listens"
|
||||
},{
|
||||
"name": "authorization-server-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route to the authorization server"
|
||||
},{
|
||||
"name": "authorization-server-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal base URI for the authorization server"
|
||||
},{
|
||||
"name": "issuer",
|
||||
"type": "java.net.URI",
|
||||
"description": "Exact value of the issuer claim in tokens"
|
||||
},{
|
||||
"name": "client-id",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-id"
|
||||
},{
|
||||
"name": "client-secret",
|
||||
"type": "java.lang.String",
|
||||
"description": "OAuth2 client-secret"
|
||||
},{
|
||||
"name": "username-claim-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as user name"
|
||||
},{
|
||||
"name": "authorities-json-path",
|
||||
"type": "java.lang.String",
|
||||
"description": "JSON path to the claim to use as Spring authorities source"
|
||||
},{
|
||||
"name": "audience",
|
||||
"type": "java.lang.String",
|
||||
"description": "Some OpenID providers (Auth0) require the client to provide the 'audience' he's going to use access token for"
|
||||
},{
|
||||
"name": "bff-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the BFF listens"
|
||||
},{
|
||||
"name": "bff-prefix",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path-prefix used by the reverse proxy to route REST requests to the BFF"
|
||||
},{
|
||||
"name": "bff-uri",
|
||||
"type": "java.net.URI",
|
||||
"description": "Internal URI to the BFF"
|
||||
},{
|
||||
"name": "resource-server-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port on which the stateless REST API listens"
|
||||
}]}
|
|
@ -0,0 +1,155 @@
|
|||
# Custom properties to ease configuration overrides
|
||||
# on command-line or IDE launch configurations
|
||||
scheme: http
|
||||
hostname: localhost
|
||||
reverse-proxy-port: 7080
|
||||
angular-port: 4201
|
||||
angular-prefix: /angular-ui
|
||||
# Update scheme if you enable SSL in angular.json
|
||||
angular-uri: http://${hostname}:${angular-port}${angular-prefix}
|
||||
vue-port: 4202
|
||||
vue-prefix: /vue-ui
|
||||
# Update scheme if you enable SSL in vite.config.ts
|
||||
vue-uri: http://${hostname}:${vue-port}${vue-prefix}
|
||||
react-port: 4203
|
||||
react-prefix: /react-ui
|
||||
react-uri: http://${hostname}:${react-port}${react-prefix}
|
||||
authorization-server-port: 8080
|
||||
authorization-server-prefix: /auth
|
||||
authorization-server-uri: ${scheme}://${hostname}:${authorization-server-port}${authorization-server-prefix}
|
||||
bff-port: 7081
|
||||
bff-prefix: /bff
|
||||
bff-uri: ${scheme}://${hostname}:${bff-port}
|
||||
|
||||
server:
|
||||
port: ${reverse-proxy-port}
|
||||
ssl:
|
||||
enabled: false
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
default-filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
routes:
|
||||
# SPAs assets
|
||||
- id: angular-ui
|
||||
uri: ${angular-uri}
|
||||
predicates:
|
||||
- Path=${angular-prefix}/**
|
||||
- id: vue-ui
|
||||
uri: ${vue-uri}
|
||||
predicates:
|
||||
- Path=${vue-prefix}/**
|
||||
- id: react-ui
|
||||
uri: ${react-uri}
|
||||
predicates:
|
||||
- Path=${react-prefix}/**
|
||||
|
||||
# Authorization-server
|
||||
- id: authorization-server
|
||||
uri: ${authorization-server-uri}
|
||||
predicates:
|
||||
- Path=${authorization-server-prefix}/**
|
||||
|
||||
# Proxy BFF
|
||||
- id: bff
|
||||
uri: ${bff-uri}
|
||||
predicates:
|
||||
- Path=${bff-prefix}/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
probes:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
health:
|
||||
livenessstate:
|
||||
enabled: true
|
||||
readinessstate:
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org:
|
||||
springframework:
|
||||
boot: INFO
|
||||
web: INFO
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: ssl
|
||||
server:
|
||||
ssl:
|
||||
enabled: true
|
||||
scheme: https
|
||||
authorization-server-port: 8443
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: cognito
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
# SPAs assets
|
||||
- id: angular-ui
|
||||
uri: ${angular-uri}
|
||||
predicates:
|
||||
- Path=${angular-prefix}/**
|
||||
- id: vue-ui
|
||||
uri: ${vue-uri}
|
||||
predicates:
|
||||
- Path=${vue-prefix}/**
|
||||
- id: react-ui
|
||||
uri: ${react-uri}
|
||||
predicates:
|
||||
- Path=${react-prefix}/**
|
||||
# not routing to authorization server here
|
||||
# Proxy BFF
|
||||
- id: bff
|
||||
uri: ${bff-uri}
|
||||
predicates:
|
||||
- Path=${bff-prefix}/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: auth0
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
# SPAs assets
|
||||
- id: angular-ui
|
||||
uri: ${angular-uri}
|
||||
predicates:
|
||||
- Path=${angular-prefix}/**
|
||||
- id: vue-ui
|
||||
uri: ${vue-uri}
|
||||
predicates:
|
||||
- Path=${vue-prefix}/**
|
||||
- id: react-ui
|
||||
uri: ${react-uri}
|
||||
predicates:
|
||||
- Path=${react-prefix}/**
|
||||
# not routing to authorization server here
|
||||
# Proxy BFF
|
||||
- id: bff
|
||||
uri: ${bff-uri}
|
||||
predicates:
|
||||
- Path=${bff-prefix}/**
|
||||
filters:
|
||||
- StripPrefix=1
|
|
@ -0,0 +1 @@
|
|||
KEYCLOAK_ADMIN_PASSWORD=admin
|
|
@ -0,0 +1,2 @@
|
|||
ssl/
|
||||
docker-compose-ssl.yaml
|
|
@ -0,0 +1,19 @@
|
|||
name: keycloak-baeldung-bff
|
||||
services:
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
command:
|
||||
- start-dev
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
|
||||
KC_HTTP_PORT: 8080
|
||||
KC_HOSTNAME_URL: http://localhost:7080/auth
|
||||
KC_HOSTNAME_ADMIN_URL: http://localhost:7080/auth
|
||||
KC_HTTP_RELATIVE_PATH: /auth
|
||||
#KC_LOG_LEVEL: DEBUG
|
||||
container_name: keycloak-baeldung-bff
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
|
@ -0,0 +1,3 @@
|
|||
NEXT_PUBLIC_REVERSE_PROXY_URI=http://localhost:7080
|
||||
NEXT_PUBLIC_BASE_PATH='/react-ui'
|
||||
NEXT_PUBLIC_BASE_URI=${NEXT_PUBLIC_REVERSE_PROXY_URI}${NEXT_PUBLIC_BASE_PATH}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
certificates
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<div className="flex">
|
||||
<span className="ml-2"></span>
|
||||
<button>
|
||||
<Link href="/">Home</Link>
|
||||
</button>
|
||||
<span className="m-auto"></span>
|
||||
<h1>About</h1>
|
||||
<span className="m-auto"></span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-between p-24">
|
||||
<p>
|
||||
This application is a show-case for an Angular app consuming a REST
|
||||
API through an OAuth2 BFF.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,61 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: none;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import { Inter } from "next/font/google";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import "./globals.css";
|
||||
import Authentication from "./lib/auth/authentication.component";
|
||||
import { User, UserService } from "./lib/auth/user.service";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const UserContext = createContext(User.ANONYMOUS);
|
||||
export function useUserContext() {
|
||||
return useContext(UserContext);
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const [user, setUser] = useState(User.ANONYMOUS);
|
||||
const userService = new UserService(user, setUser);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<UserContext.Provider value={user}>
|
||||
<div className="flex">
|
||||
<div className="m-auto"></div>
|
||||
<h1 className="mt-2">React UI</h1>
|
||||
<div className="m-auto"></div>
|
||||
<div className="mt-2">
|
||||
<Authentication
|
||||
onLogin={() => userService.refresh(user, setUser)}
|
||||
></Authentication>
|
||||
</div>
|
||||
<div className="mr-3"></div>
|
||||
</div>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { useUserContext } from "@/app/layout";
|
||||
import { EventHandler } from "react";
|
||||
import Login from "./login";
|
||||
import Logout from "./logout";
|
||||
|
||||
interface AuthenticationProperties {
|
||||
onLogin: EventHandler<any>;
|
||||
}
|
||||
|
||||
export default function Authentication({ onLogin }: AuthenticationProperties) {
|
||||
const user = useUserContext();
|
||||
|
||||
return (
|
||||
<span>
|
||||
{!user.isAuthenticated ? (
|
||||
<Login onLogin={onLogin}></Login>
|
||||
) : (
|
||||
<Logout></Logout>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
"use client";
|
||||
|
||||
import { useUserContext } from "@/app/layout";
|
||||
import axios from "axios";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { EventHandler, FormEvent, useEffect, useRef, useState } from "react";
|
||||
|
||||
enum LoginExperience {
|
||||
DEFAULT,
|
||||
IFRAME,
|
||||
}
|
||||
|
||||
interface LoginOptionDto {
|
||||
label: string;
|
||||
loginUri: string;
|
||||
isSameAuthority: boolean;
|
||||
}
|
||||
async function getLoginOptions(): Promise<Array<LoginOptionDto>> {
|
||||
const response = await axios.get<Array<LoginOptionDto>>("/bff/login-options");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
interface LoginProperties {
|
||||
onLogin: EventHandler<any>;
|
||||
}
|
||||
export default function Login({ onLogin }: LoginProperties) {
|
||||
const user = useUserContext();
|
||||
const [loginUri, setLoginUri] = useState("");
|
||||
const [selectedLoginExperience, setSelectedLoginExperience] = useState(
|
||||
LoginExperience.DEFAULT
|
||||
);
|
||||
const [isLoginModalDisplayed, setIsLoginModalDisplayed] = useState(false);
|
||||
const [isIframeLoginPossible, setIframeLoginPossible] = useState(false);
|
||||
const currentPath = usePathname();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current;
|
||||
iframe?.addEventListener("load", onIframeLoad);
|
||||
|
||||
return () => {
|
||||
fetchLoginOptions();
|
||||
iframe?.removeEventListener("load", onIframeLoad);
|
||||
};
|
||||
});
|
||||
|
||||
async function fetchLoginOptions() {
|
||||
const loginOpts = await getLoginOptions();
|
||||
if (loginOpts?.length < 1 || !loginOpts[0].loginUri) {
|
||||
setLoginUri("");
|
||||
setIframeLoginPossible(false);
|
||||
} else {
|
||||
setLoginUri(loginOpts[0].loginUri);
|
||||
setIframeLoginPossible(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!loginUri) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(loginUri);
|
||||
|
||||
url.searchParams.append(
|
||||
"post_login_success_uri",
|
||||
`${process.env.NEXT_PUBLIC_BASE_URI}${currentPath}`
|
||||
);
|
||||
const loginUrl = url.toString();
|
||||
if (
|
||||
+selectedLoginExperience === +LoginExperience.IFRAME &&
|
||||
iframeRef.current
|
||||
) {
|
||||
const iframe = iframeRef.current;
|
||||
if (iframe) {
|
||||
iframe.src = loginUrl;
|
||||
setIsLoginModalDisplayed(true);
|
||||
}
|
||||
} else {
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function onIframeLoad() {
|
||||
if (isLoginModalDisplayed) {
|
||||
onLogin({});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<form onSubmit={onSubmit}>
|
||||
{isIframeLoginPossible && (
|
||||
<select
|
||||
value={selectedLoginExperience}
|
||||
onChange={(e) => {
|
||||
setSelectedLoginExperience(
|
||||
+e?.target?.value === +LoginExperience.IFRAME
|
||||
? LoginExperience.IFRAME
|
||||
: LoginExperience.DEFAULT
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
value={LoginExperience.IFRAME}
|
||||
hidden={!isIframeLoginPossible}
|
||||
>
|
||||
iframe
|
||||
</option>
|
||||
<option value={LoginExperience.DEFAULT}>default</option>
|
||||
</select>
|
||||
)}
|
||||
<button disabled={user.isAuthenticated} type="submit">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
className={
|
||||
!user.isAuthenticated && isLoginModalDisplayed
|
||||
? "modal-overlay"
|
||||
: "hidden"
|
||||
}
|
||||
onClick={() => setIsLoginModalDisplayed(false)}
|
||||
>
|
||||
<div></div>
|
||||
<div className="modal">
|
||||
<div className="flex">
|
||||
<span className="ml-auto">
|
||||
<button onClick={() => setIsLoginModalDisplayed(false)}>X</button>
|
||||
</span>
|
||||
</div>
|
||||
<iframe ref={iframeRef}></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import axios from "axios";
|
||||
|
||||
export default function Logout() {
|
||||
|
||||
async function onClick() {
|
||||
const response = await axios.post(
|
||||
"/bff/logout",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"X-POST-LOGOUT-SUCCESS-URI": process.env.NEXT_PUBLIC_BASE_URI,
|
||||
},
|
||||
}
|
||||
);
|
||||
window.location.href = response.headers["location"];
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="submit" onClick={onClick}>
|
||||
Logout
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Subscription, interval } from "rxjs";
|
||||
|
||||
interface UserinfoDto {
|
||||
username: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export class User {
|
||||
static readonly ANONYMOUS = new User("", "", []);
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly email: string,
|
||||
readonly roles: string[]
|
||||
) {}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return !!this.name;
|
||||
}
|
||||
|
||||
hasAnyRole(...roles: string[]): boolean {
|
||||
for (const r of roles) {
|
||||
if (this.roles.includes(r)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
private refreshSub?: Subscription;
|
||||
|
||||
constructor(user: User, setUser: Dispatch<SetStateAction<User>>) {
|
||||
this.refresh(user, setUser);
|
||||
}
|
||||
|
||||
async refresh(
|
||||
user: User,
|
||||
setUser: Dispatch<SetStateAction<User>>
|
||||
): Promise<void> {
|
||||
this.refreshSub?.unsubscribe();
|
||||
const response = await axios.get<UserinfoDto>("/bff/api/me");
|
||||
if (
|
||||
response.data.username !== user.name ||
|
||||
response.data.email !== user.email ||
|
||||
(response.data.roles || []).toString() !== user.roles.toString()
|
||||
) {
|
||||
setUser(
|
||||
response.data.username
|
||||
? new User(
|
||||
response.data.username || "",
|
||||
response.data.email || "",
|
||||
response.data.roles || []
|
||||
)
|
||||
: User.ANONYMOUS
|
||||
);
|
||||
}
|
||||
if (!!response.data.exp) {
|
||||
const now = Date.now();
|
||||
const delay = (1000 * response.data.exp - now) * 0.8;
|
||||
if (delay > 2000) {
|
||||
this.refreshSub = interval(delay).subscribe(() =>
|
||||
this.refresh(user, setUser)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useUserContext } from "./layout";
|
||||
import { User } from "./lib/auth/user.service";
|
||||
|
||||
export default function Home() {
|
||||
const user = useUserContext();
|
||||
const message = user.isAuthenticated
|
||||
? `Hi ${user.name}, you are granted with ${rolesStr(user)}.`
|
||||
: "You are not authenticated.";
|
||||
|
||||
function rolesStr(user: User) {
|
||||
if (!user?.roles?.length) {
|
||||
return "[]";
|
||||
}
|
||||
return `["${user.roles.join('", "')}"]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<div className="flex">
|
||||
<span className="ml-2"></span>
|
||||
<button>
|
||||
<Link href="/about">About</Link>
|
||||
</button>
|
||||
<span className="m-auto"></span>
|
||||
<h1>Home</h1>
|
||||
<span className="m-auto"></span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-between p-24">
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
|
||||
assetPrefix: '/react-ui',
|
||||
};
|
||||
|
||||
export default nextConfig
|
4843
spring-security-modules/spring-security-oauth2-bff/react-ui/package-lock.json
generated
Normal file
4843
spring-security-modules/spring-security-oauth2-bff/react-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "react-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4203",
|
||||
"build": "next build",
|
||||
"start": "next start -p 4203",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.5",
|
||||
"next": "14.1.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
VITE_REVERSE_PROXY=http://localhost:7080
|
|
@ -0,0 +1,15 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
.vite
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
4345
spring-security-modules/spring-security-oauth2-bff/vue-ui/package-lock.json
generated
Normal file
4345
spring-security-modules/spring-security-oauth2-bff/vue-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "vue-ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-cookies": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/node": "^18.19.10",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import AuthenticationButtons from './components/AuthenticationButtons.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
|
||||
<div class="wrapper">
|
||||
<div style="flex">
|
||||
<h1 style="margin: auto;">Vue-app</h1>
|
||||
<AuthenticationButtons style="margin-left: auto;"></AuthenticationButtons>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,86 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
|
@ -0,0 +1,35 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import type { UserService } from '@/user.service';
|
||||
import { inject } from 'vue';
|
||||
import LoginForm from './LoginForm.vue';
|
||||
import LogoutForm from './LogoutForm.vue';
|
||||
|
||||
// inject the singleton defined in main.js
|
||||
const user = inject('UserService') as UserService;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<LoginForm v-if="!user.current.value.isAuthenticated"></LoginForm>
|
||||
<LogoutForm v-if="user.current.value.isAuthenticated"></LogoutForm>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -0,0 +1,122 @@
|
|||
<script setup lang="ts">
|
||||
import type { UserService } from '@/user.service'
|
||||
import { inject, onMounted, ref, type Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
enum LoginExperience {
|
||||
IFRAME,
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
interface LoginOptionDto {
|
||||
label: string
|
||||
loginUri: string
|
||||
isSameAuthority: boolean
|
||||
}
|
||||
|
||||
// inject the singleton defined in main.js
|
||||
const user = inject('UserService') as UserService
|
||||
const loginUri: Ref<string> = ref('')
|
||||
const loginExperiences: Ref<Array<LoginExperience>> = ref([LoginExperience.DEFAULT])
|
||||
const selectedLoginExperience: Ref<string> = ref(LoginExperience[LoginExperience.DEFAULT])
|
||||
const isLoginModalDisplayed: Ref<boolean> = ref(false)
|
||||
const iframeSrc: Ref<string> = ref('')
|
||||
const iframe: Ref<HTMLIFrameElement | undefined> = ref()
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
// Fetch login options from the BFF
|
||||
const response = await fetch('/bff/login-options')
|
||||
const opts = (await response.json()) as LoginOptionDto[]
|
||||
if (opts.length) {
|
||||
loginUri.value = opts[0].loginUri
|
||||
if (opts[0].isSameAuthority) {
|
||||
loginExperiences.value = [LoginExperience.IFRAME, LoginExperience.DEFAULT]
|
||||
}
|
||||
selectedLoginExperience.value = LoginExperience[loginExperiences.value[0]]
|
||||
}
|
||||
})
|
||||
|
||||
function login() {
|
||||
if (!loginUri.value) {
|
||||
return
|
||||
}
|
||||
const url = new URL(loginUri.value)
|
||||
|
||||
url.searchParams.append(
|
||||
'post_login_success_uri',
|
||||
`${import.meta.env.VITE_REVERSE_PROXY}${import.meta.env.BASE_URL}${route.path}`
|
||||
)
|
||||
const loginUrl = url.toString()
|
||||
if (selectedLoginExperience.value === LoginExperience[LoginExperience.IFRAME]) {
|
||||
iframeSrc.value = loginUrl
|
||||
isLoginModalDisplayed.value = true
|
||||
|
||||
if (iframe.value) {
|
||||
iframe.value.onload = () => {
|
||||
user.refresh()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
window.location.href = loginUrl
|
||||
}
|
||||
}
|
||||
|
||||
function onIframeLoad() {
|
||||
user.refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<select v-if="loginExperiences.length > 1" v-model="selectedLoginExperience">
|
||||
<option v-for="exp in loginExperiences" :key="exp">{{ LoginExperience[exp] }}</option>
|
||||
</select>
|
||||
<button
|
||||
@click="login()"
|
||||
:disabled="!selectedLoginExperience || user.current.value.isAuthenticated"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</span>
|
||||
<div
|
||||
class="modal-overlay"
|
||||
v-if="isLoginModalDisplayed && !user.current.value.isAuthenticated"
|
||||
@click="isLoginModalDisplayed = false"
|
||||
>
|
||||
<div class="modal">
|
||||
<iframe :src="iframeSrc" frameborder="0" :onload="onIframeLoad"></iframe>
|
||||
<button class="close-button" @click="isLoginModalDisplayed = false">Discard</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { useCookies } from "vue3-cookies";
|
||||
|
||||
const { cookies } = useCookies();
|
||||
|
||||
async function logout(xsrfToken: string) {
|
||||
const response = await fetch('/bff/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': xsrfToken,
|
||||
'X-POST-LOGOUT-SUCCESS-URI': `${import.meta.env.VITE_REVERSE_PROXY}${import.meta.env.BASE_URL}`
|
||||
}
|
||||
})
|
||||
const location = response.headers.get('Location')
|
||||
if (location) {
|
||||
window.location.href = location
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="logout(cookies.get('XSRF-TOKEN'))">Logout</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { UserService } from './user.service'
|
||||
|
||||
const app = createApp(App)
|
||||
app.provide("UserService", new UserService());
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
|
@ -0,0 +1,23 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
|
@ -0,0 +1,64 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
interface UserinfoDto {
|
||||
username: string
|
||||
email: string
|
||||
roles: string[]
|
||||
exp: number
|
||||
}
|
||||
|
||||
export class User {
|
||||
static readonly ANONYMOUS = new User('', '', [])
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly email: string,
|
||||
readonly roles: string[]
|
||||
) {}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return !!this.name
|
||||
}
|
||||
|
||||
hasAnyRole(...roles: string[]): boolean {
|
||||
for (const r of roles) {
|
||||
if (this.roles.includes(r)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
readonly current = ref(User.ANONYMOUS)
|
||||
private refreshIntervalId?: number
|
||||
|
||||
constructor() {
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
if (this.refreshIntervalId) {
|
||||
clearInterval(this.refreshIntervalId)
|
||||
}
|
||||
const response = await fetch(`/bff/api/me`)
|
||||
const user = (await response.json()) as UserinfoDto
|
||||
if (
|
||||
user?.username !== this.current?.value?.name ||
|
||||
user?.email !== this.current?.value?.email ||
|
||||
(user?.roles || []).toString() !== this.current?.value?.roles?.toString()
|
||||
) {
|
||||
this.current.value = user?.username
|
||||
? new User(user.username, user.email || '', user.roles || [])
|
||||
: User.ANONYMOUS
|
||||
}
|
||||
if (user?.exp) {
|
||||
const now = Date.now()
|
||||
const delay = (1000 * user.exp - now) * 0.8
|
||||
if (delay > 2000) {
|
||||
this.refreshIntervalId = setInterval(this.refresh, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<p>This application is a show-case for an Angular app consuming a REST API
|
||||
through an OAuth2 BFF.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import { User, UserService } from '../user.service'
|
||||
|
||||
// inject the singleton defined in main.js
|
||||
const user = inject('UserService') as UserService;
|
||||
function message(user: User) {
|
||||
return user.isAuthenticated
|
||||
? `Hi ${user.name}, you are granted with ${rolesStr(user)}.`
|
||||
: 'You are not authenticated.'
|
||||
}
|
||||
|
||||
function rolesStr(user: User) {
|
||||
if(!user?.roles?.length) {
|
||||
return '[]'
|
||||
}
|
||||
return `["${user.roles.join('", "')}"]`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<p>{{ message(user.current.value) }}</p>
|
||||
</main>
|
||||
</template>
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
base: '/vue-ui',
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 4202,
|
||||
//https: {
|
||||
// cert: 'C:/Users/ch4mp/.ssh/bravo-ch4mp_self_signed.pem',
|
||||
// key: 'C:/Users/ch4mp/.ssh/bravo-ch4mp_req_key.pem',
|
||||
//},
|
||||
//open: 'https://localhost:7080/vue-ui',
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue