Ajout des comptes à l'interface Angular.

This commit is contained in:
FyloZ 2020-10-17 15:35:12 -04:00
parent 3bafc3d9ef
commit df36da3536
37 changed files with 1193 additions and 243 deletions

View File

@ -25,7 +25,8 @@
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
{ "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,10 @@
"@angular/platform-browser": "~9.0.5",
"@angular/platform-browser-dynamic": "~9.0.5",
"@angular/router": "~9.0.5",
"@mdi/angular-material": "^5.7.55",
"bootstrap": "^4.5.2",
"copy-webpack-plugin": "^6.2.1",
"ngx-cookie": "^5.0.0",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"

View File

@ -1,6 +1,6 @@
{
"/api": {
"target": "http://localhost:9090",
"target": "http://localhost:9090/api",
"secure": false
}
}

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [{ path: 'color', loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule) }];
const routes: Routes = [{ path: 'color', loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule) }, { path: 'account', loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule) }];
@NgModule({
imports: [RouterModule.forRoot(routes)],

View File

@ -1,10 +1,13 @@
import { BrowserModule } from '@angular/platform-browser';
import {BrowserModule, DomSanitizer} from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SharedModule } from './modules/shared/shared.module';
import {HttpClientModule} from "@angular/common/http";
import {CookieModule} from "ngx-cookie";
import {MatIconRegistry} from "@angular/material/icon";
@NgModule({
declarations: [
@ -14,9 +17,17 @@ import { SharedModule } from './modules/shared/shared.module';
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
SharedModule
SharedModule,
HttpClientModule,
CookieModule.forRoot()
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
constructor(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
matIconRegistry.addSvgIconSet(
domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')
)
}
}

View File

@ -0,0 +1,14 @@
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {LoginComponent} from './pages/login/login.component';
import {LogoutComponent} from "./pages/logout/logout.component";
const routes: Routes = [{path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', redirectTo: 'login'}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccountsRoutingModule {
}

View File

@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AccountsRoutingModule} from './accounts-routing.module';
import {LoginComponent} from './pages/login/login.component';
import {SharedModule} from "../shared/shared.module";
import { LogoutComponent } from './pages/logout/logout.component';
@NgModule({
declarations: [LoginComponent, LogoutComponent],
imports: [
CommonModule,
SharedModule,
AccountsRoutingModule
]
})
export class AccountsModule {
}

View File

@ -0,0 +1,40 @@
<div class="dark-background"></div>
<mat-card class="centered">
<mat-card-header>
<mat-card-title>Connexion au système</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="invalidCredentials" class="alert alert-danger">
<p>Les identifiants entrés sont invalides.</p>
</div>
<form [formGroup]="form">
<mat-form-field>
<mat-label>Numéro d'employé</mat-label>
<input matInput [formControl]="idFormControl" type="text"/>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="idFormControl.invalid">
<span *ngIf="idFormControl.errors.required">Un numéro d'employé est requis</span>
<span *ngIf="idFormControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Mot de passe</mat-label>
<input matInput [formControl]="passwordFormControl" type="password"/>
<mat-icon matSuffix>lock</mat-icon>
<mat-error *ngIf="passwordFormControl.invalid">
<span *ngIf="passwordFormControl.errors.required">Un mot de passe est requis</span>
</mat-error>
</mat-form-field>
</form>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<button
mat-button
type="submit"
color="accent"
[disabled]="form.invalid"
(click)="submit()">
Connexion
</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,13 @@
mat-card
width: 25rem
&.centered
margin: 50vh auto auto
position: relative
transform: translateY(-70%)
.alert p
margin: 0
mat-form-field
width: 100%

View File

@ -0,0 +1,43 @@
import {Component, OnInit} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {AccountService} from "../../services/account.service";
import {take} from "rxjs/operators";
import {Router} from "@angular/router";
@Component({
selector: 'cre-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.sass']
})
export class LoginComponent implements OnInit {
form: FormGroup
idFormControl: FormControl
passwordFormControl: FormControl
invalidCredentials = false
constructor(
private formBuilder: FormBuilder,
private accountService: AccountService,
private router: Router
) {
}
ngOnInit(): void {
this.idFormControl = this.formBuilder.control(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))
this.passwordFormControl = this.formBuilder.control(null, Validators.required)
this.form = this.formBuilder.group({
id: this.idFormControl,
password: this.passwordFormControl
})
}
submit() {
this.accountService.login(
this.idFormControl.value,
this.passwordFormControl.value,
() => this.router.navigate(["/color"]),
err => this.invalidCredentials = err.status === 401
)
}
}

View File

@ -0,0 +1,24 @@
import {Component, OnInit} from '@angular/core';
import {AccountService} from "../../services/account.service";
import {Router} from "@angular/router";
@Component({
selector: 'cre-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.sass']
})
export class LogoutComponent implements OnInit {
constructor(
private accountService: AccountService,
private router: Router
) {
}
ngOnInit(): void {
this.accountService.logout(() => {
this.router.navigate(['/account/login'])
})
}
}

