feat: added header and project overview to FE and fixed keycloak test container

This commit is contained in:
mhg 2021-10-29 14:38:05 +02:00 committed by GitHub
parent 0541586aaf
commit d1c1a3814b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1126 additions and 2694 deletions

View File

@ -24,8 +24,8 @@
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/favicon-c4po.ico",
"src/assets/images/favicons/favicon.ico",
"src/assets/images/favicons/corporate_favicon.ico",
"src/assets"
],
"styles": [
@ -96,8 +96,8 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/favicon-c4po.ico",
"src/assets/images/favicons/favicon.ico",
"src/assets/images/favicons/corporate_favicon.ico",
"src/assets"
],
"styles": [

View File

@ -1,6 +1,8 @@
module.exports = {
moduleNameMapper: {
'@core/(.*)': '<rootDir>/src/app/core/$1',
'@assets/(.*)': '<rootDir>/src/assets/$1',
'@shared/(.*)': '<rootDir>/src/shared/$1'
},
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],

View File

@ -2510,9 +2510,9 @@
"dev": true
},
"@types/node": {
"version": "12.19.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.8.tgz",
"integrity": "sha512-D4k2kNi0URNBxIRCb1khTnkWNHv8KSL1owPmS/K5e5t8B2GzMReY7AsJIY1BnP5KdlgC4rj9jk2IkDMasIE7xg==",
"version": "12.20.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.33.tgz",
"integrity": "sha512-5XmYX2GECSa+CxMYaFsr2mrql71Q4EvHjKS+ox/SiwSdaASMoBIWE6UmZqFO+VX1jIcsYLStI4FFoB6V7FeIYw==",
"dev": true
},
"@types/normalize-package-data": {

View File

@ -58,7 +58,7 @@
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/jest": "26.0.15",
"@types/node": "^12.19.8",
"@types/node": "^12.20.33",
"codelyzer": "^6.0.0",
"font-awesome": "^4.7.0",
"jasmine-core": "~3.6.0",

View File

@ -3,7 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './home/home.component';
import {AuthGuardService} from '../shared/guards/auth-guard.service';
export const START_PAGE = 'home';
export const START_PAGE = 'projects';
const routes: Routes = [
{
@ -12,11 +12,11 @@ const routes: Routes = [
canActivate: [AuthGuardService]
},
{
path: 'dashboard',
loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule),
path: 'projects',
loadChildren: () => import('./project-overview').then(mod => mod.ProjectOverviewModule),
canActivate: [AuthGuardService]
},
// ToDo: Exchange default Keycloak login with self made login
// ToDo: Remove after default Keycloak login mask got reworked
/*{
path: 'login',
loadChildren: () => import('./login').then(mod => mod.LoginModule),

View File

@ -1,15 +1,9 @@
@import "../assets/@theme/styles/_variables.scss";
.content-container {
width: 90vw;
height: calc(90vh - #{$header-height});
.scrollable-content {
width: 100%;
max-width: 100vw;
height: 100%;
max-height: calc(100vh - #{$header-height});
overflow: auto;
}
}

View File

@ -6,9 +6,9 @@ import {NbEvaIconsModule} from '@nebular/eva-icons';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from './common-app.module';
import {HttpClient} from '@angular/common/http';
import {ThemeModule} from '../assets/@theme/theme.module';
import {ThemeModule} from '@assets/@theme/theme.module';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {SessionState} from '../shared/stores/session-state/session-state';
import {SessionState} from '@shared/stores/session-state/session-state';
import {NgxsModule} from '@ngxs/store';
import {HeaderModule} from './header/header.module';
import {KeycloakService} from 'keycloak-angular';

View File

@ -17,14 +17,15 @@ import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-f
import {fas} from '@fortawesome/free-solid-svg-icons';
import {far} from '@fortawesome/free-regular-svg-icons';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '../shared/stores/session-state/session-state';
import {SessionState} from '@shared/stores/session-state/session-state';
import {environment} from '../environments/environment';
import {NotificationService} from '../shared/services/notification.service';
import {NotificationService} from '@shared/services/notification.service';
import {ThemeModule} from '@assets/@theme/theme.module';
import {HeaderModule} from './header/header.module';
import {HomeModule} from './home/home.module';
import {KeycloakService} from 'keycloak-angular';
import {httpInterceptorProviders} from '../shared/interceptors';
import {httpInterceptorProviders} from '@shared/interceptors';
import {FlexLayoutModule} from '@angular/flex-layout';
@NgModule({
declarations: [
@ -53,7 +54,8 @@ import {httpInterceptorProviders} from '../shared/interceptors';
}
}),
HeaderModule,
HomeModule
HomeModule,
FlexLayoutModule
],
providers: [
HttpClient,

View File

@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {DashboardComponent} from './dashboard.component';
const routes: Routes = [
{
path: '',
component: DashboardComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule { }

View File

@ -1 +0,0 @@
<p>dashboard works!</p>

View File

@ -1,27 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
DashboardComponent
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,17 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit, OnDestroy {
constructor() { }
ngOnInit(): void {
}
ngOnDestroy(): void {
}
}

View File

@ -1,17 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {DashboardComponent} from './dashboard.component';
@NgModule({
declarations: [
DashboardComponent,
],
exports: [
DashboardComponent
],
imports: [
CommonModule
]
})
export class DashboardModule {
}

View File

@ -1,2 +0,0 @@
export {DashboardModule} from './dashboard.module';
export {DashboardRoutingModule} from './dashboard-routing.module';

View File

@ -1,4 +1,26 @@
<div class="c4po-header">
<p>header works!</p>
<div class="header" fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="2rem">
<img *ngIf="currentTheme === 'corporate', else changeImage"
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
<ng-template #changeImage>
<img src="../../assets/images/favicons/corporate_favicon.ico" alt="logo light" class="header-icon" width="60rem" height="60rem">
</ng-template>
<div class="logo-container" fxLayoutAlign="center center">
<h1 >{{SECURITYC4PO_TITLE}} </h1>
</div>
<div fxLayoutAlign="end" fxLayoutGap="4rem">
<nb-actions size="medium">
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickSwitchTheme()">
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon" [icon]="fa.faMoon"
class="new-element-icon"></fa-icon>
<ng-template #changeIcon>
<fa-icon [icon]="fa.faSun" class="new-element-icon"></fa-icon>
</ng-template>
</button>
</nb-action>
</nb-actions>
</div>
</div>

View File

@ -1,9 +1,31 @@
@import '~@nebular/theme/styles/global/breakpoints';
@import "../../assets/@theme/styles/_variables.scss";
.c4po-header {
height: $header-height;
@mixin nb-overrides {
display: inline-flex;
justify-content: space-between;
width: 100%;
.toggle-dark-mode {
text-align: right;
.header {
display: flex;
align-items: center;
width: auto;
.logo-container {
font-style: oblique;
color: #e74c3c;
}
nb-action {
height: auto;
display: flex;
align-content: center;
}
.toggle-theme {
align-content: flex-end;
display: flex;
}
}
}

View File

@ -1,6 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import {HeaderComponent} from './header.component';
import {CommonModule} from '@angular/common';
import {FontAwesomeTestingModule} from '@fortawesome/angular-fontawesome/testing';
import {NbActionsModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@ -8,9 +16,25 @@ describe('HeaderComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
declarations: [
HeaderComponent
],
imports: [
CommonModule,
NbActionsModule,
FontAwesomeTestingModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([])
]
})
.compileComponents();
.compileComponents();
});
beforeEach(() => {

View File

@ -1,14 +1,43 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbThemeService} from '@nebular/theme';
import {map} from 'rxjs/operators';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {GlobalTitlesVariables} from '@shared/config/global-variables';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
export class HeaderComponent implements OnInit, OnDestroy {
constructor() { }
readonly fa = FA;
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
currentTheme = '';
constructor(private themeService: NbThemeService) { }
ngOnInit(): void {
this.themeService.onThemeChange()
.pipe(
map(({ name }) => name),
untilDestroyed(this),
)
.subscribe(themeName => this.currentTheme = themeName);
}
onClickSwitchTheme(): void {
if (this.currentTheme === 'corporate') {
this.themeService.changeTheme('dark');
} else if (this.currentTheme === 'dark') {
this.themeService.changeTheme('corporate');
}
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
}

View File

@ -1,6 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from './header.component';
import {NbActionsModule, NbButtonModule, NbCardModule} from '@nebular/theme';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout';
@NgModule({
declarations: [
@ -10,7 +13,13 @@ import {HeaderComponent} from './header.component';
HeaderComponent
],
imports: [
CommonModule
CommonModule,
NbButtonModule,
FontAwesomeModule,
NbCardModule,
NbActionsModule,
FlexLayoutModule
]
})
export class HeaderModule { }
export class HeaderModule {
}

View File

@ -1,16 +1 @@
<nb-card>
<nb-card-header>
<button (click)="onClickGetProjects()" nbButton>get Projects</button>
</nb-card-header>
<nb-card-body>
<div *ngIf="projects.getValue().length > 0, else noProjects">
{{projects.getValue() | json}}
</div>
<ng-template #noProjects>
{{'No Projects available!'}}
</ng-template>
</nb-card-body>
</nb-card>
<p>app-home-component works...</p>

View File

@ -1,36 +1,14 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {Project} from '../../shared/models/project.model';
import {ProjectService} from '../../shared/services/project.service';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit, OnDestroy {
export class HomeComponent implements OnInit {
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
constructor(private projectService: ProjectService) { }
constructor() { }
ngOnInit(): void {
}
onClickGetProjects(): void {
this.getProjects();
}
getProjects(): void {
this.projectService.getProjects()
.pipe(untilDestroyed(this))
.subscribe((projects) => {
this.projects.next(projects);
});
}
ngOnDestroy(): void {
}
}

View File

@ -0,0 +1,2 @@
export {ProjectOverviewModule} from './project-overview.module';
export {ProjectOverviewRoutingModule} from './project-overview-routing.module';

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ProjectOverviewComponent} from './project-overview.component';
const routes: Routes = [
{
path: '',
component: ProjectOverviewComponent
},
{
path: 'id',
loadChildren: () => import('./project').then(mod => mod.ProjectModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProjectOverviewRoutingModule { }

View File

@ -0,0 +1,84 @@
<div fxLayout="row" fxLayoutGap="2rem">
<div *ngFor="let project of projects | async">
<nb-card accent="success" class="project-card">
<nb-card-header fxLayoutAlign="start center"
routerLink="id"
fragment="{{project.id}}"
class="project-link project-header"
[state]="{selectedProject:project}">
<h4>{{project?.title}}</h4>
</nb-card-header>
<nb-card-body class="project-link"
routerLink="id"
fragment="{{project.id}}"
[state]="{selectedProject:project}">
<p class="project-subheader">
{{'project.client' | translate}}:
</p>
<span class="project-paragraph">
{{project?.client}}
</span>
<p class="project-subheader">
{{'project.tester' | translate}}:
</p>
<span class="project-paragraph">
{{project?.tester}}
</span>
<p class="project-subheader">
{{'project.createdAt' | translate}}:
</p>
<span class="project-paragraph">
{{project?.createdAt | dateTimeFormat}}
</span>
</nb-card-body>
<nb-card-footer>
<!--ToDo: Display correct progress of project-->
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start end">
<nb-progress-bar class="project-progress"
status="warning"
[value]="40"
[displayValue]="true">
</nb-progress-bar>
<button nbButton
status="primary"
size="small"
class="project-button"
(click)="onClickEditProject()">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<button nbButton
status="danger"
size="small"
class="project-button"
(click)="onClickDeleteProject()">
<fa-icon [icon]="fa.faTrash"></fa-icon>
</button>
</div>
</nb-card-footer>
</nb-card>
</div>
</div>
<div *ngIf="projects.getValue().length === 0 && !isLoading()" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'project.overview.no.projects' | translate}}
</p>
</div>
<div fxLayoutAlign="end end">
<button nbButton
status="primary"
size="large"
shape="round"
class="add-project-button"
(click)="onClickAddProject()">
<fa-icon [icon]="fa.faPlus" class="new-project-icon"></fa-icon>
{{'project.overview.add.project' | translate}}
</button>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,50 @@
.project-card {
max-width: 22rem;
width: 22rem;
min-width: 20rem;
max-height: 100%;
height: 100%;
min-height: 100%;
.project-header {
max-height: 8rem;
height: 8rem;
min-height: 6rem;
}
.project-subheader {
font-size: 1.25rem;
font-weight: bold;
}
.project-paragraph {
font-size: 1.15rem;
font-style: italic;
}
.project-progress {
max-width: 65%;
width: 65%;
min-width: 65%;
}
.project-button {
height: 1.425rem;
}
}
.project-link:hover {
cursor: pointer !important;
}
.add-project-button {
margin: 6rem 2rem 6rem 0;
.new-project-icon {
padding-right: 0.5rem;
}
}
.error-text {
font-size: 1.25rem;
font-weight: bold;
}

View File

@ -0,0 +1,79 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProjectOverviewComponent} from './project-overview.component';
import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe';
import {CommonModule} from '@angular/common';
import {ProjectOverviewRoutingModule} from './project-overview-routing.module';
import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {ProjectService} from '@shared/services/project.service';
import {HttpLoaderFactory} from '../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {ProjectServiceMock} from '@shared/services/project.service.mock';
import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {KeycloakService} from 'keycloak-angular';
describe('ProjectOverviewComponent', () => {
let component: ProjectOverviewComponent;
let fixture: ComponentFixture<ProjectOverviewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ProjectOverviewComponent,
LoadingSpinnerComponent,
DateTimeFormatPipe
],
imports: [
CommonModule,
ProjectOverviewRoutingModule,
NbCardModule,
NbButtonModule,
FlexLayoutModule,
BrowserAnimationsModule,
FontAwesomeModule,
TranslateModule,
NbProgressBarModule,
NbSpinnerModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectOverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {Project} from '@shared/models/project.model';
import {BehaviorSubject, Observable} from 'rxjs';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {ProjectService} from '@shared/services/project.service';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {tap} from 'rxjs/operators';
@Component({
selector: 'app-project-overview',
templateUrl: './project-overview.component.html',
styleUrls: ['./project-overview.component.scss']
})
export class ProjectOverviewComponent implements OnInit, OnDestroy {
readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
constructor(
private readonly projectService: ProjectService,
private readonly notificationService: NotificationService) {
}
ngOnInit(): void {
this.getProjects();
}
getProjects(): void {
this.projectService.getProjects()
.pipe(
untilDestroyed(this),
tap(() => this.loading$.next(true))
)
.subscribe( {
next: (projects) => {
this.projects.next(projects);
this.loading$.next(false);
},
error: err => {
console.log(err);
this.notificationService.showPopup('project.popup.not.found', PopupType.FAILURE);
this.loading$.next(false);
}
});
}
onClickAddProject(): void {
console.log('to be implemented...');
}
onClickEditProject(): void {
console.log('to be implemented...');
}
onClickDeleteProject(): void {
console.log('to be implemented...');
}
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
}

View File

@ -0,0 +1,30 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProjectOverviewComponent} from './project-overview.component';
import {ProjectOverviewRoutingModule} from './project-overview-routing.module';
import {NbButtonModule, NbCardModule, NbProgressBarModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core';
import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe';
import {ProjectModule} from './project';
@NgModule({
declarations: [
ProjectOverviewComponent,
DateTimeFormatPipe
],
imports: [
CommonModule,
ProjectOverviewRoutingModule,
NbCardModule,
NbButtonModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
NbProgressBarModule,
ProjectModule
]
})
export class ProjectOverviewModule {
}

View File

@ -0,0 +1,2 @@
export {ProjectModule} from './project.module';
export {ProjectRoutingModule} from './project-routing.module';

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class ProjectRoutingModule { }

View File

@ -0,0 +1,6 @@
<nb-layout>
<nb-layout-header>
<p>{{selectedProjectTitle}} works!</p>
</nb-layout-header>
</nb-layout>

View File

@ -0,0 +1,57 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProjectComponent} from './project.component';
import {CommonModule} from '@angular/common';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NbLayoutModule} from '@nebular/theme';
import {KeycloakService} from 'keycloak-angular';
describe('ProjectComponent', () => {
let component: ProjectComponent;
let fixture: ComponentFixture<ProjectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ProjectComponent
],
imports: [
CommonModule,
NbLayoutModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
KeycloakService
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-project',
templateUrl: './project.component.html',
styleUrls: ['./project.component.scss']
})
export class ProjectComponent implements OnInit {
selectedProjectTitle: string = history?.state?.selectedProject ? history?.state?.selectedProject.title : '';
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,30 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {ProjectComponent} from './project.component';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {NbCardModule, NbLayoutModule, NbSpinnerModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
@NgModule({
declarations: [
ProjectComponent,
LoadingSpinnerComponent
],
exports: [
LoadingSpinnerComponent
],
imports: [
CommonModule,
RouterModule.forChild([{
path: '',
component: ProjectComponent
}]),
NbCardModule,
NbSpinnerModule,
FlexLayoutModule,
NbLayoutModule,
]
})
export class ProjectModule {
}

View File

@ -0,0 +1,20 @@
@import "_variables.scss";
nb-layout {
width: 100vw;
height: 100vh;
nb-layout-header {
max-width: 100vw;
height: $header-height;
justify-content: center;
}
nb-layout-column {
width: 100vw;
height: calc(100vh - #{$header-height});
overflow: auto;
}
}

View File

@ -1,6 +0,0 @@
@mixin ngx-layout() {
@include media-breakpoint-down(is) {
.row {
}
}
}

View File

@ -6,8 +6,6 @@
// loading progress bar theme
@import './pace.theme';
@import './layout';
@import './overrides';
@import './variables';
@ -23,8 +21,6 @@ body {
// framework global styles
@include nb-theme-global();
@include ngx-layout();
@include nb-overrides();
};
/* You can add global styles to this file, and also import other style files */

View File

@ -47,7 +47,7 @@ export class ThemeModule {
providers: [
...NbThemeModule.forRoot(
{
name: 'corporate',
name: DARK_THEME.name,
},
[CORPORATE_THEME, DARK_THEME],
).providers,

View File

@ -22,5 +22,17 @@
"title": "Einloggen",
"failed": "Benutzername oder Passwort falsch",
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
},
"project": {
"overview": {
"add.project": "Projekt hinzufügen",
"no.projects": "Keine Projekte verfügbar"
},
"popup": {
"not.found": "Keine Projekte gefunden"
},
"client": "Klient",
"tester": "Tester",
"createdAt": "Erstellt am"
}
}

View File

@ -11,7 +11,7 @@
"warning": "!",
"info": "",
"error.position": {
"permissionDenied": "Permission Denied",
"permissionDenied": "Permission denied",
"timeout": "Timeout"
},
"login": {
@ -22,5 +22,17 @@
"title": "Login",
"failed": "Wrong username or password",
"unauthorized": "User not found. Please register and try again"
},
"project": {
"overview": {
"add.project": "Add project",
"no.projects": "No projects available"
},
"popup": {
"not.found": "No projects found"
},
"client": "Client",
"tester": "Tester",
"createdAt": "Created at"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,7 +5,7 @@
<title>SecurityC4POAngular</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--<link rel="icon" type="image/x-icon" href="src/favicon.ico">-->
<link rel="icon" type="image/x-icon" href="assets/images/favicons/favicon.ico">
</head>
<body id="loader-wrapper">
<app-root id="loader"></app-root>

View File

@ -1,12 +1,8 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Store} from '@ngxs/store';
import {Observable, of} from 'rxjs';
import {SessionState} from '../stores/session-state/session-state';
import {catchError, map} from 'rxjs/operators';
import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular';
import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions';
import {User} from '../models/user.model';
@Injectable({
providedIn: 'root'

View File

@ -1,40 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { LoginGuardService } from './login-guard.service';
import {RouterTestingModule} from '@angular/router/testing';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '../stores/session-state/session-state';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../app/common-app.module';
import {HttpClient} from '@angular/common/http';
import {KeycloakService} from 'keycloak-angular';
describe('LoginGuardService', () => {
let service: LoginGuardService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([SessionState])
],
providers: [
KeycloakService
]
});
service = TestBed.inject(LoginGuardService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,73 +0,0 @@
import { Injectable } from '@angular/core';
import {Store} from '@ngxs/store';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable, of} from 'rxjs';
import {catchError, map, tap} from 'rxjs/operators';
import {SessionState} from '../stores/session-state/session-state';
import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular';
import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions';
@Injectable({
providedIn: 'root'
})
export class LoginGuardService extends KeycloakAuthGuard implements CanActivate {
constructor(
public readonly router: Router,
protected keycloakAngular: KeycloakService,
private readonly store: Store) {
super(router, keycloakAngular);
}
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this.authenticated) {
this.keycloakAngular.login()
.catch(e => console.error(e));
return reject(false);
}
const requiredRoles: string[] = route.data.roles;
if (!requiredRoles || requiredRoles.length === 0) {
this.store.dispatch(new UpdateIsAuthenticated(true));
this.store.dispatch(new UpdateUser(route.data.user, true));
return resolve(true);
} else {
if (!this.roles || this.roles.length === 0) {
this.store.dispatch(new UpdateIsAuthenticated(false));
this.store.dispatch(new UpdateUser(null, true));
resolve(false);
}
resolve(requiredRoles.every(role => this.roles.indexOf(role) > -1));
}
});
}
/* canActivate(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.isAuthenticated().pipe(
tap((canAccess: boolean) => {
if (canAccess) {
this.router.navigate(['']).then();
}
}),
// return true if login should be loaded (=> invert)
map((canAccess: boolean) => !canAccess)
);
}
/!**
* @return state of authentication
*!/
private isAuthenticated(): Observable<boolean> {
// ToDo: Should check from Authentication Provider
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
.pipe(
map((isLoggedIn: boolean) => {
return isLoggedIn;
}),
catchError(() => {
return of(false);
})
);
}*/
}

