feat: added header and project overview to FE and fixed keycloak test container
This commit is contained in:
parent
0541586aaf
commit
d1c1a3814b
|
@ -24,8 +24,8 @@
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/assets/images/favicons/favicon.ico",
|
||||||
"src/favicon-c4po.ico",
|
"src/assets/images/favicons/corporate_favicon.ico",
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
@ -96,8 +96,8 @@
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/assets/images/favicons/favicon.ico",
|
||||||
"src/favicon-c4po.ico",
|
"src/assets/images/favicons/corporate_favicon.ico",
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||||
|
'@assets/(.*)': '<rootDir>/src/assets/$1',
|
||||||
|
'@shared/(.*)': '<rootDir>/src/shared/$1'
|
||||||
},
|
},
|
||||||
preset: 'jest-preset-angular',
|
preset: 'jest-preset-angular',
|
||||||
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||||
|
|
|
@ -2510,9 +2510,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "12.19.8",
|
"version": "12.20.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.33.tgz",
|
||||||
"integrity": "sha512-D4k2kNi0URNBxIRCb1khTnkWNHv8KSL1owPmS/K5e5t8B2GzMReY7AsJIY1BnP5KdlgC4rj9jk2IkDMasIE7xg==",
|
"integrity": "sha512-5XmYX2GECSa+CxMYaFsr2mrql71Q4EvHjKS+ox/SiwSdaASMoBIWE6UmZqFO+VX1jIcsYLStI4FFoB6V7FeIYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/normalize-package-data": {
|
"@types/normalize-package-data": {
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/jest": "26.0.15",
|
"@types/jest": "26.0.15",
|
||||||
"@types/node": "^12.19.8",
|
"@types/node": "^12.20.33",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
|
||||||
import {HomeComponent} from './home/home.component';
|
import {HomeComponent} from './home/home.component';
|
||||||
import {AuthGuardService} from '../shared/guards/auth-guard.service';
|
import {AuthGuardService} from '../shared/guards/auth-guard.service';
|
||||||
|
|
||||||
export const START_PAGE = 'home';
|
export const START_PAGE = 'projects';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -12,11 +12,11 @@ const routes: Routes = [
|
||||||
canActivate: [AuthGuardService]
|
canActivate: [AuthGuardService]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'projects',
|
||||||
loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule),
|
loadChildren: () => import('./project-overview').then(mod => mod.ProjectOverviewModule),
|
||||||
canActivate: [AuthGuardService]
|
canActivate: [AuthGuardService]
|
||||||
},
|
},
|
||||||
// ToDo: Exchange default Keycloak login with self made login
|
// ToDo: Remove after default Keycloak login mask got reworked
|
||||||
/*{
|
/*{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
loadChildren: () => import('./login').then(mod => mod.LoginModule),
|
loadChildren: () => import('./login').then(mod => mod.LoginModule),
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
@import "../assets/@theme/styles/_variables.scss";
|
@import "../assets/@theme/styles/_variables.scss";
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
width: 90vw;
|
|
||||||
height: calc(90vh - #{$header-height});
|
|
||||||
|
|
||||||
.scrollable-content {
|
.scrollable-content {
|
||||||
width: 100%;
|
|
||||||
max-width: 100vw;
|
|
||||||
height: 100%;
|
|
||||||
max-height: calc(100vh - #{$header-height});
|
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {NbEvaIconsModule} from '@nebular/eva-icons';
|
||||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
import {HttpLoaderFactory} from './common-app.module';
|
import {HttpLoaderFactory} from './common-app.module';
|
||||||
import {HttpClient} from '@angular/common/http';
|
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 {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 {NgxsModule} from '@ngxs/store';
|
||||||
import {HeaderModule} from './header/header.module';
|
import {HeaderModule} from './header/header.module';
|
||||||
import {KeycloakService} from 'keycloak-angular';
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
|
@ -17,14 +17,15 @@ import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-f
|
||||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {far} from '@fortawesome/free-regular-svg-icons';
|
import {far} from '@fortawesome/free-regular-svg-icons';
|
||||||
import {NgxsModule} from '@ngxs/store';
|
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 {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 {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
import {HeaderModule} from './header/header.module';
|
import {HeaderModule} from './header/header.module';
|
||||||
import {HomeModule} from './home/home.module';
|
import {HomeModule} from './home/home.module';
|
||||||
import {KeycloakService} from 'keycloak-angular';
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
import {httpInterceptorProviders} from '../shared/interceptors';
|
import {httpInterceptorProviders} from '@shared/interceptors';
|
||||||
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -53,7 +54,8 @@ import {httpInterceptorProviders} from '../shared/interceptors';
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
HeaderModule,
|
HeaderModule,
|
||||||
HomeModule
|
HomeModule,
|
||||||
|
FlexLayoutModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
HttpClient,
|
HttpClient,
|
||||||
|
|
|
@ -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 { }
|
|
|
@ -1 +0,0 @@
|
||||||
<p>dashboard works!</p>
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
export {DashboardModule} from './dashboard.module';
|
|
||||||
export {DashboardRoutingModule} from './dashboard-routing.module';
|
|
|
@ -1,4 +1,26 @@
|
||||||
<div class="c4po-header">
|
<div class="header" fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="2rem">
|
||||||
<p>header works!</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
|
|
||||||
|
@import '~@nebular/theme/styles/global/breakpoints';
|
||||||
@import "../../assets/@theme/styles/_variables.scss";
|
@import "../../assets/@theme/styles/_variables.scss";
|
||||||
|
|
||||||
.c4po-header {
|
@mixin nb-overrides {
|
||||||
height: $header-height;
|
display: inline-flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.toggle-dark-mode {
|
.header {
|
||||||
text-align: right;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
|
@ -8,9 +16,25 @@ describe('HeaderComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
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(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -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({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
templateUrl: './header.component.html',
|
templateUrl: './header.component.html',
|
||||||
styleUrls: ['./header.component.scss']
|
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 {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { NgModule } from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {HeaderComponent} from './header.component';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -10,7 +13,13 @@ import {HeaderComponent} from './header.component';
|
||||||
HeaderComponent
|
HeaderComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule
|
CommonModule,
|
||||||
|
NbButtonModule,
|
||||||
|
FontAwesomeModule,
|
||||||
|
NbCardModule,
|
||||||
|
NbActionsModule,
|
||||||
|
FlexLayoutModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class HeaderModule { }
|
export class HeaderModule {
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1 @@
|
||||||
<nb-card>
|
<p>app-home-component works...</p>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
|
@ -1,36 +1,14 @@
|
||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, 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';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrls: ['./home.component.scss']
|
styleUrls: ['./home.component.scss']
|
||||||
})
|
})
|
||||||
export class HomeComponent implements OnInit, OnDestroy {
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
|
constructor() { }
|
||||||
|
|
||||||
constructor(private projectService: ProjectService) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickGetProjects(): void {
|
|
||||||
this.getProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
getProjects(): void {
|
|
||||||
this.projectService.getProjects()
|
|
||||||
.pipe(untilDestroyed(this))
|
|
||||||
.subscribe((projects) => {
|
|
||||||
this.projects.next(projects);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export {ProjectOverviewModule} from './project-overview.module';
|
||||||
|
export {ProjectOverviewRoutingModule} from './project-overview-routing.module';
|
|
@ -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 { }
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export {ProjectModule} from './project.module';
|
||||||
|
export {ProjectRoutingModule} from './project-routing.module';
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProjectRoutingModule { }
|
|
@ -0,0 +1,6 @@
|
||||||
|
<nb-layout>
|
||||||
|
<nb-layout-header>
|
||||||
|
<p>{{selectedProjectTitle}} works!</p>
|
||||||
|
</nb-layout-header>
|
||||||
|
</nb-layout>
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
@mixin ngx-layout() {
|
|
||||||
@include media-breakpoint-down(is) {
|
|
||||||
.row {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,8 +6,6 @@
|
||||||
|
|
||||||
// loading progress bar theme
|
// loading progress bar theme
|
||||||
@import './pace.theme';
|
@import './pace.theme';
|
||||||
|
|
||||||
@import './layout';
|
|
||||||
@import './overrides';
|
@import './overrides';
|
||||||
@import './variables';
|
@import './variables';
|
||||||
|
|
||||||
|
@ -23,8 +21,6 @@ body {
|
||||||
// framework global styles
|
// framework global styles
|
||||||
@include nb-theme-global();
|
@include nb-theme-global();
|
||||||
|
|
||||||
@include ngx-layout();
|
|
||||||
|
|
||||||
@include nb-overrides();
|
@include nb-overrides();
|
||||||
};
|
};
|
||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class ThemeModule {
|
||||||
providers: [
|
providers: [
|
||||||
...NbThemeModule.forRoot(
|
...NbThemeModule.forRoot(
|
||||||
{
|
{
|
||||||
name: 'corporate',
|
name: DARK_THEME.name,
|
||||||
},
|
},
|
||||||
[CORPORATE_THEME, DARK_THEME],
|
[CORPORATE_THEME, DARK_THEME],
|
||||||
).providers,
|
).providers,
|
||||||
|
|
|
@ -22,5 +22,17 @@
|
||||||
"title": "Einloggen",
|
"title": "Einloggen",
|
||||||
"failed": "Benutzername oder Passwort falsch",
|
"failed": "Benutzername oder Passwort falsch",
|
||||||
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"warning": "!",
|
"warning": "!",
|
||||||
"info": "",
|
"info": "",
|
||||||
"error.position": {
|
"error.position": {
|
||||||
"permissionDenied": "Permission Denied",
|
"permissionDenied": "Permission denied",
|
||||||
"timeout": "Timeout"
|
"timeout": "Timeout"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
|
@ -22,5 +22,17 @@
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
"failed": "Wrong username or password",
|
"failed": "Wrong username or password",
|
||||||
"unauthorized": "User not found. Please register and try again"
|
"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 |
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
|
@ -5,7 +5,7 @@
|
||||||
<title>SecurityC4POAngular</title>
|
<title>SecurityC4POAngular</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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>
|
</head>
|
||||||
<body id="loader-wrapper">
|
<body id="loader-wrapper">
|
||||||
<app-root id="loader"></app-root>
|
<app-root id="loader"></app-root>
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
|
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
|
||||||
import {Store} from '@ngxs/store';
|
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 {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular';
|
||||||
import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions';
|
import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions';
|
||||||
import {User} from '../models/user.model';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum NumberAndDateFormatSystem {
|
||||||
|
ENGLISH = 'en-US',
|
||||||
|
GERMAN = 'de-DE'
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
import { v4 as UUID } from 'uuid';
|
|
||||||
|
|
||||||
export class Project {
|
export class Project {
|
||||||
id: string;
|
id: string;
|
||||||
client: string;
|
client: string;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,9 @@
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableIvy": false
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/main.ts",
|
"src/main.ts",
|
||||||
"src/polyfills.ts"
|
"src/polyfills.ts"
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
"dom"
|
"dom"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@shared/*": ["./src/app/shared/*"],
|
"@shared/*": ["./src/shared/*"],
|
||||||
"@assets/*": ["./src/assets/*"]
|
"@assets/*": ["./src/assets/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
],
|
],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"allowJs": true
|
"allowJs": true,
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/polyfills.ts"
|
"src/polyfills.ts"
|
||||||
|
|
|
@ -74,7 +74,6 @@ dependencies {
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||||
implementation("com.auth0:java-jwt:3.18.1")
|
|
||||||
implementation("org.modelmapper:modelmapper:2.3.2")
|
implementation("org.modelmapper:modelmapper:2.3.2")
|
||||||
|
|
||||||
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")
|
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")
|
||||||
|
|
|
@ -29,15 +29,15 @@ To get projects, call the GET request /projects
|
||||||
|
|
||||||
==== Request example
|
==== Request example
|
||||||
|
|
||||||
#include::{snippets}/getProjects/com.securityc4po.api.http-request.adoc[]
|
include::{snippets}/getProjects/http-request.adoc[]
|
||||||
|
|
||||||
==== Response example
|
==== Response example
|
||||||
|
|
||||||
#include::{snippets}/getProjects/com.securityc4po.api.http-response.adoc[]
|
include::{snippets}/getProjects/http-response.adoc[]
|
||||||
|
|
||||||
==== Response structure
|
==== Response structure
|
||||||
|
|
||||||
#include::{snippets}/getProjects/response-fields.adoc[]
|
include::{snippets}/getProjects/response-fields.adoc[]
|
||||||
|
|
||||||
== Change History
|
== Change History
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
package com.securityc4po.api
|
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>(
|
abstract class BaseEntity<T>(
|
||||||
var data: T
|
var data: T
|
||||||
) {
|
) {
|
||||||
|
@Id
|
||||||
|
lateinit var id: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
import org.springframework.security.core.GrantedAuthority
|
import org.springframework.security.core.GrantedAuthority
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
|
||||||
class Appuser internal constructor(
|
class Appuser internal constructor() : UserDetails {
|
||||||
val sub: String,
|
|
||||||
val extractedUsername: String,
|
|
||||||
val token: String
|
|
||||||
) : UserDetails {
|
|
||||||
|
|
||||||
override fun getAuthorities(): Collection<GrantedAuthority> {
|
override fun getAuthorities(): Collection<GrantedAuthority> {
|
||||||
return listOf("user").stream().map {
|
return listOf("user").stream().map {
|
||||||
|
|
|
@ -9,17 +9,20 @@ import org.springframework.security.core.GrantedAuthority
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
import org.springframework.security.oauth2.jwt.Jwt
|
import org.springframework.security.oauth2.jwt.Jwt
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.kotlin.core.publisher.toMono
|
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
/** JWT converter that takes the roles from 'groups' claim of JWT token. */
|
class AppuserJwtAuthConverter(private val appuserDetailsService: UserAccountDetailsService) :
|
||||||
class AppuserJwtAuthConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||||
|
|
||||||
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
|
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
|
||||||
val authorities = extractAuthorities(jwt)
|
val authorities = extractAuthorities(jwt)
|
||||||
val sub = extractSub(jwt)
|
// val sub = extractSub(jwt)
|
||||||
val username = extractUserName(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 {
|
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 scopes = jwt.getClaims().get(GROUPS_CLAIM).toString()
|
||||||
val roleStringValue = mapper.readTree(scopes).get("roles").toString()
|
val roleStringValue = mapper.readTree(scopes).get("roles").toString()
|
||||||
val roles = mapper.readValue<Collection<String>>(roleStringValue)
|
val roles = mapper.readValue<Collection<String>>(roleStringValue)
|
||||||
if (!roles.isEmpty()){
|
if (!roles.isEmpty()) {
|
||||||
return roles
|
return roles
|
||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
package com.securityc4po.api.configuration.security
|
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.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.http.HttpMethod
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
|
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
@ -12,10 +13,12 @@ import org.springframework.web.cors.CorsConfiguration
|
||||||
|
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableReactiveMethodSecurity
|
@EnableReactiveMethodSecurity
|
||||||
class WebSecurityConfiguration {
|
@Configuration
|
||||||
|
@ComponentScan
|
||||||
|
class WebSecurityConfiguration(private val userAccountDetailsService: UserAccountDetailsService) {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
fun setSecurityWebFilterChains(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
http.cors().configurationSource {
|
http.cors().configurationSource {
|
||||||
CorsConfiguration().apply {
|
CorsConfiguration().apply {
|
||||||
this.applyPermitDefaultValues()
|
this.applyPermitDefaultValues()
|
||||||
|
@ -43,6 +46,6 @@ class WebSecurityConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter {
|
fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter {
|
||||||
return AppuserJwtAuthConverter()
|
return AppuserJwtAuthConverter(userAccountDetailsService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.securityc4po.api.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.securityc4po.api.BaseEntity
|
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
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
|
||||||
@Document(collection = "projects")
|
@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> {
|
fun List<ProjectEntity>.toProjects(): List<Project> {
|
||||||
return this.map {
|
return this.map {
|
||||||
it.toProject()
|
it.toProject()
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package com.securityc4po.api.project
|
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 com.securityc4po.api.extensions.getLoggerFor
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
|
||||||
class ProjectService(private val projectRepository: ProjectRepository) {
|
class ProjectService(private val projectRepository: ProjectRepository) {
|
||||||
|
|
||||||
var logger = getLoggerFor<ProjectService>()
|
var logger = getLoggerFor<ProjectService>()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
## IdentityProvider (Keycloak) ##
|
## IdentityProvider (Keycloak) ##
|
||||||
# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
||||||
# keycloakhost=localhost
|
keycloakhost=localhost
|
||||||
# keycloak.client.url=http://localhost:8888/
|
keycloak.client.url=http://localhost:8888
|
||||||
|
keycloak.client.realm.path=auth/realms/c4po_realm_local/
|
||||||
|
|
||||||
## Database (MONGODB) Config ##
|
## Database (MONGODB) Config ##
|
||||||
spring.data.mongodb.host=c4po-db
|
spring.data.mongodb.host=c4po-db
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
## IdentityProvider (Keycloak) ##
|
## IdentityProvider (Keycloak) ##
|
||||||
# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
||||||
# keycloakhost=localhost
|
keycloakhost=localhost
|
||||||
# keycloak.client.url=http://localhost:8888/
|
keycloak.client.url=http://localhost:8888/
|
||||||
|
|
||||||
## Database (MONGODB) Config ##
|
## Database (MONGODB) Config ##
|
||||||
spring.data.mongodb.host=localhost
|
spring.data.mongodb.host=localhost
|
||||||
|
|
|
@ -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
|
|
@ -18,6 +18,5 @@ spring.data.mongodb.auto-index-creation=true
|
||||||
## IdentityProvider (Keycloak) ##
|
## IdentityProvider (Keycloak) ##
|
||||||
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
||||||
keycloakhost=localhost
|
keycloakhost=localhost
|
||||||
keycloak.client.url=http://localhost:8888/
|
keycloak.client.url=http://localhost:8888
|
||||||
# keycloak.client.realm.path=auth/realms/c4po_realm_local/
|
keycloak.client.realm.path=auth/realms/c4po_realm_local/
|
||||||
idp.jwt.claim.name.user=username
|
|
|
@ -11,6 +11,7 @@ import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.context.ActiveProfiles
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.util.LinkedMultiValueMap
|
import org.springframework.util.LinkedMultiValueMap
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
@ -24,7 +25,7 @@ import java.nio.file.Paths
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@AutoConfigureWireMock(port = 0)
|
@AutoConfigureWireMock(port = 0)
|
||||||
@TestPropertySource(properties = [
|
@TestPropertySource(properties = [
|
||||||
"spring.data.mongodb.port=10002",
|
"spring.data.mongodb.port=27017",
|
||||||
"spring.data.mongodb.authentication-database=admin",
|
"spring.data.mongodb.authentication-database=admin",
|
||||||
"spring.data.mongodb.password=test",
|
"spring.data.mongodb.password=test",
|
||||||
"spring.data.mongodb.username=testuser",
|
"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))
|
}.withFileFromPath("insert-mongodb-user.js", Paths.get(MountableFile.forClasspathResource("insert-mongodb-user.js", 700).resolvedPath))
|
||||||
).apply {
|
).apply {
|
||||||
withCreateContainerCmdModifier {
|
withCreateContainerCmdModifier {
|
||||||
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(10002), ExposedPort(27017)))
|
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(27017), ExposedPort(27017)))
|
||||||
}
|
}
|
||||||
start()
|
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_USER", "admin")
|
||||||
withEnv("KEYCLOAK_PASSWORD", "admin")
|
withEnv("KEYCLOAK_PASSWORD", "admin")
|
||||||
withEnv("KEYCLOAK_IMPORT", "/tmp/realm.json")
|
withEnv("KEYCLOAK_IMPORT", "/tmp/realm.json")
|
||||||
|
@ -59,9 +60,7 @@ abstract class BaseContainerizedTest {
|
||||||
withCreateContainerCmdModifier {
|
withCreateContainerCmdModifier {
|
||||||
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(8888), ExposedPort(8080)))
|
it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(8888), ExposedPort(8080)))
|
||||||
}
|
}
|
||||||
withCopyFileToContainer(MountableFile.forClasspathResource("outdated_realm-export.json", 700), "/tmp/realm.json")
|
withCopyFileToContainer(MountableFile.forClasspathResource("realm-export.json", 700), "/tmp/realm.json")
|
||||||
withCopyFileToContainer(MountableFile.forClasspathResource("create-keycloak-user.sh", 700),
|
|
||||||
"/opt/jboss/create-keycloak-user.sh")
|
|
||||||
start()
|
start()
|
||||||
println("== Inserting users must wait until Keycloak is started completely ==")
|
println("== Inserting users must wait until Keycloak is started completely ==")
|
||||||
execInContainer("sh", "/opt/jboss/create-keycloak-user.sh")
|
execInContainer("sh", "/opt/jboss/create-keycloak-user.sh")
|
||||||
|
@ -80,10 +79,10 @@ abstract class BaseContainerizedTest {
|
||||||
headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
|
headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
|
|
||||||
val map = LinkedMultiValueMap<Any, Any>()
|
val map = LinkedMultiValueMap<Any, Any>()
|
||||||
map.add("grant_type", "password")
|
|
||||||
map.add("client_id", clientId)
|
map.add("client_id", clientId)
|
||||||
map.add("username", username)
|
map.add("username", username)
|
||||||
map.add("password", password)
|
map.add("password", password)
|
||||||
|
map.add("grant_type", "password")
|
||||||
map.add("client_secret", "secret")
|
map.add("client_secret", "secret")
|
||||||
val responseString = restTemplate.postForObject("$keycloakHost/auth/realms/$realm/protocol/openid-connect/token",
|
val responseString = restTemplate.postForObject("$keycloakHost/auth/realms/$realm/protocol/openid-connect/token",
|
||||||
HttpEntity<Any>(map, headers), String::class.java)
|
HttpEntity<Any>(map, headers), String::class.java)
|
||||||
|
|
|
@ -2,13 +2,15 @@ package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.common.Json
|
import com.github.tomakehurst.wiremock.common.Json
|
||||||
import com.securityc4po.api.BaseDocumentationIntTest
|
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 com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
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.MongoTemplate
|
||||||
import org.springframework.data.mongodb.core.query.Query
|
import org.springframework.data.mongodb.core.query.Query
|
||||||
import org.springframework.restdocs.operation.preprocess.Preprocessors
|
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.payload.PayloadDocumentation
|
||||||
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
|
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
|
||||||
|
|
||||||
@AutoConfigureWireMock(port = 0)
|
@SuppressFBWarnings(
|
||||||
@SuppressFBWarnings(SIC_INNER_SHOULD_BE_STATIC)
|
SIC_INNER_SHOULD_BE_STATIC,
|
||||||
|
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
|
||||||
|
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||||
|
)
|
||||||
class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
|
class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -25,18 +30,21 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun init() {
|
fun init() {
|
||||||
cleanUp()
|
configureAdminToken()
|
||||||
persistBasicTestScenario()
|
persistBasicTestScenario()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun destroy() {
|
||||||
|
cleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class GetProjects {
|
inner class GetProjects {
|
||||||
@Test
|
@Test
|
||||||
fun getProjects() {
|
fun getProjects() {
|
||||||
/* Implement after the implementation of database */
|
webTestClient.get().uri("/projects")
|
||||||
|
.header("Authorization", "Bearer $tokenAdmin")
|
||||||
/*webTestClient.get().uri("/v1/projects")
|
|
||||||
.header("")
|
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk
|
.expectStatus().isOk
|
||||||
.expectHeader().doesNotExist("")
|
.expectHeader().doesNotExist("")
|
||||||
|
@ -49,14 +57,14 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
Preprocessors.prettyPrint()
|
Preprocessors.prettyPrint()
|
||||||
),
|
),
|
||||||
PayloadDocumentation.relaxedResponseFields(
|
PayloadDocumentation.relaxedResponseFields(
|
||||||
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested 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("[].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("[].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("[].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("[].tester").type(JsonFieldType.STRING).description("The user that is assigned as a tester in the project"),
|
||||||
PayloadDocumentation.fieldWithPath("[].logo").type(JsonFieldType.STRING).description("The sensors contained in the Project")
|
PayloadDocumentation.fieldWithPath("[].createdBy").type(JsonFieldType.STRING).description("The id of the user that created the project")
|
||||||
)
|
)
|
||||||
))*/
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val projectOne = Project(
|
val projectOne = Project(
|
||||||
|
@ -82,30 +90,36 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanUp() {
|
|
||||||
mongoTemplate.findAllAndRemove(Query(), Project::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun persistBasicTestScenario() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
val projectOne = Project(
|
val projectOne = Project(
|
||||||
id = "260aa538-0873-43fc-84de-3a09b008646d",
|
id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9",
|
||||||
client = "",
|
client = "E Corp",
|
||||||
title = "",
|
title = "Some Mock API (v1.0) Scanning",
|
||||||
createdAt = "",
|
createdAt = "2021-01-10T18:05:00Z",
|
||||||
tester = "",
|
tester = "Novatester",
|
||||||
createdBy = ""
|
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
||||||
)
|
)
|
||||||
val projectTwo = Project(
|
val projectTwo = Project(
|
||||||
id = "260aa538-0873-43fc-84de-3a09b008646d",
|
id = "61360a47-796b-4b3f-abf9-c46c668596c5",
|
||||||
client = "",
|
client = "Allsafe",
|
||||||
title = "",
|
title = "CashMyData (iOS)",
|
||||||
createdAt = "",
|
createdAt = "2021-01-10T18:05:00Z",
|
||||||
tester = "",
|
tester = "Elliot",
|
||||||
createdBy = ""
|
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
||||||
)
|
)
|
||||||
cleanUp()
|
// persist test data in database
|
||||||
mongoTemplate.save(ProjectEntity(projectOne))
|
mongoTemplate.save(ProjectEntity(projectOne))
|
||||||
mongoTemplate.save(ProjectEntity(projectTwo))
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,34 +2,25 @@ package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.common.Json
|
import com.github.tomakehurst.wiremock.common.Json
|
||||||
import com.securityc4po.api.BaseIntTest
|
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.SIC_INNER_SHOULD_BE_STATIC
|
||||||
import com.securityc4po.api.configuration.URF_UNREAD_FIELD
|
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.web.server.LocalServerPort
|
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.MongoTemplate
|
||||||
import org.springframework.data.mongodb.core.query.Query
|
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.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.util.ResourceUtils
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
@AutoConfigureWireMock(port = 0)
|
|
||||||
/*@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"keycloak.client.url=http://localhost:${'$'}{wiremock.server.port}"
|
|
||||||
]
|
|
||||||
)*/
|
|
||||||
@SuppressFBWarnings(
|
@SuppressFBWarnings(
|
||||||
SIC_INNER_SHOULD_BE_STATIC,
|
SIC_INNER_SHOULD_BE_STATIC,
|
||||||
URF_UNREAD_FIELD,
|
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
|
||||||
"Unread field will become used after database implementation"
|
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||||
)
|
)
|
||||||
class ProjectControllerIntTest : BaseIntTest() {
|
class ProjectControllerIntTest : BaseIntTest() {
|
||||||
|
|
||||||
|
@ -39,6 +30,7 @@ class ProjectControllerIntTest : BaseIntTest() {
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var mongoTemplate: MongoTemplate
|
lateinit var mongoTemplate: MongoTemplate
|
||||||
|
|
||||||
|
@Autowired
|
||||||
private lateinit var webTestClient: WebTestClient
|
private lateinit var webTestClient: WebTestClient
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -51,20 +43,24 @@ class ProjectControllerIntTest : BaseIntTest() {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun init() {
|
fun init() {
|
||||||
cleanUp()
|
|
||||||
configureAdminToken()
|
configureAdminToken()
|
||||||
persistBasicTestScenario()
|
persistBasicTestScenario()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun destroy() {
|
||||||
|
cleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class GetProjects {
|
inner class GetProjects {
|
||||||
@Test
|
@Test
|
||||||
fun `requesting projects successfully`() {
|
fun `requesting projects successfully`() {
|
||||||
webTestClient.get().uri("/v1/projects")
|
webTestClient.get().uri("/projects")
|
||||||
.header("Authorization", "Bearer $tokenAdmin")
|
.header("Authorization", "Bearer $tokenAdmin")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk
|
.expectStatus().isOk
|
||||||
.expectHeader().valueEquals("Application-Name", "security-c4po-api")
|
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||||
.expectBody().json(Json.write(getProjects()))
|
.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() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
val projectOne = Project(
|
val projectOne = Project(
|
||||||
|
@ -115,7 +105,7 @@ class ProjectControllerIntTest : BaseIntTest() {
|
||||||
tester = "Elliot",
|
tester = "Elliot",
|
||||||
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
||||||
)
|
)
|
||||||
cleanUp()
|
// persist test data in database
|
||||||
mongoTemplate.save(ProjectEntity(projectOne))
|
mongoTemplate.save(ProjectEntity(projectOne))
|
||||||
mongoTemplate.save(ProjectEntity(projectTwo))
|
mongoTemplate.save(ProjectEntity(projectTwo))
|
||||||
}
|
}
|
||||||
|
@ -123,4 +113,10 @@ class ProjectControllerIntTest : BaseIntTest() {
|
||||||
private fun configureAdminToken() {
|
private fun configureAdminToken() {
|
||||||
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
|
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanUp() {
|
||||||
|
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
|
||||||
|
|
||||||
|
tokenAdmin = "n/a"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
@ -365,26 +365,51 @@
|
||||||
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false,
|
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false,
|
||||||
"webAuthnPolicyPasswordlessAcceptableAaguids" : [ ],
|
"webAuthnPolicyPasswordlessAcceptableAaguids" : [ ],
|
||||||
"users" : [ {
|
"users" : [ {
|
||||||
"id" : "10e06d7a-8dd0-4ecd-8963-056b45079c4f",
|
"id" : "8f725a10-bdf5-4530-a185-4627fb092d78",
|
||||||
"createdTimestamp" : 1617897245335,
|
"createdTimestamp" : 1634988614562,
|
||||||
"username" : "ttt",
|
"username" : "test_admin",
|
||||||
"enabled" : true,
|
"enabled" : true,
|
||||||
"totp" : false,
|
"totp" : false,
|
||||||
"emailVerified" : false,
|
"emailVerified" : true,
|
||||||
"firstName" : "test",
|
"firstName" : "test",
|
||||||
"lastName" : "user",
|
"lastName" : "admin",
|
||||||
|
"email" : "testadmin@test.de",
|
||||||
"credentials" : [ {
|
"credentials" : [ {
|
||||||
"id" : "7026fefc-ae26-442b-acae-92f1f2d24eac",
|
"id" : "52ec7433-9b1c-46f1-ae5b-4f9851d1424c",
|
||||||
"type" : "password",
|
"type" : "password",
|
||||||
"createdDate" : 1617897287400,
|
"createdDate" : 1634988624873,
|
||||||
"secretData" : "{\"value\":\"mhW4yxOg+8bcyPF4yWsfPZnLGUp4oaqc9aNA+WBcpr9qXgs/Jw+rM2VlLEgeD/kXGItcScA8V20sVGrMWT94Yw==\",\"salt\":\"nkH510WAwjKZJqd/ZEkIHA==\"}",
|
"secretData" : "{\"value\":\"acmk/oJGV9GGtvrUjT1NJZPTizGvO9gvCj5tvzOSZKxW3OPwPYWfCE91OfmmLXOZcVddO8xdDufDLFliu52bxA==\",\"salt\":\"oHvWAC39azkfs7wJOwLdlQ==\"}",
|
||||||
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}"
|
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}"
|
||||||
} ],
|
} ],
|
||||||
"disableableCredentialTypes" : [ ],
|
"disableableCredentialTypes" : [ ],
|
||||||
"requiredActions" : [ ],
|
"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" : {
|
"clientRoles" : {
|
||||||
"c4po_local" : [ "user" ],
|
|
||||||
"account" : [ "view-profile", "manage-account" ]
|
"account" : [ "view-profile", "manage-account" ]
|
||||||
},
|
},
|
||||||
"notBefore" : 0,
|
"notBefore" : 0,
|
||||||
|
@ -1208,7 +1233,7 @@
|
||||||
"subType" : "anonymous",
|
"subType" : "anonymous",
|
||||||
"subComponents" : { },
|
"subComponents" : { },
|
||||||
"config" : {
|
"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",
|
"id" : "cc2d0cd7-3d3f-4b0a-ad95-7118f36bf188",
|
||||||
|
@ -1240,7 +1265,7 @@
|
||||||
"subType" : "authenticated",
|
"subType" : "authenticated",
|
||||||
"subComponents" : { },
|
"subComponents" : { },
|
||||||
"config" : {
|
"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",
|
"id" : "92230e65-7480-44c3-af2d-72ddee758cbc",
|
||||||
|
@ -1289,7 +1314,7 @@
|
||||||
"internationalizationEnabled" : false,
|
"internationalizationEnabled" : false,
|
||||||
"supportedLocales" : [ ],
|
"supportedLocales" : [ ],
|
||||||
"authenticationFlows" : [ {
|
"authenticationFlows" : [ {
|
||||||
"id" : "fa5fc78f-19a9-4737-868b-618163f28c79",
|
"id" : "9f2b9c09-a331-4126-9f89-5d459e911053",
|
||||||
"alias" : "Account verification options",
|
"alias" : "Account verification options",
|
||||||
"description" : "Method with which to verity the existing account",
|
"description" : "Method with which to verity the existing account",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1309,7 +1334,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "01735b0f-139f-46e5-bb63-f797a27efa77",
|
"id" : "6802ac9b-69cf-4838-99d4-747dd9de3f32",
|
||||||
"alias" : "Authentication Options",
|
"alias" : "Authentication Options",
|
||||||
"description" : "Authentication options.",
|
"description" : "Authentication options.",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1335,7 +1360,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "a7666cf0-626c-48c4-9e71-e408832de725",
|
"id" : "c1e6806d-ff33-46bc-be0f-70eb6924fc92",
|
||||||
"alias" : "Browser - Conditional OTP",
|
"alias" : "Browser - Conditional OTP",
|
||||||
"description" : "Flow to determine if the OTP is required for the authentication",
|
"description" : "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1355,7 +1380,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "1dfabb7a-efdd-4964-bba5-389cad79b654",
|
"id" : "8b0f8be2-5933-4fc1-9546-484ecdf0766b",
|
||||||
"alias" : "Direct Grant - Conditional OTP",
|
"alias" : "Direct Grant - Conditional OTP",
|
||||||
"description" : "Flow to determine if the OTP is required for the authentication",
|
"description" : "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1375,7 +1400,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "c3b2bf2b-3da8-430d-a9b7-8793c3dc30a3",
|
"id" : "d1b5d09c-36eb-40e2-8c1f-9b414e63b44e",
|
||||||
"alias" : "First broker login - Conditional OTP",
|
"alias" : "First broker login - Conditional OTP",
|
||||||
"description" : "Flow to determine if the OTP is required for the authentication",
|
"description" : "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1395,7 +1420,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "44343bdf-8592-4242-835f-e349943a110b",
|
"id" : "ffeb5151-3357-4cd6-aa17-860c60de0cb3",
|
||||||
"alias" : "Handle Existing Account",
|
"alias" : "Handle Existing Account",
|
||||||
"description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1415,7 +1440,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "e72b8fcb-cd8b-4e7a-a057-3446b806b538",
|
"id" : "816cc72c-df79-49ee-b7a4-291f496e145b",
|
||||||
"alias" : "Reset - Conditional OTP",
|
"alias" : "Reset - Conditional OTP",
|
||||||
"description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1435,7 +1460,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "2416145b-4d20-493c-bdf7-419898c002ee",
|
"id" : "5a782d53-4c32-437e-803a-a9e893b0f5eb",
|
||||||
"alias" : "User creation or linking",
|
"alias" : "User creation or linking",
|
||||||
"description" : "Flow for the existing/non-existing user alternatives",
|
"description" : "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1456,7 +1481,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "b7ff8aad-2daa-4736-8815-f3e8f0df391e",
|
"id" : "3faef6d7-2cfd-4190-a8d1-db1b8e953304",
|
||||||
"alias" : "Verify Existing Account by Re-authentication",
|
"alias" : "Verify Existing Account by Re-authentication",
|
||||||
"description" : "Reauthentication of existing account",
|
"description" : "Reauthentication of existing account",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1476,7 +1501,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "8339d3ba-2d0a-4d23-bbfa-a78e4973d3c9",
|
"id" : "5a57eb55-59f8-4d28-9003-ec350b4053cf",
|
||||||
"alias" : "browser",
|
"alias" : "browser",
|
||||||
"description" : "browser based authentication",
|
"description" : "browser based authentication",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1508,7 +1533,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "5ece002a-4e62-4d0d-8705-4b116164b424",
|
"id" : "1cf8aebb-27ae-4df5-96d4-82d6a0ba3bfd",
|
||||||
"alias" : "clients",
|
"alias" : "clients",
|
||||||
"description" : "Base authentication for clients",
|
"description" : "Base authentication for clients",
|
||||||
"providerId" : "client-flow",
|
"providerId" : "client-flow",
|
||||||
|
@ -1540,7 +1565,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "bd27b0dc-bc87-40b7-a626-491b9955668d",
|
"id" : "1ec7e0bf-7bbe-4841-a21d-bfbc6cb492ae",
|
||||||
"alias" : "direct grant",
|
"alias" : "direct grant",
|
||||||
"description" : "OpenID Connect Resource Owner Grant",
|
"description" : "OpenID Connect Resource Owner Grant",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1566,7 +1591,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "2db79d60-7c9d-4516-80f0-0c5d60349899",
|
"id" : "d633bda1-5c4e-43c4-9a0e-776502549b57",
|
||||||
"alias" : "docker auth",
|
"alias" : "docker auth",
|
||||||
"description" : "Used by Docker clients to authenticate against the IDP",
|
"description" : "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1580,7 +1605,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "25a92fbe-7d4d-46bc-a751-29ef844290a3",
|
"id" : "ac608b5f-4849-4830-a928-b51f996f77a8",
|
||||||
"alias" : "first broker login",
|
"alias" : "first broker login",
|
||||||
"description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1601,7 +1626,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "26f6a5db-9be8-446c-82d0-6f4e29b5f08d",
|
"id" : "b0731e4d-5935-4c29-8a4d-e7124e9eb164",
|
||||||
"alias" : "forms",
|
"alias" : "forms",
|
||||||
"description" : "Username, password, otp and other auth forms.",
|
"description" : "Username, password, otp and other auth forms.",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1621,7 +1646,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "05a94701-ad98-4bbc-a162-746a107afba5",
|
"id" : "b09ede5d-b7cd-4e9a-bb8d-e7caa880bc6d",
|
||||||
"alias" : "http challenge",
|
"alias" : "http challenge",
|
||||||
"description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1641,7 +1666,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "75347884-d4cb-4eba-9b89-63566d509b92",
|
"id" : "96479ab9-2db1-47c9-ba6b-c9832e54937d",
|
||||||
"alias" : "registration",
|
"alias" : "registration",
|
||||||
"description" : "registration flow",
|
"description" : "registration flow",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1656,7 +1681,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "74e3a2d3-ecda-400d-8bff-0926dc272e4b",
|
"id" : "ae46ccfd-1bd2-4717-8380-de1040d7fe43",
|
||||||
"alias" : "registration form",
|
"alias" : "registration form",
|
||||||
"description" : "registration form",
|
"description" : "registration form",
|
||||||
"providerId" : "form-flow",
|
"providerId" : "form-flow",
|
||||||
|
@ -1688,7 +1713,7 @@
|
||||||
"autheticatorFlow" : false
|
"autheticatorFlow" : false
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "6eae8652-baf7-4a7d-80a4-1711906caec7",
|
"id" : "99d2284f-52e8-4c09-a500-6a7d1c5b108e",
|
||||||
"alias" : "reset credentials",
|
"alias" : "reset credentials",
|
||||||
"description" : "Reset credentials for a user if they forgot their password or something",
|
"description" : "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1720,7 +1745,7 @@
|
||||||
"autheticatorFlow" : true
|
"autheticatorFlow" : true
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "6135710b-b019-4117-ba32-578d3d496b2a",
|
"id" : "db4d353d-4b8d-4241-a09b-d4246590c82a",
|
||||||
"alias" : "saml ecp",
|
"alias" : "saml ecp",
|
||||||
"description" : "SAML ECP Profile Authentication Flow",
|
"description" : "SAML ECP Profile Authentication Flow",
|
||||||
"providerId" : "basic-flow",
|
"providerId" : "basic-flow",
|
||||||
|
@ -1735,13 +1760,13 @@
|
||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"authenticatorConfig" : [ {
|
"authenticatorConfig" : [ {
|
||||||
"id" : "3d3735a0-1362-4f0d-9306-bfc727da1b5b",
|
"id" : "1a64855a-6fe8-40df-90eb-bd936aed6e76",
|
||||||
"alias" : "create unique user config",
|
"alias" : "create unique user config",
|
||||||
"config" : {
|
"config" : {
|
||||||
"require.password.update.after.registration" : "false"
|
"require.password.update.after.registration" : "false"
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
"id" : "c1f4a15f-8234-4f0f-affa-baf610b001e1",
|
"id" : "e10161fb-9f1c-4c5c-ba84-5bcbf0dd358d",
|
||||||
"alias" : "review profile config",
|
"alias" : "review profile config",
|
||||||
"config" : {
|
"config" : {
|
||||||
"update.profile.on.first.login" : "missing"
|
"update.profile.on.first.login" : "missing"
|
||||||
|
|
|
@ -4,11 +4,12 @@ sleep 20
|
||||||
./kcadm.sh config credentials --server http://localhost:8888/auth --realm master --user admin --password admin
|
./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 \
|
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 firstName=test \
|
||||||
-s lastName=admin \
|
-s lastName=admin \
|
||||||
-s attributes.lang="de-DE" \
|
-s attributes.lang="de-DE" \
|
||||||
-s attributes.datenumberformat="en-US" \
|
-s attributes.datenumberformat="en-US" \
|
||||||
|
-s enabled=true \
|
||||||
-o --fields id | jq '.id' | tr -d '"')
|
-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 update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n
|
||||||
|
@ -16,13 +17,14 @@ 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
|
./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 \
|
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 firstName=test \
|
||||||
-s lastName=user \
|
-s lastName=user \
|
||||||
-s attributes.lang="de-DE" \
|
-s attributes.lang="de-DE" \
|
||||||
-s attributes.datenumberformat="en-US" \
|
-s attributes.datenumberformat="en-US" \
|
||||||
|
-s enabled=true \
|
||||||
-o --fields id | jq '.id' | tr -d '"')
|
-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 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 --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
|
./kcadm.sh add-roles -r c4po_realm_test --uusername test_user --cclientid realm-management --rolename create-client --rolename view-users
|
||||||
|
|
|
@ -7,12 +7,6 @@ services:
|
||||||
container_name: c4po-api
|
container_name: c4po-api
|
||||||
environment:
|
environment:
|
||||||
- SPRING_PROFILES_ACTIVE=COMPOSE
|
- SPRING_PROFILES_ACTIVE=COMPOSE
|
||||||
depends_on:
|
|
||||||
- c4po-db
|
|
||||||
- c4po-keycloak
|
|
||||||
links:
|
|
||||||
- c4po-db
|
|
||||||
- c4po-keycloak
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|
|
@ -5,8 +5,6 @@ services:
|
||||||
build: '../../security-c4po-angular'
|
build: '../../security-c4po-angular'
|
||||||
image: security-c4po-angular:latest
|
image: security-c4po-angular:latest
|
||||||
container_name: c4po-angular
|
container_name: c4po-angular
|
||||||
depends_on:
|
|
||||||
- c4po-keycloak
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|
|
@ -3,8 +3,6 @@ version: '3.1'
|
||||||
services:
|
services:
|
||||||
c4po-keycloak:
|
c4po-keycloak:
|
||||||
container_name: c4po-keycloak
|
container_name: c4po-keycloak
|
||||||
depends_on:
|
|
||||||
- c4po-keycloak-postgres
|
|
||||||
image: jboss/keycloak:11.0.3
|
image: jboss/keycloak:11.0.3
|
||||||
volumes:
|
volumes:
|
||||||
- ../cfg/c4po_realm_export.json:/tmp/c4po_realm_export.json
|
- ../cfg/c4po_realm_export.json:/tmp/c4po_realm_export.json
|
||||||
|
|
Loading…
Reference in New Issue