View File

@ -0,0 +1,70 @@
import {Injectable, OnDestroy} from '@angular/core';
import {Subject} from "rxjs";
import {take, takeUntil} from "rxjs/operators";
import {AppState} from "../../shared/app-state";
import {HttpClient, HttpResponse} from "@angular/common/http";
import {environment} from "../../../../environments/environment";
import {ApiService} from "../../shared/service/api.service";
import {Employee} from "../../shared/model/employee";
@Injectable({
providedIn: 'root'
})
export class AccountService implements OnDestroy {
private $destroy = new Subject<boolean>()
constructor(
private http: HttpClient,
private api: ApiService,
private appState: AppState
) {
}
ngOnDestroy(): void {
this.$destroy.next(true)
this.$destroy.complete()
}
login(id: number, password: string, success: () => void, error: (err) => void) {
const loginForm = {id, password}
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
withCredentials: true,
observe: 'response' as 'body'
})
.pipe(
take(1),
takeUntil(this.$destroy)
)
.subscribe({
next: (response: HttpResponse<any>) => {
this.appState.authenticationExpiration = parseInt(response.headers.get("X-Authentication-Expiration"))
this.appState.isAuthenticated = true
this.setLoggedInEmployeeFromApi()
success()
},
error: err => error(err)
})
}
logout(success: () => void) {
this.appState.isAuthenticated = false
this.appState.authenticationExpiration = -1
this.appState.authenticatedEmployee = null
success()
}
private setLoggedInEmployeeFromApi() {
this.api.get<Employee>("/employee/current", true)
.pipe(
take(1),
takeUntil(this.$destroy)
)
.subscribe({
next: employee => this.appState.authenticatedEmployee = employee,
error: err => {
console.error("Could not get the logged in employee from the API: ")
console.error(err)
}
})
}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'cre-colors',
@ -7,9 +7,9 @@ import { Component, OnInit } from '@angular/core';
})
export class ColorsComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,51 @@
import {Injectable} from "@angular/core";
import {Employee} from "./model/employee";
import {Subject} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class AppState {
private readonly KEY_AUTHENTICATED = "authenticated"
private readonly KEY_AUTHENTICATION_EXPIRATION = "authentication-expiration"
private readonly KEY_LOGGED_IN_EMPLOYEE = "logged-in-employee"
authenticatedUser$ = new Subject<{ authenticated: boolean, authenticatedUser: Employee }>()
get isAuthenticated(): boolean {
return sessionStorage.getItem(this.KEY_AUTHENTICATED) === "true"
}
set isAuthenticated(value: boolean) {
sessionStorage.setItem(this.KEY_AUTHENTICATED, value.toString())
this.authenticatedUser$.next({
authenticated: value,
authenticatedUser: this.authenticatedEmployee
})
}
get authenticationExpiration(): number {
return parseInt(sessionStorage.getItem(this.KEY_AUTHENTICATION_EXPIRATION))
}
set authenticationExpiration(value: number) {
sessionStorage.setItem(this.KEY_AUTHENTICATION_EXPIRATION, value.toString())
}
get authenticatedEmployee(): Employee {
const employeeString = sessionStorage.getItem(this.KEY_LOGGED_IN_EMPLOYEE)
return employeeString ? JSON.parse(employeeString) : null
}
set authenticatedEmployee(value: Employee) {
if (value === null) {
sessionStorage.removeItem(this.KEY_LOGGED_IN_EMPLOYEE)
return
}
sessionStorage.setItem(this.KEY_LOGGED_IN_EMPLOYEE, JSON.stringify(value))
this.authenticatedUser$.next({
authenticated: this.isAuthenticated,
authenticatedUser: value
})
}
}