View File

@ -0,0 +1,10 @@
export enum CustomPipe {
DATE_FMT_EN = 'MM/dd/yyyy',
DATE_FMT_DE = 'dd.MM.yyyy',
DATE_TIME_FMT_EN = 'MM/dd/yyyy hh:mm a',
DATE_TIME_FMT_DE = 'dd.MM.yyyy HH:mm',
TIME_FMT_EN = 'hh:mm a',
TIME_FMT_DE = 'HH:mm',
PERCENT_FMT = '3.1-2',
TIME_FORMAT = 'shortTime'
}

View File

@ -0,0 +1,4 @@
export enum NumberAndDateFormatSystem {
ENGLISH = 'en-US',
GERMAN = 'de-DE'
}

View File

@ -1,5 +1,3 @@
import { v4 as UUID } from 'uuid';
export class Project {
id: string;
client: string;

View File

@ -0,0 +1,128 @@
import {DateTimeFormatPipe} from './date-time-format.pipe';
import {formatDate, registerLocaleData} from '@angular/common';
import localeDe from '@angular/common/locales/de';
import {CustomPipe} from 'src/shared/models/custom-pipe.mode';
import {User} from '@shared/models/user.model';
import {UpdateUserSettings} from '@shared/stores/session-state/session-state.actions';
import {NumberAndDateFormatSystem} from '@shared/models/number-and-date-time-format.model';
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
import {NgxsModule, Store} from '@ngxs/store';
import {UserService} from '@shared/services/user.service';
import {inject, TestBed} from '@angular/core/testing';
import {HttpClient} from '@angular/common/http';
import {HttpLoaderFactory} from '../../app/common-app.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpClientTestingModule} from '@angular/common/http/testing';
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
userAccount: {
...new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US'),
id: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
isAuthenticated: true
};
describe('DateTimeFormatPipe', () => {
let pipe: DateTimeFormatPipe;
let store: Store;
// tslint:disable-next-line:prefer-const
let dateAndNumberFormat: NumberAndDateFormatSystem;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([SessionState]),
],
declarations: [
DateTimeFormatPipe,
],
providers: [
{provide: UserService},
]
}).compileComponents();
});
beforeEach(inject([Store], (inStore: Store) => {
store = inStore;
store.reset({
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
pipe = new DateTimeFormatPipe(inStore);
})
);
it('should init', () => {
expect(pipe).toBeTruthy();
});
it('return "-" if value is null', () => {
dateAndNumberFormat = NumberAndDateFormatSystem.ENGLISH;
const value: string = null;
expect(pipe.transform(value)).toEqual('-');
});
// ToDo: Enable tests after angular version upgrade
/*
it('return english date time format if NumberAndDateFormatSystem is undefined', () => {
dateAndNumberFormat = undefined;
const value = '2019-11-24T14:34:16.802Z';
const oldFormatDate = formatDate;
const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate')
.mockReturnValue((val, format, locale, timezone) => {
expect(val).toBe(value);
expect(format).toBe(CustomPipe.DATE_TIME_FMT_EN);
expect(locale).toBe('en-US');
expect(timezone).toBeUndefined();
return oldFormatDate(val, format, locale, timezone);
});
pipe.transform(value);
// Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us');
expect(formatSpy).toHaveBeenCalledTimes(1);
});
it('return english date time format if NumberAndDateFormatSystem is english', () => {
dateAndNumberFormat = NumberAndDateFormatSystem.ENGLISH;
const value = '2019-11-24T14:34:16.802Z';
const oldFormatDate = formatDate;
const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate')
.mockReturnValue((val, format, locale, timezone) => {
expect(val).toBe(value);
expect(format).toBe(CustomPipe.DATE_TIME_FMT_EN);
expect(locale).toBe('en-US');
expect(timezone).toBeUndefined();
return oldFormatDate(val, format, locale, timezone);
});
pipe.transform(value);
// Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us');
expect(formatSpy).toHaveBeenCalledTimes(1);
});
it('return german date time format if NumberAndDateFormatSystem is german', () => {
registerLocaleData(localeDe, 'de-DE');
dateAndNumberFormat = NumberAndDateFormatSystem.GERMAN;
const userAccountUpdate = new User('test', 'test', 'test', 'test@test.de', 'de-DE');
store.dispatch(new UpdateUserSettings(userAccountUpdate));
const value = '2019-11-24T14:34:16.802Z';
const oldFormatDate = formatDate;
const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate')
.mockReturnValue((val, format, locale, timezone) => {
expect(val).toBe(value);
expect(format).toBe(CustomPipe.DATE_TIME_FMT_DE);
expect(locale).toBe('de-DE');
expect(timezone).toBeUndefined();
return oldFormatDate(val, format, locale, timezone);
});
expect(pipe.transform(value).endsWith(' Uhr')).toBeTruthy();
// Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us');
expect(formatSpy).toHaveBeenCalledTimes(1);
});
*/
});

