Ajout des comptes à l'interface Angular.
This commit is contained in:
parent
3bafc3d9ef
commit
df36da3536
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:9090",
|
||||
"target": "http://localhost:9090/api",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)],
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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>
|
|
@ -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%
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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'])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<div class="d-flex flex-row">
|
||||
<mat-icon [svgIcon]="icon"></mat-icon>
|
||||
<p>{{label}}</p>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
mat-icon
|
||||
width: 1em
|
||||
|
||||
p
|
||||
margin: 4px 0 0 .3em
|
||||
font-size: .8em
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'])
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const environment = {
|
||||
production: true
|
||||
production: true,
|
||||
apiUrl: '/api'
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -32,7 +32,7 @@ class Employee(
|
|||
val password: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
val isRoot: Boolean = false,
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
@field:ManyToOne
|
||||
@Fetch(FetchMode.SELECT)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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=
|
||||
|
|
Loading…
Reference in New Issue