View File

@ -0,0 +1,7 @@
<div *ngIf="authenticated && employee" class="employee-info-container d-flex flex-column">
<labeled-icon icon="account" label="{{employee.firstName}} {{employee.lastName}}"></labeled-icon>
<div class="d-flex flex-row">
<labeled-icon icon="pound" [label]="employee.id.toString()"></labeled-icon>
<labeled-icon *ngIf="employeeInGroup" class="employee-info-group" icon="account-multiple" [label]="employee.group.name"></labeled-icon>
</div>
</div>

View File

@ -0,0 +1,12 @@
@import "../../../../../custom-theme"
p, labeled-icon
margin: 0
color: $light-primary-text
.employee-info-container
margin-top: .85rem
margin-right: 1rem
.employee-info-group
margin-left: 0.7rem

View File

@ -0,0 +1,45 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {AppState} from "../../app-state";
import {Employee} from "../../model/employee";
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
@Component({
selector: 'cre-employee-info',
templateUrl: './employee-info.component.html',
styleUrls: ['./employee-info.component.sass']
})
export class EmployeeInfoComponent implements OnInit, OnDestroy {
authenticated = false
employee: Employee = null
employeeInGroup = false
private destroy$ = new Subject<boolean>()
constructor(
public appState: AppState
) {
}
ngOnInit(): void {
this.authenticationState(this.appState.isAuthenticated, this.appState.authenticatedEmployee)
this.appState.authenticatedUser$
.pipe(takeUntil(this.destroy$))
.subscribe({
next: authentication => this.authenticationState(authentication.authenticated, authentication.authenticatedUser)
})
}
ngOnDestroy(): void {
this.destroy$.next(true)
this.destroy$.complete()
}
private authenticationState(authenticated: boolean, employee: Employee) {
this.authenticated = authenticated
this.employee = employee
if (this.employee != null) {
this.employeeInGroup = this.employee.group != null
}
}
}

View File

@ -2,15 +2,19 @@
<nav class="d-flex flex-column flex-grow-1">
<div class="spacer"></div>
<div mat-tab-nav-bar backgroundColor="primary">
<a
*ngFor="let link of links"
mat-tab-link
(click)="activeLink = link.route"
[active]="activeLink == link.route">
{{ link.title }}
</a>
<ng-container *ngFor="let link of links">
<a
*ngIf="link.enabled"
(load)="test(link)"
mat-tab-link
(click)="activeLink = link.route"
[active]="activeLink == link.route">
{{ link.title }}
</a>
</ng-container>
</div>
</nav>
<cre-employee-info></cre-employee-info>
<img src="assets/logo.png" alt="Logo" class="flex-grow-0"/>
</header>

View File