View File

@ -0,0 +1,39 @@
import {Pipe, PipeTransform} from '@angular/core';
import {formatDate} from '@angular/common';
import {Store} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {CustomPipe} from '@shared/models/custom-pipe.mode';
@Pipe({
name: 'dateTimeFormat'
})
export class DateTimeFormatPipe implements PipeTransform {
constructor(private store: Store) {
}
/**
* Transforms the value to the appropriate date and time format according to the selected number and date format
* @param value of type any
* @return formatted value as string
*/
transform(value: any): string {
if (!value) {
return '-';
}
const localeDateAndNumberFormat = this.store.selectSnapshot(SessionState.userAccount) ?
// @ts-ignore
this.store.selectSnapshot(SessionState.userAccount.interfaceLang) : 'en-US';
if (!localeDateAndNumberFormat) {
return formatDate(value, CustomPipe.DATE_TIME_FMT_EN, 'en-US');
}
if (localeDateAndNumberFormat === 'de-DE') {
return formatDate(value, CustomPipe.DATE_TIME_FMT_DE, localeDateAndNumberFormat) + ' Uhr';
}
// @ts-ignore
return formatDate(value, CustomPipe.DATE_TIME_FMT_EN, localeDateAndNumberFormat);
}
}

View File

@ -0,0 +1,14 @@
import {ProjectService} from '@shared/services/project.service';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {Project} from '@shared/models/project.model';
export class ProjectServiceMock implements Required<ProjectService> {
private http: HttpClient;
getProjects(): Observable<Project[]> {
return of([]);
}
}