@ -1,24 +1,41 @@
import {Component} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {Router} from "@angular/router";
import {AppState} from "../../app-state";
@Component({
selector: 'cre-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.sass']
})
export class HeaderComponent {
export class HeaderComponent implements OnInit {
links = [
{route: 'color', title: 'Couleurs'},
{route: 'inventory', title: 'Inventaire'},
{route: 'account', title: 'Connexion'}
{route: 'color', title: 'Couleurs', enabled: true},
{route: 'inventory', title: 'Inventaire', enabled: true},
{route: 'account/login', title: 'Connexion', enabled: true},
{route: 'account/logout', title: 'Déconnexion', enabled: false}
];
_activeLink = this.links[0].route;
constructor(
private router: Router
private router: Router,
private appState: AppState
) {
}
ngOnInit(): void {
const loginLink = this.links[2]
const logoutLink = this.links[3]
loginLink.enabled = !this.appState.isAuthenticated
logoutLink.enabled = this.appState.isAuthenticated
this.appState.authenticatedUser$.subscribe({
next: authentication => {
loginLink.enabled = !authentication.authenticated
logoutLink.enabled = authentication.authenticated
}
})
}
set activeLink(link: string) {
this._activeLink = link;
this.router.navigate([link]);
@ -27,4 +44,8 @@ export class HeaderComponent {
get activeLink() {
return this._activeLink;
}
test(link: any) {
console.log(link.condition ? link.condition() : true)
}
}

View File

@ -0,0 +1,4 @@
<div class="d-flex flex-row">
<mat-icon [svgIcon]="icon"></mat-icon>
<p>{{label}}</p>
</div>

View File

@ -0,0 +1,6 @@
mat-icon
width: 1em
p
margin: 4px 0 0 .3em
font-size: .8em

View File

@ -0,0 +1,11 @@
import {Component, Input} from '@angular/core';
@Component({
selector: 'labeled-icon',
templateUrl: './labeled-icon.component.html',
styleUrls: ['./labeled-icon.component.sass']
})
export class LabeledIconComponent {
@Input() icon: string
@Input() label: string
}

View File

@ -0,0 +1,37 @@
export class Employee {
constructor(
public id: number,
public firstName: string,
public lastName: string,
public permissions: EmployeePermission[],
public excludedPermissions: EmployeePermission[],
public group?: EmployeeGroup,
public lastLoginTime?: Date
) {
}
}
export class EmployeeGroup {
constructor(
public id: number,
public name: string,
public permissions: EmployeePermission[]
) {
}
}
export enum EmployeePermission {
VIEW_EMPLOYEE,
VIEW_EMPLOYEE_GROUP,
VIEW,
EDIT_EMPLOYEE,
EDIT_EMPLOYEE_GROUP,
EDIT,
REMOVE_EMPLOYEE,
REMOVE_EMPLOYEE_GROUP,
REMOVE,
ADMIN
}

View File

@ -0,0 +1,61 @@
import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {environment} from "../../../../environments/environment";
import {AppState} from "../app-state";
import {Router} from "@angular/router";
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(
private http: HttpClient,
private appState: AppState,
private router: Router
) {
}
get<T>(url: string, needAuthentication = false, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.get<string>(environment.apiUrl + url, options)
}
}
post<T>(url: string, body: any, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.post<T>(environment.apiUrl + url, body, options)
}
}
put<T>(url: string, body: any, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.put<T>(environment.apiUrl + url, body, options)
}
}
delete<T>(url: string, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.delete<T>(environment.apiUrl + url, options)
}
}
private checkAuthenticated(needAuthentication: boolean, httpOptions: any): boolean {
if (needAuthentication) {
if (!this.appState.isAuthenticated || Date.now() > this.appState.authenticationExpiration) {
this.navigateToLogin()
return false
}
httpOptions.withCredentials = true
}
return true
}
private navigateToLogin() {
this.router.navigate(['/account/login'])
}
}

View File

@ -2,17 +2,34 @@ import { NgModule } from '@angular/core';
import { HeaderComponent } from './components/header/header.component';
import {MatTabsModule} from "@angular/material/tabs";
import {CommonModule} from "@angular/common";
import {MatCardModule} from "@angular/material/card";
import {MatButtonModule} from "@angular/material/button";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {ReactiveFormsModule} from "@angular/forms";
import {RouterModule} from "@angular/router";
import { EmployeeInfoComponent } from './components/employee-info/employee-info.component';
import { LabeledIconComponent } from './components/labeled-icon/labeled-icon.component';
@NgModule({
declarations: [HeaderComponent],
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent],
exports: [
HeaderComponent
HeaderComponent,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
ReactiveFormsModule,
RouterModule
],
imports: [
MatTabsModule,
CommonModule
CommonModule,
MatIconModule
]
})
export class SharedModule { }