View File

@ -0,0 +1,3 @@
<div fxFill fxLayout="row" fxLayoutAlign="center center" *ngIf="loading">
<nb-spinner fxFlexAlign="center center" status="danger" size="giant" message=""></nb-spinner>
</div>

View File

@ -0,0 +1,31 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoadingSpinnerComponent} from './loading-spinner.component';
import {NbSpinnerModule} from '@nebular/theme';
describe('LoadingSpinnerComponent', () => {
let component: LoadingSpinnerComponent;
let fixture: ComponentFixture<LoadingSpinnerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
LoadingSpinnerComponent
],
imports: [
NbSpinnerModule
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoadingSpinnerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Observable, of} from 'rxjs';
import {untilDestroyed} from 'ngx-take-until-destroy';
@Component({
selector: 'app-loading-spinner',
templateUrl: './loading-spinner.component.html',
styleUrls: ['./loading-spinner.component.scss']
})
export class LoadingSpinnerComponent implements OnInit, OnDestroy {
@Input() isLoading$: Observable<boolean> = of(false);
loading: boolean;
ngOnInit(): void {
this.loading = false;
this.isLoading$
.pipe(untilDestroyed(this))
.subscribe((value: boolean): void => {
this.loading = value;
});
}
ngOnDestroy(): void {
}
}

View File

@ -5,6 +5,9 @@
"outDir": "./out-tsc/app",
"types": []
},
"angularCompilerOptions": {
"enableIvy": false
},
"files": [
"src/main.ts",
"src/polyfills.ts"

View File

@ -18,7 +18,7 @@
"dom"
],
"paths": {
"@shared/*": ["./src/app/shared/*"],
"@shared/*": ["./src/shared/*"],
"@assets/*": ["./src/assets/*"]
}
}

View File

@ -7,7 +7,7 @@
],
"module": "commonjs",
"emitDecoratorMetadata": true,
"allowJs": true
"allowJs": true,
},
"files": [
"src/polyfills.ts"

View File

@ -74,7 +74,6 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("com.auth0:java-jwt:3.18.1")
implementation("org.modelmapper:modelmapper:2.3.2")
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")

View File

@ -29,15 +29,15 @@ To get projects, call the GET request /projects
==== Request example
#include::{snippets}/getProjects/com.securityc4po.api.http-request.adoc[]
include::{snippets}/getProjects/http-request.adoc[]
==== Response example
#include::{snippets}/getProjects/com.securityc4po.api.http-response.adoc[]
include::{snippets}/getProjects/http-response.adoc[]
==== Response structure
#include::{snippets}/getProjects/response-fields.adoc[]
include::{snippets}/getProjects/response-fields.adoc[]
== Change History

View File

@ -1,6 +1,19 @@
package com.securityc4po.api
import com.securityc4po.api.configuration.MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.data.annotation.Id
@SuppressFBWarnings(
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE,
MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK
)
abstract class BaseEntity<T>(
var data: T
) {
@Id
lateinit var id: String
}

View File

@ -5,11 +5,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class Appuser internal constructor(
val sub: String,
val extractedUsername: String,
val token: String
) : UserDetails {
class Appuser internal constructor() : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
return listOf("user").stream().map {

View File

@ -9,17 +9,20 @@ import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
import java.util.stream.Collectors
/** JWT converter that takes the roles from 'groups' claim of JWT token. */
class AppuserJwtAuthConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
class AppuserJwtAuthConverter(private val appuserDetailsService: UserAccountDetailsService) :
Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
val authorities = extractAuthorities(jwt)
val sub = extractSub(jwt)
// val sub = extractSub(jwt)
val username = extractUserName(jwt)
return UsernamePasswordAuthenticationToken(Appuser(sub, username, jwt.tokenValue!!), "n/a", authorities).toMono()
return appuserDetailsService
.findByUsername(username)
.map { user ->
UsernamePasswordAuthenticationToken(user, "n/a", authorities);
}
}
private fun extractSub(jwt: Jwt): String {
@ -51,7 +54,7 @@ class AppuserJwtAuthConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>
val scopes = jwt.getClaims().get(GROUPS_CLAIM).toString()
val roleStringValue = mapper.readTree(scopes).get("roles").toString()
val roles = mapper.readValue<Collection<String>>(roleStringValue)
if (!roles.isEmpty()){
if (!roles.isEmpty()) {
return roles
}
return emptyList()

View File

@ -0,0 +1,14 @@
package com.securityc4po.api.configuration.security
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
@Service
class UserAccountDetailsService : ReactiveUserDetailsService {
override fun findByUsername(username: String): Mono<UserDetails> {
return Appuser().toMono()
}
}

View File

@ -1,8 +1,9 @@
package com.securityc4po.api.configuration.security
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
@ -12,10 +13,12 @@ import org.springframework.web.cors.CorsConfiguration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class WebSecurityConfiguration {
@Configuration
@ComponentScan
class WebSecurityConfiguration(private val userAccountDetailsService: UserAccountDetailsService) {
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
fun setSecurityWebFilterChains(http: ServerHttpSecurity): SecurityWebFilterChain {
http.cors().configurationSource {
CorsConfiguration().apply {
this.applyPermitDefaultValues()
@ -43,6 +46,6 @@ class WebSecurityConfiguration {
@Bean
fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter {
return AppuserJwtAuthConverter()
return AppuserJwtAuthConverter(userAccountDetailsService)
}
}

View File

@ -1,6 +1,9 @@
package com.securityc4po.api.project
import com.securityc4po.api.BaseEntity
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.data.mongodb.core.mapping.Document
@Document(collection = "projects")
@ -19,6 +22,7 @@ fun ProjectEntity.toProject() : Project {
)
}
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
fun List<ProjectEntity>.toProjects(): List<Project> {
return this.map {
it.toProject()

View File

@ -1,10 +1,14 @@
package com.securityc4po.api.project
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
class ProjectService(private val projectRepository: ProjectRepository) {
var logger = getLoggerFor<ProjectService>()

View File

@ -1,7 +1,8 @@
## IdentityProvider (Keycloak) ##
# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
# keycloakhost=localhost
# keycloak.client.url=http://localhost:8888/
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:8888
keycloak.client.realm.path=auth/realms/c4po_realm_local/
## Database (MONGODB) Config ##
spring.data.mongodb.host=c4po-db

View File

@ -1,7 +1,7 @@
## IdentityProvider (Keycloak) ##
# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
# keycloakhost=localhost
# keycloak.client.url=http://localhost:8888/
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:8888/
## Database (MONGODB) Config ##
spring.data.mongodb.host=localhost

View File

@ -0,0 +1,9 @@
## IdentityProvider (Keycloak) ##
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9999/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:9999
keycloak.client.realm.path=auth/realms/c4po_realm_local/
## Database (MONGODB) Config ##
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27021

View File

@ -18,6 +18,5 @@ spring.data.mongodb.auto-index-creation=true
## IdentityProvider (Keycloak) ##
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:8888/
# keycloak.client.realm.path=auth/realms/c4po_realm_local/
idp.jwt.claim.name.user=username
keycloak.client.url=http://localhost:8888
keycloak.client.realm.path=auth/realms/c4po_realm_local/

View File

@ -11,6 +11,7 @@ import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestPropertySource
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.client.RestTemplate
@ -24,7 +25,7 @@ import java.nio.file.Paths
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureWireMock(port = 0)
@TestPropertySource(properties = [
"spring.data.mongodb.port=10002",
"spring.data.mongodb.port=27017",
"spring.data.mongodb.authentication-database=admin",
"spring.data.mongodb.password=test",
"spring.data.mongodb.username=testuser",
@ -46,12 +47,12 @@ abstract class BaseContainerizedTest {
}.withFileFromPath("insert-mongodb-user.js", Paths.get(MountableFile.forClasspathResource("insert-mongodb-user.js", 700).resolvedPath))
).apply {
withCreateContainerCmdModifier {
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(10002), ExposedPort(27017)))
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(27017), ExposedPort(27017)))
}
start()
}
val keycloakContainer = KGenericContainerFromImage(DockerImageName.parse("jboss/keycloak:6.0.1")).apply {
val keycloakContainer = KGenericContainerFromImage(DockerImageName.parse("jboss/keycloak:11.0.3")).apply {
withEnv("KEYCLOAK_USER", "admin")
withEnv("KEYCLOAK_PASSWORD", "admin")
withEnv("KEYCLOAK_IMPORT", "/tmp/realm.json")
@ -59,9 +60,7 @@ abstract class BaseContainerizedTest {
withCreateContainerCmdModifier {
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(8888), ExposedPort(8080)))
}
withCopyFileToContainer(MountableFile.forClasspathResource("outdated_realm-export.json", 700), "/tmp/realm.json")
withCopyFileToContainer(MountableFile.forClasspathResource("create-keycloak-user.sh", 700),
"/opt/jboss/create-keycloak-user.sh")
withCopyFileToContainer(MountableFile.forClasspathResource("realm-export.json", 700), "/tmp/realm.json")
start()
println("== Inserting users must wait until Keycloak is started completely ==")
execInContainer("sh", "/opt/jboss/create-keycloak-user.sh")
@ -80,10 +79,10 @@ abstract class BaseContainerizedTest {
headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
val map = LinkedMultiValueMap<Any, Any>()
map.add("grant_type", "password")
map.add("client_id", clientId)
map.add("username", username)
map.add("password", password)
map.add("grant_type", "password")
map.add("client_secret", "secret")
val responseString = restTemplate.postForObject("$keycloakHost/auth/realms/$realm/protocol/openid-connect/token",
HttpEntity<Any>(map, headers), String::class.java)

View File

@ -2,13 +2,15 @@ package com.securityc4po.api.project
import com.github.tomakehurst.wiremock.common.Json
import com.securityc4po.api.BaseDocumentationIntTest
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.restdocs.operation.preprocess.Preprocessors
@ -16,8 +18,11 @@ import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
@AutoConfigureWireMock(port = 0)
@SuppressFBWarnings(SIC_INNER_SHOULD_BE_STATIC)
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
@Autowired
@ -25,18 +30,21 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
@BeforeEach
fun init() {
cleanUp()
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetProjects {
@Test
fun getProjects() {
/* Implement after the implementation of database */
/*webTestClient.get().uri("/v1/projects")
.header("")
webTestClient.get().uri("/projects")
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().doesNotExist("")
@ -49,14 +57,14 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
Preprocessors.prettyPrint()
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested Project"),
PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING).description("The name of the client of the requested Project"),
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING).description("The title of the requested Project"),
PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("The date where the Project was created at"),
PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING).description("The user that is used as a tester in the Project"),
PayloadDocumentation.fieldWithPath("[].logo").type(JsonFieldType.STRING).description("The sensors contained in the Project")
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested project"),
PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING).description("The name of the client of the requested project"),
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING).description("The title of the requested project"),
PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("The date where the project was created at"),
PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING).description("The user that is assigned as a tester in the project"),
PayloadDocumentation.fieldWithPath("[].createdBy").type(JsonFieldType.STRING).description("The id of the user that created the project")
)
))*/
))
}
val projectOne = Project(
@ -82,30 +90,36 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
)
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), Project::class.java)
}
private fun persistBasicTestScenario() {
// setup test data
val projectOne = Project(
id = "260aa538-0873-43fc-84de-3a09b008646d",
client = "",
title = "",
createdAt = "",
tester = "",
createdBy = ""
id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9",
client = "E Corp",
title = "Some Mock API (v1.0) Scanning",
createdAt = "2021-01-10T18:05:00Z",
tester = "Novatester",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
val projectTwo = Project(
id = "260aa538-0873-43fc-84de-3a09b008646d",
client = "",
title = "",
createdAt = "",
tester = "",
createdBy = ""
id = "61360a47-796b-4b3f-abf9-c46c668596c5",
client = "Allsafe",
title = "CashMyData (iOS)",
createdAt = "2021-01-10T18:05:00Z",
tester = "Elliot",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
cleanUp()
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))
mongoTemplate.save(ProjectEntity(projectTwo))
}
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -2,34 +2,25 @@ package com.securityc4po.api.project
import com.github.tomakehurst.wiremock.common.Json
import com.securityc4po.api.BaseIntTest
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
import com.securityc4po.api.configuration.URF_UNREAD_FIELD
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import io.netty.handler.ssl.SslContextBuilder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.test.context.TestPropertySource
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.util.ResourceUtils
import reactor.netty.http.client.HttpClient
import java.time.Duration
@AutoConfigureWireMock(port = 0)
/*@TestPropertySource(
properties = [
"keycloak.client.url=http://localhost:${'$'}{wiremock.server.port}"
]
)*/
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
URF_UNREAD_FIELD,
"Unread field will become used after database implementation"
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class ProjectControllerIntTest : BaseIntTest() {
@ -39,6 +30,7 @@ class ProjectControllerIntTest : BaseIntTest() {
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
private lateinit var webTestClient: WebTestClient
@BeforeEach
@ -51,20 +43,24 @@ class ProjectControllerIntTest : BaseIntTest() {
@BeforeEach
fun init() {
cleanUp()
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetProjects {
@Test
fun `requesting projects successfully`() {
webTestClient.get().uri("/v1/projects")
webTestClient.get().uri("/projects")
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Application-Name", "security-c4po-api")
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody().json(Json.write(getProjects()))
}
@ -91,12 +87,6 @@ class ProjectControllerIntTest : BaseIntTest() {
)
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), Project::class.java)
tokenAdmin = "n/a"
}
private fun persistBasicTestScenario() {
// setup test data
val projectOne = Project(
@ -115,7 +105,7 @@ class ProjectControllerIntTest : BaseIntTest() {
tester = "Elliot",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
cleanUp()
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))
mongoTemplate.save(ProjectEntity(projectTwo))
}
@ -123,4 +113,10 @@ class ProjectControllerIntTest : BaseIntTest() {
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -1,28 +0,0 @@
#!/usr/bin/env bash
cd keycloak/bin
sleep 20
./kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin --password admin
USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \
-s email=Test.Admin@heros.com \
-s firstName=test \
-s lastName=admin \
-s attributes.lang="de-DE" \
-s attributes.datenumberformat="en-US" \
-o --fields id | jq '.id' | tr -d '"')
./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n
./kcadm.sh add-roles --uusername test_admin --rolename c4po_admin -r c4po_realm_test
./kcadm.sh add-roles -r c4po_realm_test --uusername test_admin --cclientid realm-management --rolename create-client --rolename view-users
USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_user \
-s email=Test.User@heros.com \
-s firstName=test \
-s lastName=user \
-s attributes.lang="de-DE" \
-s attributes.datenumberformat="en-US" \
-o --fields id | jq '.id' | tr -d '"')
./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n
./kcadm.sh add-roles --uusername test_user --rolename c4po_user -r c4po_realm_test
./kcadm.sh add-roles -r c4po_realm_test --uusername test_user --cclientid realm-management --rolename create-client --rolename view-users

File diff suppressed because it is too large Load Diff

View File

@ -365,26 +365,51 @@
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false,
"webAuthnPolicyPasswordlessAcceptableAaguids" : [ ],
"users" : [ {
"id" : "10e06d7a-8dd0-4ecd-8963-056b45079c4f",
"createdTimestamp" : 1617897245335,
"username" : "ttt",
"id" : "8f725a10-bdf5-4530-a185-4627fb092d78",
"createdTimestamp" : 1634988614562,
"username" : "test_admin",
"enabled" : true,
"totp" : false,
"emailVerified" : false,
"emailVerified" : true,
"firstName" : "test",
"lastName" : "user",
"lastName" : "admin",
"email" : "testadmin@test.de",
"credentials" : [ {
"id" : "7026fefc-ae26-442b-acae-92f1f2d24eac",
"id" : "52ec7433-9b1c-46f1-ae5b-4f9851d1424c",
"type" : "password",
"createdDate" : 1617897287400,
"secretData" : "{\"value\":\"mhW4yxOg+8bcyPF4yWsfPZnLGUp4oaqc9aNA+WBcpr9qXgs/Jw+rM2VlLEgeD/kXGItcScA8V20sVGrMWT94Yw==\",\"salt\":\"nkH510WAwjKZJqd/ZEkIHA==\"}",
"createdDate" : 1634988624873,
"secretData" : "{\"value\":\"acmk/oJGV9GGtvrUjT1NJZPTizGvO9gvCj5tvzOSZKxW3OPwPYWfCE91OfmmLXOZcVddO8xdDufDLFliu52bxA==\",\"salt\":\"oHvWAC39azkfs7wJOwLdlQ==\"}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "uma_authorization", "c4po_user", "c4po_admin" ],
"realmRoles" : [ "uma_authorization", "offline_access", "c4po_admin" ],
"clientRoles" : {
"account" : [ "view-profile", "manage-account" ]
},
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "34869cfa-6454-4379-94d7-3a19fc2d98e0",
"createdTimestamp" : 1634988651658,
"username" : "test_user",
"enabled" : true,
"totp" : false,
"emailVerified" : true,
"firstName" : "test",
"lastName" : "user",
"email" : "testuser@test.de",
"credentials" : [ {
"id" : "58d1b3a1-7d28-4d40-b4cd-bfab0c9472d9",
"type" : "password",
"createdDate" : 1634988666427,
"secretData" : "{\"value\":\"IUz/GL+eof5R8MNEy4VB04sr1YBmyyy6/HdR0QFzKXRkhHcDWxO+vn1S3jy5n+iB/JBdFcrprI3/5rPgfyBiaA==\",\"salt\":\"l5BdVHeOO3RVvpxDWK2Jnw==\"}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "uma_authorization", "c4po_user", "offline_access" ],
"clientRoles" : {
"c4po_local" : [ "user" ],
"account" : [ "view-profile", "manage-account" ]
},
"notBefore" : 0,
@ -1208,7 +1233,7 @@
"subType" : "anonymous",
"subComponents" : { },
"config" : {
"allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper" ]
"allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ]
}
}, {
"id" : "cc2d0cd7-3d3f-4b0a-ad95-7118f36bf188",
@ -1240,7 +1265,7 @@
"subType" : "authenticated",
"subComponents" : { },
"config" : {
"allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ]
"allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper" ]
}
}, {
"id" : "92230e65-7480-44c3-af2d-72ddee758cbc",
@ -1289,7 +1314,7 @@
"internationalizationEnabled" : false,
"supportedLocales" : [ ],
"authenticationFlows" : [ {
"id" : "fa5fc78f-19a9-4737-868b-618163f28c79",
"id" : "9f2b9c09-a331-4126-9f89-5d459e911053",
"alias" : "Account verification options",
"description" : "Method with which to verity the existing account",
"providerId" : "basic-flow",
@ -1309,7 +1334,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "01735b0f-139f-46e5-bb63-f797a27efa77",
"id" : "6802ac9b-69cf-4838-99d4-747dd9de3f32",
"alias" : "Authentication Options",
"description" : "Authentication options.",
"providerId" : "basic-flow",
@ -1335,7 +1360,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "a7666cf0-626c-48c4-9e71-e408832de725",
"id" : "c1e6806d-ff33-46bc-be0f-70eb6924fc92",
"alias" : "Browser - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -1355,7 +1380,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "1dfabb7a-efdd-4964-bba5-389cad79b654",
"id" : "8b0f8be2-5933-4fc1-9546-484ecdf0766b",
"alias" : "Direct Grant - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -1375,7 +1400,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "c3b2bf2b-3da8-430d-a9b7-8793c3dc30a3",
"id" : "d1b5d09c-36eb-40e2-8c1f-9b414e63b44e",
"alias" : "First broker login - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -1395,7 +1420,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "44343bdf-8592-4242-835f-e349943a110b",
"id" : "ffeb5151-3357-4cd6-aa17-860c60de0cb3",
"alias" : "Handle Existing Account",
"description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId" : "basic-flow",
@ -1415,7 +1440,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "e72b8fcb-cd8b-4e7a-a057-3446b806b538",
"id" : "816cc72c-df79-49ee-b7a4-291f496e145b",
"alias" : "Reset - Conditional OTP",
"description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId" : "basic-flow",
@ -1435,7 +1460,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "2416145b-4d20-493c-bdf7-419898c002ee",
"id" : "5a782d53-4c32-437e-803a-a9e893b0f5eb",
"alias" : "User creation or linking",
"description" : "Flow for the existing/non-existing user alternatives",
"providerId" : "basic-flow",
@ -1456,7 +1481,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "b7ff8aad-2daa-4736-8815-f3e8f0df391e",
"id" : "3faef6d7-2cfd-4190-a8d1-db1b8e953304",
"alias" : "Verify Existing Account by Re-authentication",
"description" : "Reauthentication of existing account",
"providerId" : "basic-flow",
@ -1476,7 +1501,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "8339d3ba-2d0a-4d23-bbfa-a78e4973d3c9",
"id" : "5a57eb55-59f8-4d28-9003-ec350b4053cf",
"alias" : "browser",
"description" : "browser based authentication",
"providerId" : "basic-flow",
@ -1508,7 +1533,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "5ece002a-4e62-4d0d-8705-4b116164b424",
"id" : "1cf8aebb-27ae-4df5-96d4-82d6a0ba3bfd",
"alias" : "clients",
"description" : "Base authentication for clients",
"providerId" : "client-flow",
@ -1540,7 +1565,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "bd27b0dc-bc87-40b7-a626-491b9955668d",
"id" : "1ec7e0bf-7bbe-4841-a21d-bfbc6cb492ae",
"alias" : "direct grant",
"description" : "OpenID Connect Resource Owner Grant",
"providerId" : "basic-flow",
@ -1566,7 +1591,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "2db79d60-7c9d-4516-80f0-0c5d60349899",
"id" : "d633bda1-5c4e-43c4-9a0e-776502549b57",
"alias" : "docker auth",
"description" : "Used by Docker clients to authenticate against the IDP",
"providerId" : "basic-flow",
@ -1580,7 +1605,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "25a92fbe-7d4d-46bc-a751-29ef844290a3",
"id" : "ac608b5f-4849-4830-a928-b51f996f77a8",
"alias" : "first broker login",
"description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId" : "basic-flow",
@ -1601,7 +1626,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "26f6a5db-9be8-446c-82d0-6f4e29b5f08d",
"id" : "b0731e4d-5935-4c29-8a4d-e7124e9eb164",
"alias" : "forms",
"description" : "Username, password, otp and other auth forms.",
"providerId" : "basic-flow",
@ -1621,7 +1646,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "05a94701-ad98-4bbc-a162-746a107afba5",
"id" : "b09ede5d-b7cd-4e9a-bb8d-e7caa880bc6d",
"alias" : "http challenge",
"description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId" : "basic-flow",
@ -1641,7 +1666,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "75347884-d4cb-4eba-9b89-63566d509b92",
"id" : "96479ab9-2db1-47c9-ba6b-c9832e54937d",
"alias" : "registration",
"description" : "registration flow",
"providerId" : "basic-flow",
@ -1656,7 +1681,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "74e3a2d3-ecda-400d-8bff-0926dc272e4b",
"id" : "ae46ccfd-1bd2-4717-8380-de1040d7fe43",
"alias" : "registration form",
"description" : "registration form",
"providerId" : "form-flow",
@ -1688,7 +1713,7 @@
"autheticatorFlow" : false
} ]
}, {
"id" : "6eae8652-baf7-4a7d-80a4-1711906caec7",
"id" : "99d2284f-52e8-4c09-a500-6a7d1c5b108e",
"alias" : "reset credentials",
"description" : "Reset credentials for a user if they forgot their password or something",
"providerId" : "basic-flow",
@ -1720,7 +1745,7 @@
"autheticatorFlow" : true
} ]
}, {
"id" : "6135710b-b019-4117-ba32-578d3d496b2a",
"id" : "db4d353d-4b8d-4241-a09b-d4246590c82a",
"alias" : "saml ecp",
"description" : "SAML ECP Profile Authentication Flow",
"providerId" : "basic-flow",
@ -1735,13 +1760,13 @@
} ]
} ],
"authenticatorConfig" : [ {
"id" : "3d3735a0-1362-4f0d-9306-bfc727da1b5b",
"id" : "1a64855a-6fe8-40df-90eb-bd936aed6e76",
"alias" : "create unique user config",
"config" : {
"require.password.update.after.registration" : "false"
}
}, {
"id" : "c1f4a15f-8234-4f0f-affa-baf610b001e1",
"id" : "e10161fb-9f1c-4c5c-ba84-5bcbf0dd358d",
"alias" : "review profile config",
"config" : {
"update.profile.on.first.login" : "missing"

View File

@ -4,11 +4,12 @@ sleep 20
./kcadm.sh config credentials --server http://localhost:8888/auth --realm master --user admin --password admin
USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \
-s email=Troy.Stewart@heros.com \
-s email=testadmin@test.de \
-s firstName=test \
-s lastName=admin \
-s attributes.lang="de-DE" \
-s attributes.datenumberformat="en-US" \
-s enabled=true \
-o --fields id | jq '.id' | tr -d '"')
./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n
@ -16,11 +17,12 @@ USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \
./kcadm.sh add-roles -r c4po_realm_test --uusername test_admin --cclientid realm-management --rolename create-client --rolename view-users
USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_user \
-s email=Troy.Stewart@heros.com \
-s email=testuser@test.de \
-s firstName=test \
-s lastName=user \
-s attributes.lang="de-DE" \
-s attributes.datenumberformat="en-US" \
-s enabled=true \
-o --fields id | jq '.id' | tr -d '"')
./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n

View File

@ -7,12 +7,6 @@ services:
container_name: c4po-api
environment:
- SPRING_PROFILES_ACTIVE=COMPOSE
depends_on:
- c4po-db
- c4po-keycloak
links:
- c4po-db
- c4po-keycloak
deploy:
resources:
limits:

View File

@ -5,8 +5,6 @@ services:
build: '../../security-c4po-angular'
image: security-c4po-angular:latest
container_name: c4po-angular
depends_on:
- c4po-keycloak
deploy:
resources:
limits:

View File

@ -3,8 +3,6 @@ version: '3.1'
services:
c4po-keycloak:
container_name: c4po-keycloak
depends_on:
- c4po-keycloak-postgres
image: jboss/keycloak:11.0.3
volumes:
- ../cfg/c4po_realm_export.json:/tmp/c4po_realm_export.json