View File

@ -15,7 +15,7 @@ $custom-typography: mat-typography-config(
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$color-recipes-explorer-frontend-primary: mat-palette((
$theme-primary: mat-palette((
50 : #e0e0e0,
100 : #b3b3b3,
200 : #808080,
@ -46,7 +46,7 @@ $color-recipes-explorer-frontend-primary: mat-palette((
A400 : #ffffff,
A700 : #ffffff,
)));
$color-recipes-explorer-frontend-accent: mat-palette((
$theme-accent: mat-palette((
50 : #edf9e0,
100 : #d1f0b3,
200 : #b3e680,
@ -80,16 +80,19 @@ $color-recipes-explorer-frontend-accent: mat-palette((
));
// The warn palette is optional (defaults to red).
$color-recipes-explorer-frontend-warn: mat-palette($mat-red);
$theme-warn: mat-palette($mat-red);
// Create the theme object (a Sass map containing all of the palettes).
$color-recipes-explorer-frontend-theme: mat-light-theme($color-recipes-explorer-frontend-primary, $color-recipes-explorer-frontend-accent, $color-recipes-explorer-frontend-warn);
$color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-accent, $theme-warn);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($color-recipes-explorer-frontend-theme);
$color-primary: map-get($theme-primary, 500);
$color-accent: map-get($theme-accent, 500);
html, body {
height: 100%;

View File

@ -1,3 +1,4 @@
export const environment = {
production: true
production: true,
apiUrl: '/api'
};

View File

@ -3,7 +3,8 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
apiUrl: 'http://localhost:9090/api'
};
/*

View File

@ -1 +1,31 @@
/* You can add global styles to this file, and also import other style files */
@import "custom-theme"
mat-card
padding: 0 !important
mat-card-header
background-color: $color-primary
color: $light-primary-text
padding: 16px 16px 0 16px
border-radius: 4px 4px 0 0
mat-card-content
margin-top: 16px
padding: 0 16px
mat-card-actions
display: flex !important
padding: 0 24px 16px 24px !important
flex-direction: row
button
text-transform: uppercase
letter-spacing: 1.25px
.dark-background
position: fixed
width: 100%
height: 100%
top: 0
background-color: black
opacity: 0.05

View File

@ -38,9 +38,13 @@ import org.springframework.util.Assert
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.util.WebUtils
import java.util.*
import javax.annotation.PostConstruct
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@ -75,19 +79,42 @@ class WebSecurityConfig(
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
return UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().applyPermitDefaultValues())
registerCorsConfiguration("/**", CorsConfiguration().apply {
allowedOrigins = listOf("http://localhost:4200") // Angular development server
allowedMethods = listOf(
HttpMethod.GET.name,
HttpMethod.POST.name,
HttpMethod.PUT.name,
HttpMethod.DELETE.name,
HttpMethod.OPTIONS.name,
HttpMethod.HEAD.name
)
allowCredentials = true
}.applyPermitDefaultValues())
}
}
@PostConstruct
fun createRootUser() {
val rootUserCredentials = securityConfigurationProperties.root
Assert.notNull(rootUserCredentials, "No root user has been defined.")
Assert.notNull(rootUserCredentials!!.id, "The root user has no identifier defined.")
Assert.notNull(rootUserCredentials.password, "The root user has no password defined.")
if (!employeeService.existsById(rootUserCredentials.id!!)) {
employeeService.save(Employee(rootUserCredentials.id!!, "Root", "Employee", passwordEncoder().encode(rootUserCredentials.password!!), true, permissions = mutableListOf(EmployeePermission.ADMIN)))
fun createSystemUsers() {
fun createUser(credentials: SecurityConfigurationProperties.SystemUserCredentials?, firstName: String, lastName: String, permissions: List<EmployeePermission>) {
Assert.notNull(credentials, "No root user has been defined.")
credentials!!
Assert.notNull(credentials.id, "The root user has no identifier defined.")
Assert.notNull(credentials.password, "The root user has no password defined.")
if (!employeeService.existsById(credentials.id!!)) {
employeeService.save(Employee(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableList()
))
}
}
createUser(securityConfigurationProperties.root, "Root", "User", listOf(EmployeePermission.ADMIN))
createUser(securityConfigurationProperties.common, "Common", "User", listOf(EmployeePermission.VIEW))
}
override fun configure(http: HttpSecurity) {
@ -104,6 +131,7 @@ class WebSecurityConfig(
}
http
// .addFilterBefore(CorsFilter())
.cors()
.and()
.csrf().disable()
@ -124,6 +152,20 @@ class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}
class CorsFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
response as HttpServletResponse
response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200")
response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS")
response.setHeader("Access-Control-Allow-Headers", "*")
response.setHeader("Access-Control-Allow-Credentials", true.toString())
response.setHeader("Access-Control-Max-Age", 180.toString())
chain.doFilter(request, response)
}
}
class JwtAuthenticationFilter(
val authManager: AuthenticationManager,
val employeeService: EmployeeService,
@ -145,12 +187,17 @@ class JwtAuthenticationFilter(
Assert.notNull(jwtDuration, "No JWT duration has been defined.")
val employeeId = (authResult.principal as User).username
employeeService.updateLastLoginTime(employeeId.toLong())
val expirationMs = System.currentTimeMillis() + jwtDuration!!
val expirationDate = Date(expirationMs)
val token = Jwts.builder()
.setSubject(employeeId)
.setExpiration(Date(System.currentTimeMillis() + jwtDuration!!))
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
response.addHeader("Set-Cookie", "Authorization=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict")
response.addHeader("Authorization", "Bearer $token")
response.addHeader("X-Authentication-Expiration", "$expirationMs")
}
}
@ -160,30 +207,32 @@ class JwtAuthorizationFilter(
authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
val header = request.getHeader("Authorization")
if (header != null && header.startsWith("Bearer")) {
val authenticationToken = getAuthentication(request)
SecurityContextHolder.getContext().authentication = authenticationToken
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
val authorizationValue = if (authorizationCookie != null) authorizationCookie.value else request.getHeader("Authorization")
val authenticationToken = if (authorizationValue != null && authorizationValue.startsWith("Bearer")) {
getAuthentication(authorizationValue)
} else {
// Load common user if there is no valid authentication data
getAuthenticationToken(securityConfigurationProperties.common!!.id!!.toString())
}
SecurityContextHolder.getContext().authentication = authenticationToken
chain.doFilter(request, response)
}
private fun getAuthentication(request: HttpServletRequest): UsernamePasswordAuthenticationToken? {
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
val jwtSecret = securityConfigurationProperties.jwtSecret
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
val token = request.getHeader("Authorization")
if (token != null) {
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
return if (employeeId != null) {
val employeeDetails = userDetailsService.loadUserByUsername(employeeId)
UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
} else null
}
return null
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
return if (employeeId != null) getAuthenticationToken(employeeId) else null
}
private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken {
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), true)
return UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
}
}
@ -191,9 +240,10 @@ class JwtAuthorizationFilter(
class SecurityConfigurationProperties {
var jwtSecret: String? = null
var jwtDuration: Long? = null
var root: RootUserCredentials? = null
var root: SystemUserCredentials? = null
var common: SystemUserCredentials? = null
class RootUserCredentials(var id: Long? = null, var password: String? = null)
class SystemUserCredentials(var id: Long? = null, var password: String? = null)
}
private enum class ControllerAuthorizations(

View File

@ -32,7 +32,7 @@ class Employee(
val password: String = "",
@JsonIgnore
val isRoot: Boolean = false,
val isSystemUser: Boolean = false,
@field:ManyToOne
@Fetch(FetchMode.SELECT)

View File

@ -1,5 +1,6 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services
import dev.fyloz.trial.colorrecipesexplorer.core.configuration.SecurityConfigurationProperties
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.*
import dev.fyloz.trial.colorrecipesexplorer.core.model.*
import dev.fyloz.trial.colorrecipesexplorer.dao.EmployeeGroupRepository
@ -22,7 +23,7 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
}
override fun getAll(): Collection<Employee> {
return super.getAll().filter { !it.isRoot }
return super.getAll().filter { !it.isSystemUser }
}
override fun getById(id: Long): Employee {
@ -30,9 +31,9 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
}
/** Gets the employee with the given [id]. */
fun getById(id: Long, ignoreRoot: Boolean): Employee {
fun getById(id: Long, ignoreSystemUsers: Boolean): Employee {
return super.getById(id).apply {
if (ignoreRoot && isRoot) throw EntityNotFoundRestException(id)
if (ignoreSystemUsers && isSystemUser) throw EntityNotFoundRestException(id)
}
}
@ -60,8 +61,8 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
}
/** Updates de given [entity]. **/
fun update(entity: Employee, ignoreRoot: Boolean): Employee {
val persistedEmployee = getById(entity.id, ignoreRoot)
fun update(entity: Employee, ignoreSystemUsers: Boolean): Employee {
val persistedEmployee = getById(entity.id, ignoreSystemUsers)
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw EntityAlreadyExistsRestException("${entity.firstName} ${entity.lastName}")
@ -73,7 +74,7 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
if (firstName.isNotBlank()) firstName else persistedEmployee.firstName,
if (lastName.isNotBlank()) lastName else persistedEmployee.lastName,
persistedEmployee.password,
if (ignoreRoot) false else persistedEmployee.isRoot,
if (ignoreSystemUsers) false else persistedEmployee.isSystemUser,
persistedEmployee.group,
if (permissions.isNotEmpty()) permissions else persistedEmployee.permissions,
if (excludedPermissions.isNotEmpty()) excludedPermissions else persistedEmployee.excludedPermissions,
@ -136,7 +137,7 @@ class EmployeeGroupService(val employeeGroupRepository: EmployeeGroupRepository,
}
@Service
class EmployeeUserDetailsService(val employeeService: EmployeeService) : UserDetailsService {
class EmployeeUserDetailsService(val employeeService: EmployeeService, val securityConfigurationProperties: SecurityConfigurationProperties) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserByEmployeeId(username.toLong())
@ -146,7 +147,9 @@ class EmployeeUserDetailsService(val employeeService: EmployeeService) : UserDet
}
/** Loads an [User] for the given [employeeId]. */
fun loadUserByEmployeeId(employeeId: Long): UserDetails {
fun loadUserByEmployeeId(employeeId: Long, allowCommonUser: Boolean = false): UserDetails {
if (!allowCommonUser && employeeId == securityConfigurationProperties.common!!.id!!)
throw UsernameNotFoundException(employeeId.toString())
val employee = employeeService.getById(employeeId, false)
return User(employee.id.toString(), employee.password, employee.getAuthorities())
}

View File

@ -25,7 +25,7 @@ class EmployeeController(employeeService: EmployeeService) :
AbstractRestModelController<Employee, EmployeeService>(employeeService, EMPLOYEE_CONTROLLER_PATH) {
@GetMapping("current")
@ResponseStatus(HttpStatus.OK)
fun getCurrent(loggedInEmployee: Principal): ResponseEntity<Employee> = getById(loggedInEmployee.name.toLong())
fun getCurrent(loggedInEmployee: Principal): ResponseEntity<Employee> = ResponseEntity.ok(service.getById(loggedInEmployee.name.toLong(), false))
@PostMapping
@ResponseStatus(HttpStatus.CREATED)

View File

@ -13,8 +13,12 @@ cre.server.url-use-port=true
cre.server.url-use-https=false
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE
cre.security.jwt-duration=18000000
# Root user
cre.security.root.id=9999
cre.security.root.password=password
# Common user
cre.security.common.id=9998
cre.security.common.password=common
# TYPES DE PRODUIT PAR DÉFAUT
entities.material-types.defaults[0].name=Aucun
entities.material-types.defaults[0].prefix=