Ajout du support complet des employés/groupes.
This commit is contained in:
parent
df36da3536
commit
02588ae2f1
|
@ -7597,21 +7597,6 @@
|
|||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"ngx-cookie": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-5.0.0.tgz",
|
||||
"integrity": "sha512-wzHC3u9n8H6O2YNfoptNM78re/wnRs1guo8Qg1yThtH64eL/E34JPuzAa/g085beIGhsXMR1YDJGVLnMbluo2A==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"@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"
|
||||
|
|
|
@ -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) }, { path: 'account', loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule) }];
|
||||
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) }, { path: 'employee', loadChildren: () => import('./modules/employees/employees.module').then(m => m.EmployeesModule) }, { path: 'group', loadChildren: () => import('./modules/groups/groups.module').then(m => m.GroupsModule) }];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
import {BrowserModule, DomSanitizer} from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import {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 {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {MatIconRegistry} from "@angular/material/icon";
|
||||
import {SharedModule} from "./modules/shared/shared.module";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
SharedModule,
|
||||
HttpClientModule,
|
||||
CookieModule.forRoot()
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
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';
|
||||
import {LogoutComponent} from './pages/logout/logout.component';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {BrowserModule} from "@angular/platform-browser";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginComponent, LogoutComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
AccountsRoutingModule
|
||||
AccountsRoutingModule,
|
||||
]
|
||||
})
|
||||
export class AccountsModule {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<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">
|
||||
<form [formGroup]="form">
|
||||
<mat-card class="x-centered y-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>
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</mat-label>
|
||||
<input matInput [formControl]="idFormControl" type="text"/>
|
||||
|
@ -25,16 +25,16 @@
|
|||
<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>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="justify-content-end">
|
||||
<button
|
||||
mat-raised-button
|
||||
type="submit"
|
||||
color="accent"
|
||||
[disabled]="form.invalid"
|
||||
(click)="submit()">
|
||||
Connexion
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
mat-card
|
||||
width: 25rem
|
||||
|
||||
&.centered
|
||||
margin: 50vh auto auto
|
||||
position: relative
|
||||
transform: translateY(-70%)
|
||||
|
||||
.alert p
|
||||
margin: 0
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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({
|
||||
|
@ -24,6 +23,10 @@ export class LoginComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/'])
|
||||
}
|
||||
|
||||
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({
|
||||
|
|
|
@ -16,6 +16,10 @@ export class LogoutComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/account/login'])
|
||||
}
|
||||
|
||||
this.accountService.logout(() => {
|
||||
this.router.navigate(['/account/login'])
|
||||
})
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {Subject} from "rxjs";
|
||||
import {take, takeUntil} from "rxjs/operators";
|
||||
import {take, takeUntil, tap} 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";
|
||||
import {Employee, EmployeePermission} from "../../shared/model/employee";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService implements OnDestroy {
|
||||
private $destroy = new Subject<boolean>()
|
||||
private destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
@ -21,8 +21,33 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.$destroy.next(true)
|
||||
this.$destroy.complete()
|
||||
this.destroy$.next(true)
|
||||
this.destroy$.complete()
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.appState.isAuthenticated
|
||||
}
|
||||
|
||||
checkAuthenticationStatus() {
|
||||
if (!this.appState.authenticatedEmployee) {
|
||||
// Try to get current default group user
|
||||
this.http.get<Employee>(`${environment.apiUrl}/employee/current`, {withCredentials: true})
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe({
|
||||
next: employee => this.appState.authenticatedEmployee = employee,
|
||||
error: err => {
|
||||
if (err.status === 404) {
|
||||
console.error('No default user is defined on this computer')
|
||||
} else {
|
||||
console.error('An error occurred while authenticating the default user')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
login(id: number, password: string, success: () => void, error: (err) => void) {
|
||||
|
@ -33,7 +58,7 @@ export class AccountService implements OnDestroy {
|
|||
})
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.$destroy)
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: (response: HttpResponse<any>) => {
|
||||
|
@ -47,17 +72,31 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
logout(success: () => void) {
|
||||
this.appState.isAuthenticated = false
|
||||
this.appState.authenticationExpiration = -1
|
||||
this.appState.authenticatedEmployee = null
|
||||
success()
|
||||
this.api.get<void>('/employee/logout', true).pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.appState.isAuthenticated = false
|
||||
this.appState.authenticationExpiration = -1
|
||||
this.appState.authenticatedEmployee = null
|
||||
this.checkAuthenticationStatus()
|
||||
success()
|
||||
},
|
||||
error: err => console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
hasPermission(permission: EmployeePermission): boolean {
|
||||
return this.appState.authenticatedEmployee.permissions.indexOf(permission) >= 0
|
||||
}
|
||||
|
||||
private setLoggedInEmployeeFromApi() {
|
||||
this.api.get<Employee>("/employee/current", true)
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.$destroy)
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: employee => this.appState.authenticatedEmployee = employee,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import { ColorsRoutingModule } from './colors-routing.module';
|
||||
import { ColorsComponent } from './colors.component';
|
||||
import {ColorsRoutingModule} from './colors-routing.module';
|
||||
import {ColorsComponent} from './colors.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ColorsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ColorsRoutingModule
|
||||
ColorsRoutingModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class ColorsModule { }
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ListComponent } from './pages/list/list.component';
|
||||
import {AddComponent} from "./pages/add/add.component";
|
||||
import {EditComponent} from "./pages/edit/edit.component";
|
||||
import {PasswordEditComponent} from "./pages/password-edit/password-edit.component";
|
||||
|
||||
const routes: Routes = [{ path: 'list', component: ListComponent }, {path: 'add', component: AddComponent}, {path: 'edit/:id', component: EditComponent}, {path: 'password/edit/:id', component: PasswordEditComponent}, {path: '', redirectTo: 'list'}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class EmployeesRoutingModule { }
|
|
@ -0,0 +1,20 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {EmployeesRoutingModule} from './employees-routing.module';
|
||||
import {ListComponent} from './pages/list/list.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import { AddComponent } from './pages/add/add.component';
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import { EditComponent } from './pages/edit/edit.component';
|
||||
import { PasswordEditComponent } from './pages/password-edit/password-edit.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ListComponent, AddComponent, EditComponent, PasswordEditComponent],
|
||||
imports: [
|
||||
EmployeesRoutingModule,
|
||||
SharedModule,
|
||||
MatSelectModule
|
||||
]
|
||||
})
|
||||
export class EmployeesModule { }
|
|
@ -0,0 +1,60 @@
|
|||
<mat-card class="x-centered mt-5">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Création d'un employé</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</mat-label>
|
||||
<input matInput type="text" [formControl]="idControl"/>
|
||||
<mat-icon svgIcon="pound" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="idControl.invalid">
|
||||
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
|
||||
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Prénom</mat-label>
|
||||
<input matInput type="text" [formControl]="firstNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="firstNameControl.invalid">
|
||||
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="lastNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="lastNameControl.invalid">
|
||||
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input matInput type="password" [formControl]="passwordControl"/>
|
||||
<mat-icon svgIcon="lock" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="passwordControl.invalid">
|
||||
<span *ngIf="passwordControl.errors.required">Un mot de passe est requis</span>
|
||||
<span *ngIf="passwordControl.errors.minlength">Le mot de passe doit comprendre au moins 8 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="group$ | async as groups">
|
||||
<mat-label>Groupe</mat-label>
|
||||
<mat-select [formControl]="groupControl">
|
||||
<mat-option [value]="null">Aucun</mat-option>
|
||||
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field #permissionsField></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Créer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,76 @@
|
|||
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {EmployeeGroup} from "../../../shared/model/employee";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {GroupService} from "../../../groups/services/group.service";
|
||||
import {take, takeUntil} from "rxjs/operators";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.sass']
|
||||
})
|
||||
export class AddComponent extends SubscribingComponent {
|
||||
@ViewChild('permissionsField', {static: true}) permissionsField: PermissionsFieldComponent
|
||||
|
||||
form: FormGroup
|
||||
idControl: FormControl
|
||||
firstNameControl: FormControl
|
||||
lastNameControl: FormControl
|
||||
passwordControl: FormControl
|
||||
groupControl: FormControl
|
||||
unknownError = false
|
||||
|
||||
group$: Observable<EmployeeGroup[]> | null
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private groupService: GroupService,
|
||||
private router: Router
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.group$ = this.groupService.all
|
||||
|
||||
this.idControl = new FormControl(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)]))
|
||||
this.firstNameControl = new FormControl(null, Validators.required)
|
||||
this.lastNameControl = new FormControl(null, Validators.required)
|
||||
this.passwordControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8)]))
|
||||
this.groupControl = new FormControl(null, Validators.min(0))
|
||||
this.form = new FormGroup({
|
||||
id: this.idControl,
|
||||
firstName: this.firstNameControl,
|
||||
lastName: this.lastNameControl,
|
||||
password: this.passwordControl,
|
||||
group: this.groupControl
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.permissionsField.valid() && this.form.valid) {
|
||||
this.subscribe(
|
||||
this.employeeService.save(
|
||||
parseInt(this.idControl.value),
|
||||
this.firstNameControl.value,
|
||||
this.lastNameControl.value,
|
||||
this.passwordControl.value,
|
||||
this.groupControl.value,
|
||||
this.permissionsField.allEnabledPermissions
|
||||
),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<mat-card *ngIf="employee" class="x-centered mt-5">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Modification de l'employé #{{employee.id}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</mat-label>
|
||||
<input matInput type="text" [formControl]="idControl"/>
|
||||
<mat-icon svgIcon="pound" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="idControl.invalid">
|
||||
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
|
||||
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Prénom</mat-label>
|
||||
<input matInput type="text" [formControl]="firstNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="firstNameControl.invalid">
|
||||
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="lastNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="lastNameControl.invalid">
|
||||
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="group$ | async as groups">
|
||||
<mat-label>Groupe</mat-label>
|
||||
<mat-select [formControl]="groupControl">
|
||||
<mat-option [value]="null">Aucun</mat-option>
|
||||
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field #permissionsField [enabledPermissions]="employee.permissions"></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="warn" *ngIf="canRemoveEmployee" (click)="confirmBoxComponent.show()">Supprimer
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="submit(permissionsField)" [disabled]="form.invalid">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
<cre-confirm-box #confirmBoxComponent message="Voulez-vous vraiment supprimer l'employé {{employee.id}}?" (confirm)="delete()"></cre-confirm-box>
|
||||
</mat-card>
|
|
@ -0,0 +1,161 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {GroupService} from "../../../groups/services/group.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Observable} from "rxjs";
|
||||
import {Employee, EmployeeGroup, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-edit',
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrls: ['./edit.component.sass']
|
||||
})
|
||||
export class EditComponent extends SubscribingComponent {
|
||||
employee: Employee | null
|
||||
unknownError = false
|
||||
|
||||
group$: Observable<EmployeeGroup[]> | null
|
||||
|
||||
private _idControl: FormControl
|
||||
private _firstNameControl: FormControl
|
||||
private _lastNameControl: FormControl
|
||||
private _groupControl: FormControl
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private employeeService: EmployeeService,
|
||||
private groupService: GroupService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const employeeId = this.activatedRoute.snapshot.paramMap.get("id")
|
||||
this.subscribe(
|
||||
this.employeeService.get(parseInt(employeeId)),
|
||||
{
|
||||
next: employee => this.employee = employee,
|
||||
error: err => {
|
||||
if (err.status === 404) {
|
||||
this.router.navigate(['/employee/list'])
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
this.group$ = this.groupService.all
|
||||
}
|
||||
|
||||
submit(permissionsField: PermissionsFieldComponent) {
|
||||
if (permissionsField.valid() && this.form.valid) {
|
||||
this.subscribe(
|
||||
this.employeeService.update(
|
||||
parseInt(this.idControl.value),
|
||||
this.firstNameControl.value,
|
||||
this.lastNameControl.value,
|
||||
permissionsField.allEnabledPermissions
|
||||
),
|
||||
{
|
||||
next: () => {
|
||||
const group = parseInt(this._groupControl.value)
|
||||
if (!isNaN(group)) {
|
||||
this.subscribe(
|
||||
this.groupService.addEmployeeToGroup(group, this.employee),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (this.employee.group) {
|
||||
this.subscribe(
|
||||
this.groupService.removeEmployeeFromGroup(this.employee),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.router.navigate(['/employee/list'])
|
||||
}
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribe(
|
||||
this.employeeService.delete(this.employee.id),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get form(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
id: this._idControl,
|
||||
firstName: this._firstNameControl,
|
||||
lastName: this._lastNameControl,
|
||||
group: this._groupControl
|
||||
})
|
||||
}
|
||||
|
||||
get idControl(): FormControl {
|
||||
this._idControl = this.lazyControl(this._idControl, () => new FormControl(this.employee.id, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)])))
|
||||
return this._idControl
|
||||
}
|
||||
|
||||
get firstNameControl(): FormControl {
|
||||
this._firstNameControl = this.lazyControl(this._firstNameControl, () => new FormControl(this.employee.firstName, Validators.required))
|
||||
return this._firstNameControl
|
||||
}
|
||||
|
||||
get lastNameControl(): FormControl {
|
||||
this._lastNameControl = this.lazyControl(this._lastNameControl, () => new FormControl(this.employee.lastName, Validators.required))
|
||||
return this._lastNameControl
|
||||
}
|
||||
|
||||
get groupControl(): FormControl {
|
||||
this._groupControl = this.lazyControl(this._groupControl, () => new FormControl(this.employee.group?.id))
|
||||
return this._groupControl
|
||||
}
|
||||
|
||||
private lazyControl(control: FormControl, provider: () => FormControl): FormControl {
|
||||
if (control) return control
|
||||
if (this.employee) {
|
||||
return provider()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
get canRemoveEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.REMOVE_EMPLOYEE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<div class="action-bar">
|
||||
<button *ngIf="canEditEmployee" mat-raised-button color="accent" routerLink="/employee/add">Ajouter</button>
|
||||
</div>
|
||||
|
||||
<table class="mx-auto" *ngIf="employees$ | async as employees" mat-table multiTemplateDataRows [dataSource]="employees">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Numéro d'employé</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.id}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.firstName}} {{employee.lastName}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="group">
|
||||
<th mat-header-cell *matHeaderCellDef>Groupe</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.group">{{employee.group.name}}</ng-container>
|
||||
<ng-container *ngIf="!employee.group">Aucun</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="lastLogin">
|
||||
<th mat-header-cell *matHeaderCellDef>Dernière connexion</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.lastLoginTime">{{getDate(employee.lastLoginTime).toLocaleString()}}</ng-container>
|
||||
<ng-container *ngIf="!employee.lastLoginTime">Jamais</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="permissionCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Permissions</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.permissions">{{employee.permissions.length}}</ng-container>
|
||||
<ng-container *ngIf="!employee.permissions">0</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="editButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditEmployee" *matCellDef="let employee">
|
||||
<button mat-raised-button color="accent" routerLink="/employee/edit/{{employee.id}}">Modifier</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="editPasswordButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditEmployeePassword" *matCellDef="let employee">
|
||||
<button mat-raised-button color="accent" routerLink="/employee/password/edit/{{employee.id}}">Modifier mot de passe</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expandedDetail">
|
||||
<td mat-cell *matCellDef="let employee" [attr.colspan]="columns.length">
|
||||
<div class="entity-detail"
|
||||
[@detailExpand]="employee == expandedElement ? 'expanded' : 'collapsed'">
|
||||
<cre-permissions-list [employee]="employee" class="w-100"></cre-permissions-list>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let employee; columns: columns"
|
||||
class="entity-row can-expand"
|
||||
[class.expanded-row]="expandedElement === employee"
|
||||
(click)="expandedElement = expandedElement === employee ? null : employee">
|
||||
</tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
|
||||
</table>
|
|
@ -0,0 +1,2 @@
|
|||
th, td
|
||||
padding: 0 .7rem !important
|
|
@ -0,0 +1,50 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Observable} from "rxjs";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {Employee, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-employees',
|
||||
templateUrl: './list.component.html',
|
||||
styleUrls: ['./list.component.sass'],
|
||||
animations: [
|
||||
trigger('detailExpand', [
|
||||
state('collapsed', style({height: '0px', minHeight: '0'})),
|
||||
state('expanded', style({height: '*'})),
|
||||
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
|
||||
])
|
||||
]
|
||||
})
|
||||
export class ListComponent extends SubscribingComponent {
|
||||
employees$: Observable<Employee[]>
|
||||
columns = ['id', 'name', 'group', 'permissionCount', 'lastLogin', 'editButton', 'editPasswordButton']
|
||||
|
||||
expandedElement: Employee | null
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private accountService: AccountService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.employees$ = this.employeeService.all.pipe(takeUntil(this.destroy$))
|
||||
}
|
||||
|
||||
getDate(dateString: string) {
|
||||
return new Date(dateString)
|
||||
}
|
||||
|
||||
get canEditEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE)
|
||||
}
|
||||
|
||||
get canEditEmployeePassword(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE_PASSWORD)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<mat-card *ngIf="employee" class="x-centered mt-5">
|
||||
<form [formGroup]="form">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Modification du mot de passe de l'employé #{{employee.id}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input type="password" matInput [formControl]="passwordControl"/>
|
||||
<mat-icon matSuffix svgIcon="lock"></mat-icon>
|
||||
<mat-error *ngIf="passwordControl.invalid">
|
||||
<span *ngIf="passwordControl.errors.required">Un mot de passe est requis</span>
|
||||
<span *ngIf="passwordControl.errors.minlength">Le mot de passe doit comprendre au moins 8 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submit()">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
</form>
|
||||
</mat-card>
|
|
@ -0,0 +1,52 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {Employee} from "../../../shared/model/employee";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-password-edit',
|
||||
templateUrl: './password-edit.component.html',
|
||||
styleUrls: ['./password-edit.component.sass']
|
||||
})
|
||||
export class PasswordEditComponent extends SubscribingComponent {
|
||||
employee: Employee | null
|
||||
|
||||
form: FormGroup
|
||||
passwordControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8)]))
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const employeeId = this.activatedRoute.snapshot.paramMap.get('id')
|
||||
this.subscribe(
|
||||
this.employeeService.get(parseInt(employeeId)),
|
||||
{
|
||||
next: employee => this.employee = employee
|
||||
}
|
||||
)
|
||||
|
||||
this.form = this.formBuilder.group({
|
||||
password: this.passwordControl
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.form.valid) {
|
||||
this.subscribe(
|
||||
this.employeeService.updatePassword(this.employee.id, this.passwordControl.value),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list'])
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Employee, EmployeePermission} from "../../shared/model/employee";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class EmployeeService implements OnDestroy {
|
||||
private _destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy$.next(true)
|
||||
this._destroy$.complete()
|
||||
}
|
||||
|
||||
get all(): Observable<Employee[]> {
|
||||
return this.api.get<Employee[]>('/employee', true)
|
||||
}
|
||||
|
||||
get(id: number): Observable<Employee> {
|
||||
return this.api.get<Employee>(`/employee/${id}`).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
|
||||
save(id: number, firstName: string, lastName: string, password: string, group: number, permissions: EmployeePermission[]): Observable<Employee> {
|
||||
const employee = {id, firstName, lastName, password, group, permissions}
|
||||
return this.api.post<Employee>('/employee', employee).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
|
||||
update(id: number, firstName: string, lastName: string, permissions: EmployeePermission[]): Observable<void> {
|
||||
const employee = {id, firstName, lastName, permissions}
|
||||
return this.api.put<void>('/employee', employee).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
|
||||
updatePassword(id: number, password: string): Observable<void> {
|
||||
return this.api.put<void>(`/employee/${id}/password`, password, true, {headers: {contentType: 'text/plain'}})
|
||||
}
|
||||
|
||||
delete(id: number): Observable<void> {
|
||||
return this.api.delete<void>(`/employee/${id}`)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<ng-container *ngIf="employees$ | async as employees">
|
||||
<table class="my-3 mx-auto mat-elevation-z1" *ngIf="employees.length > 0" mat-table [dataSource]="employees">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Numéro d'employé</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.id}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="firstName">
|
||||
<th mat-header-cell *matHeaderCellDef>Prénom</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.firstName}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="lastName">
|
||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.lastName}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="edit">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditEmployee" *matCellDef="let employee">
|
||||
<button mat-raised-button color="accent">Modifier</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr mat-row *matRowDef="let employee; columns: columns"></tr>
|
||||
<tr>
|
||||
<td>test</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<ng-container *ngIf="employees.length <= 0">
|
||||
<div class="w-100 mt-2 text-center empty">
|
||||
<p>Il n'y a aucun employé dans ce groupe</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
|
@ -0,0 +1,2 @@
|
|||
.d-flex
|
||||
gap: 2rem
|
|
@ -0,0 +1,38 @@
|
|||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Employee, EmployeeGroup, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {GroupService} from "../../services/group.service";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-employees-list',
|
||||
templateUrl: './employees-list.component.html',
|
||||
styleUrls: ['./employees-list.component.sass']
|
||||
})
|
||||
export class EmployeesListComponent implements OnInit, OnDestroy {
|
||||
@Input() group: EmployeeGroup
|
||||
employees$: Observable<Employee[]> | null
|
||||
columns = ['id', 'firstName', 'lastName', 'edit']
|
||||
|
||||
private _destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private groupService: GroupService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.employees$ = this.groupService.getEmployeesForGroup(this.group.id).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy$.next(true)
|
||||
this._destroy$.complete()
|
||||
}
|
||||
|
||||
get canEditEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
|
||||
import {ListComponent} from './pages/list/list.component';
|
||||
import {AddComponent} from "./pages/add/add.component";
|
||||
import {EditComponent} from "./pages/edit/edit.component";
|
||||
|
||||
const routes: Routes = [{path: 'list', component: ListComponent}, {path: 'add', component: AddComponent}, {path: 'edit/:id', component: EditComponent}, {path: '', redirectTo: 'list'}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class GroupsRoutingModule {
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {GroupsRoutingModule} from './groups-routing.module';
|
||||
import {ListComponent} from './pages/list/list.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {AddComponent} from './pages/add/add.component';
|
||||
import {EditComponent} from './pages/edit/edit.component';
|
||||
import {EmployeesListComponent} from './components/employees-list/employees-list.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ListComponent, AddComponent, EditComponent, EmployeesListComponent],
|
||||
imports: [
|
||||
GroupsRoutingModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class GroupsModule { }
|
|
@ -0,0 +1,27 @@
|
|||
<mat-card class="x-centered mt-5">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Création d'un groupe</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field class="pb-3">
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="nameControl"/>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="nameControl.invalid">
|
||||
<span *ngIf="nameControl.errors.required">Un nom est requis</span>
|
||||
<span *ngIf="nameControl.errors.minlength">Le nom d'un groupe doit comprendre au moins 3 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field #permissionsField></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/group/list">Retour</button>
|
||||
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Créer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,5 @@
|
|||
mat-card
|
||||
width: max-content
|
||||
|
||||
mat-checkbox
|
||||
font-size: .8em
|
|
@ -0,0 +1,49 @@
|
|||
import {Component, ViewChild} from '@angular/core';
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {GroupService} from "../../services/group.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.sass']
|
||||
})
|
||||
export class AddComponent extends SubscribingComponent {
|
||||
@ViewChild('permissionsField', {static: true}) permissionsField: PermissionsFieldComponent
|
||||
|
||||
form: FormGroup
|
||||
nameControl: FormControl
|
||||
unknownError = false
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private groupService: GroupService,
|
||||
private router: Router
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nameControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(3)]))
|
||||
this.form = this.formBuilder.group({
|
||||
name: this.nameControl
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.form.valid && this.permissionsField.valid()) {
|
||||
this.subscribe(
|
||||
this.groupService.save(this.nameControl.value, this.permissionsField.allEnabledPermissions),
|
||||
{
|
||||
next: () => this.router.navigate(['/group/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<mat-card *ngIf="group" class="mt-5 x-centered">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Modifier le groupe {{group.name}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="nameControl"/>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="nameControl.invalid">
|
||||
<span *ngIf="nameControl.errors.required">Un nom est requis</span>
|
||||
<span *ngIf="nameControl.errors.minlength">Le nom d'un groupe doit comprendre au moins 3 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field [enabledPermissions]="group.permissions" #permissionsField></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="warn" *ngIf="canRemoveGroup" [disabled]="group.employeeCount > 0"
|
||||
[title]="group.employeeCount > 0 ? 'Il y a des employés dans le groupe' : ''"
|
||||
(click)="confirmBoxComponent.show()">Supprimer
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
<cre-confirm-box #confirmBoxComponent [message]="confirmBoxMessage" (confirm)="delete()"></cre-confirm-box>
|
||||
</mat-card>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import {Component, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {EmployeeGroup, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {GroupService} from "../../services/group.service";
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-edit',
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrls: ['./edit.component.sass']
|
||||
})
|
||||
export class EditComponent extends SubscribingComponent {
|
||||
@ViewChild('permissionsField') permissionsField: PermissionsFieldComponent
|
||||
|
||||
group: EmployeeGroup | null
|
||||
unknownError = false
|
||||
|
||||
private _nameControl: FormControl
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private groupService: GroupService,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const groupId = this.activatedRoute.snapshot.paramMap.get("id")
|
||||
this.subscribe(
|
||||
this.groupService.get(parseInt(groupId)),
|
||||
{
|
||||
next: group => this.group = group,
|
||||
error: err => {
|
||||
if (err.status === 404) {
|
||||
this.router.navigate(['/group/list'])
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (this.form.valid && this.permissionsField.valid()) {
|
||||
this.subscribe(
|
||||
this.groupService.update(this.group.id, this.nameControl.value, this.permissionsField.allEnabledPermissions),
|
||||
{
|
||||
next: () => this.router.navigate(['/group/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribe(
|
||||
this.groupService.delete(this.group.id),
|
||||
{
|
||||
next: () => this.router.navigate(['/group/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get form(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
name: this.nameControl
|
||||
})
|
||||
}
|
||||
|
||||
get confirmBoxMessage(): string {
|
||||
return `Voulez-vous vraiment supprimer le groupe ${this.group.name}?`
|
||||
}
|
||||
|
||||
get nameControl(): FormControl {
|
||||
if (this._nameControl) return this._nameControl
|
||||
if (this.group) {
|
||||
this._nameControl = new FormControl(this.group.name, Validators.compose([Validators.required, Validators.minLength(3)]))
|
||||
return this._nameControl
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
get canRemoveGroup(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.REMOVE_EMPLOYEE_GROUP)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<div class="action-bar">
|
||||
<button *ngIf="canEditGroup" mat-raised-button color="accent" routerLink="/group/add">Ajouter</button>
|
||||
</div>
|
||||
|
||||
<table *ngIf="groups$ | async as groups" mat-table class="mx-auto" multiTemplateDataRows [dataSource]="groups">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||
<td mat-cell *matCellDef="let group">{{group.name}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="permissionCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Nombre de permissions</th>
|
||||
<td mat-cell *matCellDef="let group">{{group.permissions.length}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="employeeCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Nombre d'employés</th>
|
||||
<td mat-cell *matCellDef="let group">{{group.employeeCount}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="defaultGroup">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canSetBrowserDefaultGroup" *matCellDef="let group">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
[disabled]="isDefaultGroup(group) ? true : null"
|
||||
(click)="setDefaultGroup(group)">
|
||||
<ng-container *ngIf="!isDefaultGroup(group)">
|
||||
Définir par défaut
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isDefaultGroup(group)">
|
||||
Par défaut
|
||||
</ng-container>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="editGroup">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditGroup" *matCellDef="let group">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
routerLink="/group/edit/{{group.id}}">
|
||||
Modifier
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expandedDetail">
|
||||
<td mat-cell *matCellDef="let group" [attr.colspan]="columns.length">
|
||||
<div class="entity-detail"
|
||||
[@detailExpand]="group == expandedElement && canViewEmployee ? 'expanded' : 'collapsed'">
|
||||
<cre-employees-list [group]="group" class="w-100"></cre-employees-list>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let group; columns: columns"
|
||||
class="entity-row"
|
||||
[class.expanded-row]="expandedElement === group"
|
||||
[class.can-expand]="canViewEmployee"
|
||||
(click)="expandedElement = expandedElement === group ? null : group">
|
||||
</tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
|
||||
</table>
|
|
@ -0,0 +1,65 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Observable} from "rxjs";
|
||||
import {GroupService} from "../../services/group.service";
|
||||
import {EmployeeGroup, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-groups',
|
||||
templateUrl: './list.component.html',
|
||||
styleUrls: ['./list.component.sass'],
|
||||
animations: [
|
||||
trigger('detailExpand', [
|
||||
state('collapsed', style({height: '0px', minHeight: '0'})),
|
||||
state('expanded', style({height: '*'})),
|
||||
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
|
||||
])
|
||||
]
|
||||
})
|
||||
export class ListComponent extends SubscribingComponent {
|
||||
groups$: Observable<EmployeeGroup[]>
|
||||
defaultGroup: EmployeeGroup = null
|
||||
columns = ['name', 'permissionCount', 'employeeCount', 'defaultGroup', 'editGroup']
|
||||
expandedElement: EmployeeGroup | null
|
||||
|
||||
constructor(
|
||||
private groupService: GroupService,
|
||||
private accountService: AccountService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.groups$ = this.groupService.all.pipe(takeUntil(this.destroy$))
|
||||
this.subscribe(
|
||||
this.groupService.defaultGroup,
|
||||
{next: g => this.defaultGroup = g}
|
||||
)
|
||||
}
|
||||
|
||||
setDefaultGroup(group: EmployeeGroup) {
|
||||
this.subscribe(
|
||||
this.groupService.setDefaultGroup(group),
|
||||
{next: () => this.defaultGroup = group}
|
||||
)
|
||||
}
|
||||
|
||||
isDefaultGroup(group: EmployeeGroup): boolean {
|
||||
return this.defaultGroup && this.defaultGroup.id == group.id
|
||||
}
|
||||
|
||||
get canViewEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.VIEW_EMPLOYEE)
|
||||
}
|
||||
|
||||
get canEditGroup(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE_GROUP)
|
||||
}
|
||||
|
||||
get canSetBrowserDefaultGroup(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.SET_BROWSER_DEFAULT_GROUP)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {Employee, EmployeeGroup, EmployeePermission} from "../../shared/model/employee";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GroupService {
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
get all(): Observable<EmployeeGroup[]> {
|
||||
return this.api.get<EmployeeGroup[]>('/employee/group')
|
||||
}
|
||||
|
||||
get(id: number): Observable<EmployeeGroup> {
|
||||
return this.api.get<EmployeeGroup>(`/employee/group/${id}`);
|
||||
}
|
||||
|
||||
get defaultGroup(): Observable<EmployeeGroup> {
|
||||
return this.api.get<EmployeeGroup>('/employee/group/default')
|
||||
}
|
||||
|
||||
setDefaultGroup(value: EmployeeGroup): Observable<void> {
|
||||
return this.api.post<void>(`/employee/group/default/${value.id}`, {})
|
||||
}
|
||||
|
||||
getEmployeesForGroup(id: number): Observable<Employee[]> {
|
||||
return this.api.get<Employee[]>(`/employee/group/${id}/employees`)
|
||||
}
|
||||
|
||||
addEmployeeToGroup(id: number, employee: Employee): Observable<void> {
|
||||
return this.api.put<void>(`/employee/group/${id}/${employee.id}`)
|
||||
}
|
||||
|
||||
removeEmployeeFromGroup(employee: Employee): Observable<void> {
|
||||
return this.api.delete<void>(`/employee/group/${employee.group.id}/${employee.id}`)
|
||||
}
|
||||
|
||||
save(name: string, permissions: EmployeePermission[]): Observable<EmployeeGroup> {
|
||||
const group = {name, permissions}
|
||||
return this.api.post<EmployeeGroup>('/employee/group', group)
|
||||
}
|
||||
|
||||
update(id: number, name: string, permissions: EmployeePermission[]): Observable<EmployeeGroup> {
|
||||
const group = {id, name, permissions}
|
||||
return this.api.put<EmployeeGroup>('/employee/group', group)
|
||||
}
|
||||
|
||||
delete(id: number): Observable<EmployeeGroup> {
|
||||
return this.api.delete<EmployeeGroup>(`/employee/group/${id}`)
|
||||
}
|
||||
}
|
|
@ -40,9 +40,9 @@ export class AppState {
|
|||
set authenticatedEmployee(value: Employee) {
|
||||
if (value === null) {
|
||||
sessionStorage.removeItem(this.KEY_LOGGED_IN_EMPLOYEE)
|
||||
return
|
||||
} else {
|
||||
sessionStorage.setItem(this.KEY_LOGGED_IN_EMPLOYEE, JSON.stringify(value))
|
||||
}
|
||||
sessionStorage.setItem(this.KEY_LOGGED_IN_EMPLOYEE, JSON.stringify(value))
|
||||
this.authenticatedUser$.next({
|
||||
authenticated: this.isAuthenticated,
|
||||
authenticatedUser: value
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<div *ngIf="visible">
|
||||
<div class="darker-background"></div>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Confirmation</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
{{message}}
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" (click)="emitCancel()">Annuler</button>
|
||||
<button mat-raised-button color="accent" (click)="emitConfirm()">Confirmer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
mat-card
|
||||
z-index: 50
|
||||
position: fixed
|
||||
left: 50%
|
||||
top: 50%
|
||||
transform: translate(-50%, -50%)
|
|
@ -0,0 +1,33 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'cre-confirm-box',
|
||||
templateUrl: './confirm-box.component.html',
|
||||
styleUrls: ['./confirm-box.component.sass']
|
||||
})
|
||||
export class ConfirmBoxComponent {
|
||||
@Input() message: string
|
||||
|
||||
@Output() cancel = new EventEmitter<void>()
|
||||
@Output() confirm = new EventEmitter<void>()
|
||||
|
||||
visible = false
|
||||
|
||||
emitCancel() {
|
||||
this.visible = false
|
||||
this.cancel.emit()
|
||||
}
|
||||
|
||||
emitConfirm() {
|
||||
this.visible = false
|
||||
this.confirm.emit()
|
||||
}
|
||||
|
||||
show() {
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
|
@ -1,7 +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 *ngIf="employee" class="employee-info-container d-flex flex-column">
|
||||
<labeled-icon *ngIf="authenticated" 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="authenticated" 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>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<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">
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
header
|
||||
background-color: black
|
||||
position: relative
|
||||
z-index: 99
|
||||
|
||||
nav
|
||||
padding-bottom: 1px
|
||||
|
|
|
@ -1,51 +1,86 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Router} from "@angular/router";
|
||||
import {AppState} from "../../app-state";
|
||||
import {Employee, EmployeePermission} from "../../model/employee";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.sass']
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
links = [
|
||||
{route: 'color', title: 'Couleurs', enabled: true},
|
||||
{route: 'inventory', title: 'Inventaire', enabled: true},
|
||||
export class HeaderComponent implements OnInit, OnDestroy {
|
||||
links: HeaderLink[] = [
|
||||
// {route: 'color', title: 'Couleurs', enabled: true},
|
||||
// {route: 'inventory', title: 'Inventaire', enabled: true},
|
||||
new HeaderLink('employee', 'Employés', EmployeePermission.VIEW_EMPLOYEE),
|
||||
new HeaderLink('group', 'Groupes', EmployeePermission.VIEW_EMPLOYEE_GROUP),
|
||||
{route: 'account/login', title: 'Connexion', enabled: true},
|
||||
{route: 'account/logout', title: 'Déconnexion', enabled: false}
|
||||
{route: 'account/logout', title: 'Déconnexion', enabled: false},
|
||||
];
|
||||
_activeLink = this.links[0].route;
|
||||
|
||||
private destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
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.accountService.checkAuthenticationStatus()
|
||||
this.updateEnabledLinks(this.appState.isAuthenticated, this.appState.authenticatedEmployee)
|
||||
|
||||
this.appState.authenticatedUser$.subscribe({
|
||||
next: authentication => {
|
||||
loginLink.enabled = !authentication.authenticated
|
||||
logoutLink.enabled = authentication.authenticated
|
||||
}
|
||||
this.appState.authenticatedUser$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: authentication => this.updateEnabledLinks(authentication.authenticated, authentication.authenticatedUser)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(true)
|
||||
this.destroy$.complete()
|
||||
this.accountService.logout(() => {
|
||||
console.log("Successfully logged out")
|
||||
})
|
||||
}
|
||||
|
||||
set activeLink(link: string) {
|
||||
this._activeLink = link;
|
||||
this.router.navigate([link]);
|
||||
this._activeLink = link
|
||||
this.router.navigate([link])
|
||||
}
|
||||
|
||||
get activeLink() {
|
||||
return this._activeLink;
|
||||
return this._activeLink
|
||||
}
|
||||
|
||||
test(link: any) {
|
||||
console.log(link.condition ? link.condition() : true)
|
||||
private updateEnabledLinks(authenticated: boolean, employee: Employee) {
|
||||
this.link('account/login').enabled = !authenticated
|
||||
this.link('account/logout').enabled = authenticated
|
||||
|
||||
this.links.forEach(l => {
|
||||
if (l.requiredPermission) {
|
||||
l.enabled = employee && employee.permissions.indexOf(l.requiredPermission) >= 0;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private link(route: string) {
|
||||
return this.links.filter(l => l.route === route)[0]
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderLink {
|
||||
constructor(
|
||||
public route: string,
|
||||
public title: string,
|
||||
public requiredPermission?: EmployeePermission,
|
||||
public enabled = false
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<div #permissions class="permissions-field">
|
||||
<p>{{title}}</p>
|
||||
<div class="d-flex flex-row justify-content-between permissions-list">
|
||||
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'view'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'edit'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'remove'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'other'}"></ng-container>
|
||||
</div>
|
||||
<mat-error *ngIf="!permissionsValid">Un group doit avoir au moins une permission</mat-error>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
#permissionTemplate
|
||||
let-type="type">
|
||||
<div class="d-flex flex-column">
|
||||
<mat-checkbox
|
||||
*ngFor="let permission of permissionControls[type]"
|
||||
[formControl]="permission.control"
|
||||
(click)="togglePermission(permission)">
|
||||
{{permission.description}}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,12 @@
|
|||
.permissions-field
|
||||
p
|
||||
margin-bottom: .5em !important
|
||||
|
||||
&.invalid p
|
||||
color: rgb(244, 67, 54)
|
||||
|
||||
mat-error
|
||||
font-size: .8em
|
||||
|
||||
.permissions-list
|
||||
gap: 0 1rem
|
|
@ -0,0 +1,110 @@
|
|||
import {Component, Input, OnInit, ViewChild} from '@angular/core';
|
||||
import {EmployeePermission, mapped_permissions} from "../../model/employee";
|
||||
import {FormControl} from "@angular/forms";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-permissions-field',
|
||||
templateUrl: './permissions-field.component.html',
|
||||
styleUrls: ['./permissions-field.component.sass']
|
||||
})
|
||||
export class PermissionsFieldComponent implements OnInit {
|
||||
@Input() enabledPermissions: EmployeePermission[]
|
||||
@Input() title = 'Permissions'
|
||||
@Input() required = true
|
||||
|
||||
permissionControls: any = {view: [], edit: [], remove: [], other: []}
|
||||
@ViewChild('permissions', {static: true}) permissionsDiv: HTMLDivElement
|
||||
permissionsValid = true
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.mapPermissions('view')
|
||||
this.mapPermissions('edit')
|
||||
this.mapPermissions('remove')
|
||||
this.mapPermissions('other')
|
||||
|
||||
if (this.enabledPermissions) {
|
||||
this.enabledPermissions.forEach(p => {
|
||||
const control = this.findPermissionControl(p)
|
||||
control.control.setValue(true)
|
||||
this.togglePermission(control, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
togglePermission(permission: any, bypassValue?: boolean) {
|
||||
if (permission.control.enabled) {
|
||||
const allImpliedControls = this.getImpliedPermissionControls(permission)
|
||||
allImpliedControls.forEach(c => {
|
||||
c.control.setValue(bypassValue === undefined ? !permission.control.value : bypassValue)
|
||||
if (bypassValue === undefined) {
|
||||
permission.control.value ? c.control.enable() : c.control.disable()
|
||||
} else {
|
||||
!bypassValue ? c.control.enable() : c.control.disable()
|
||||
}
|
||||
c.enabledFromParent = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
valid() {
|
||||
if (this.checkPermissionsValid()) {
|
||||
// @ts-ignore
|
||||
this.permissionsDiv.nativeElement.classList.remove('invalid')
|
||||
this.permissionsValid = true
|
||||
|
||||
return true
|
||||
}
|
||||
// @ts-ignore
|
||||
this.permissionsDiv.nativeElement.classList.add('invalid')
|
||||
this.permissionsValid = false
|
||||
return false
|
||||
}
|
||||
|
||||
get allEnabledPermissions(): EmployeePermission[] {
|
||||
return this.allPermissionControls().filter(p => p.control.value).map(p => p.permission)
|
||||
}
|
||||
|
||||
private checkPermissionsValid() {
|
||||
return !this.required || this.allPermissionControls().map(p => p.control).filter(c => c.value).length > 0
|
||||
}
|
||||
|
||||
private mapPermissions(type: string) {
|
||||
mapped_permissions[type].forEach(p => this.permissionControls[type].push({
|
||||
permission: p.permission,
|
||||
impliedPermissions: p.impliedPermissions,
|
||||
description: p.description,
|
||||
control: new FormControl({value: false, disabled: !this.accountService.hasPermission(p.permission)})
|
||||
}))
|
||||
}
|
||||
|
||||
private allPermissionControls(): any[] {
|
||||
// @ts-ignore
|
||||
return Object.values(this.permissionControls).flatMap(p => p)
|
||||
}
|
||||
|
||||
private findPermissionControl(permission: EmployeePermission): any {
|
||||
return this.allPermissionControls().filter(p => p.permission === permission)[0]
|
||||
}
|
||||
|
||||
private getImpliedPermissionControls(permissionControl: any): any[] {
|
||||
const impliedPermissions = []
|
||||
if (permissionControl.impliedPermissions && permissionControl.impliedPermissions.length > 0) {
|
||||
permissionControl.impliedPermissions.map(p => {
|
||||
const permission = this.findPermissionControl(p)
|
||||
impliedPermissions.push(permission)
|
||||
this.getImpliedPermissionControls(permission).forEach(i => {
|
||||
if (impliedPermissions.indexOf(i) < 0) {
|
||||
impliedPermissions.push(i)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return impliedPermissions
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<div class="d-flex flex-column">
|
||||
<div class="permissions-list" *ngIf="employee.permissions">
|
||||
<p>Permissions</p>
|
||||
<ng-container *ngTemplateOutlet="permissionsList; context:{permissions: permissions}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #permissionsList let-permissions="permissions">
|
||||
<mat-chip-list>
|
||||
<mat-chip *ngFor="let permission of permissions">{{permission}}</mat-chip>
|
||||
</mat-chip-list>
|
||||
</ng-template>
|
|
@ -0,0 +1,6 @@
|
|||
.permissions-list
|
||||
padding: 0 1rem 1rem
|
||||
|
||||
p
|
||||
font-weight: bold
|
||||
margin-bottom: .5em
|
|
@ -0,0 +1,24 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {Employee, EmployeePermission, mapped_permissions} from "../../model/employee";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-permissions-list',
|
||||
templateUrl: './permissions-list.component.html',
|
||||
styleUrls: ['./permissions-list.component.sass']
|
||||
})
|
||||
export class PermissionsListComponent implements OnInit {
|
||||
@Input() employee: Employee
|
||||
|
||||
// @ts-ignore
|
||||
private _permissions = Object.values(mapped_permissions).flatMap(p => p)
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
get permissions(): EmployeePermission[] {
|
||||
return this._permissions.filter(p => this.employee.permissions.indexOf(p.permission) >= 0).map(p => p.description)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import {take, takeUntil} from "rxjs/operators";
|
||||
import {OnDestroy, OnInit} from "@angular/core";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
|
||||
export abstract class SubscribingComponent implements OnInit, OnDestroy {
|
||||
protected subscribers$ = []
|
||||
protected destroy$ = new Subject<boolean>()
|
||||
|
||||
subscribe<T>(observable: Observable<T>, observer, take_count = -1) {
|
||||
if (!observer.error) {
|
||||
observer.error = err => console.log(err)
|
||||
}
|
||||
|
||||
if (take_count >= 0) {
|
||||
observable.pipe(take(take_count), takeUntil(this.destroy$))
|
||||
} else {
|
||||
observable.pipe(takeUntil(this.destroy$))
|
||||
}
|
||||
this.subscribers$.push(observable.subscribe(observer))
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(true)
|
||||
this.destroy$.complete()
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ export class Employee {
|
|||
public firstName: string,
|
||||
public lastName: string,
|
||||
public permissions: EmployeePermission[],
|
||||
public excludedPermissions: EmployeePermission[],
|
||||
public group?: EmployeeGroup,
|
||||
public lastLoginTime?: Date
|
||||
) {
|
||||
|
@ -15,23 +14,49 @@ export class EmployeeGroup {
|
|||
constructor(
|
||||
public id: number,
|
||||
public name: string,
|
||||
public permissions: EmployeePermission[]
|
||||
public permissions: EmployeePermission[],
|
||||
public employeeCount: number
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
export enum EmployeePermission {
|
||||
VIEW_EMPLOYEE,
|
||||
VIEW_EMPLOYEE_GROUP,
|
||||
VIEW,
|
||||
VIEW_EMPLOYEE = 'VIEW_EMPLOYEE',
|
||||
VIEW_EMPLOYEE_GROUP = 'VIEW_EMPLOYEE_GROUP',
|
||||
VIEW = 'VIEW',
|
||||
|
||||
EDIT_EMPLOYEE,
|
||||
EDIT_EMPLOYEE_GROUP,
|
||||
EDIT,
|
||||
EDIT_EMPLOYEE = 'EDIT_EMPLOYEE',
|
||||
EDIT_EMPLOYEE_PASSWORD = 'EDIT_EMPLOYEE_PASSWORD',
|
||||
EDIT_EMPLOYEE_GROUP = 'EDIT_EMPLOYEE_GROUP',
|
||||
EDIT = 'EDIT',
|
||||
|
||||
REMOVE_EMPLOYEE,
|
||||
REMOVE_EMPLOYEE_GROUP,
|
||||
REMOVE,
|
||||
REMOVE_EMPLOYEE = 'REMOVE_EMPLOYEE',
|
||||
REMOVE_EMPLOYEE_GROUP = 'REMOVE_EMPLOYEE_GROUP',
|
||||
REMOVE = 'REMOVE',
|
||||
|
||||
ADMIN
|
||||
SET_BROWSER_DEFAULT_GROUP = 'SET_BROWSER_DEFAULT_GROUP',
|
||||
ADMIN = 'ADMIN'
|
||||
}
|
||||
|
||||
export const mapped_permissions = {
|
||||
view: [
|
||||
{permission: EmployeePermission.VIEW_EMPLOYEE, description: 'Voir les employés', impliedPermissions: []},
|
||||
{permission: EmployeePermission.VIEW_EMPLOYEE_GROUP, description: 'Voir les groupes', impliedPermissions: []},
|
||||
{permission: EmployeePermission.VIEW, description: 'Voir', impliedPermissions: []},
|
||||
],
|
||||
edit: [
|
||||
{permission: EmployeePermission.EDIT_EMPLOYEE, description: 'Modifier les employés', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE]},
|
||||
{permission: EmployeePermission.EDIT_EMPLOYEE_PASSWORD, description: 'Modifier le mot de passe des employés', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]},
|
||||
{permission: EmployeePermission.EDIT_EMPLOYEE_GROUP, description: 'Modifier les groupes', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]},
|
||||
{permission: EmployeePermission.EDIT, description: 'Modifier', impliedPermissions: [EmployeePermission.VIEW]},
|
||||
],
|
||||
remove: [
|
||||
{permission: EmployeePermission.REMOVE_EMPLOYEE, description: 'Supprimer les employés', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]},
|
||||
{permission: EmployeePermission.REMOVE_EMPLOYEE_GROUP, description: 'Supprimer les groupes', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE_GROUP]},
|
||||
{permission: EmployeePermission.REMOVE, description: 'Supprimer', impliedPermissions: [EmployeePermission.EDIT]},
|
||||
],
|
||||
other: [
|
||||
{permission: EmployeePermission.SET_BROWSER_DEFAULT_GROUP, description: 'Définir le groupe par défaut', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]},
|
||||
{permission: EmployeePermission.ADMIN, description: 'Administrateur', impliedPermissions: [EmployeePermission.REMOVE, EmployeePermission.SET_BROWSER_DEFAULT_GROUP, EmployeePermission.REMOVE_EMPLOYEE, EmployeePermission.EDIT_EMPLOYEE_PASSWORD, EmployeePermission.REMOVE_EMPLOYEE_GROUP]}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {Observable} from "rxjs";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {environment} from "../../../../environments/environment";
|
||||
import {AppState} from "../app-state";
|
||||
import {Router} from "@angular/router";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
export class ApiService implements OnDestroy {
|
||||
private _destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private appState: AppState,
|
||||
|
@ -16,31 +19,36 @@ export class ApiService {
|
|||
) {
|
||||
}
|
||||
|
||||
get<T>(url: string, needAuthentication = false, options: any = {}): Observable<T> {
|
||||
ngOnDestroy(): void {
|
||||
this._destroy$.next(true)
|
||||
this._destroy$.complete()
|
||||
}
|
||||
|
||||
get<T>(url: string, needAuthentication = true, options: any = {}): Observable<T> {
|
||||
if (this.checkAuthenticated(needAuthentication, options)) {
|
||||
// @ts-ignore
|
||||
return this.http.get<string>(environment.apiUrl + url, options)
|
||||
return this.http.get<string>(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
}
|
||||
|
||||
post<T>(url: string, body: any, needAuthentication = true, options: any = {}): Observable<T> {
|
||||
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)
|
||||
return this.http.post<T>(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
}
|
||||
|
||||
put<T>(url: string, body: any, needAuthentication = true, options: any = {}): Observable<T> {
|
||||
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)
|
||||
return this.http.put<T>(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return this.http.delete<T>(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +1,56 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
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';
|
||||
|
||||
import {EmployeeInfoComponent} from './components/employee-info/employee-info.component';
|
||||
import {LabeledIconComponent} from './components/labeled-icon/labeled-icon.component';
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {HttpClientModule} from "@angular/common/http";
|
||||
import {MatCheckboxModule} from "@angular/material/checkbox";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {ConfirmBoxComponent} from './components/confirm-box/confirm-box.component';
|
||||
import {PermissionsListComponent} from './components/permissions-list/permissions-list.component';
|
||||
import {MatChipsModule} from "@angular/material/chips";
|
||||
import {PermissionsFieldComponent} from "./components/permissions-field/permissions-field.component";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent],
|
||||
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent],
|
||||
exports: [
|
||||
CommonModule,
|
||||
HttpClientModule,
|
||||
HeaderComponent,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatCheckboxModule,
|
||||
MatListModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
LabeledIconComponent,
|
||||
ConfirmBoxComponent,
|
||||
PermissionsListComponent,
|
||||
PermissionsFieldComponent
|
||||
],
|
||||
imports: [
|
||||
MatTabsModule,
|
||||
CommonModule,
|
||||
MatIconModule
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
export class SharedModule {
|
||||
}
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
mat-card
|
||||
padding: 0 !important
|
||||
width: max-content
|
||||
|
||||
&.x-centered
|
||||
margin: auto
|
||||
|
||||
&.y-centered
|
||||
margin-top: 50vh
|
||||
position: relative
|
||||
transform: translateY(-70%)
|
||||
|
||||
mat-card-header
|
||||
background-color: $color-primary
|
||||
|
@ -13,19 +22,93 @@ mat-card
|
|||
margin-top: 16px
|
||||
padding: 0 16px
|
||||
|
||||
mat-form-field
|
||||
width: 100%
|
||||
|
||||
mat-card-actions
|
||||
display: flex !important
|
||||
padding: 0 24px 16px 24px !important
|
||||
flex-direction: row
|
||||
justify-content: flex-end
|
||||
gap: 1rem
|
||||
|
||||
button
|
||||
text-transform: uppercase
|
||||
letter-spacing: 1.25px
|
||||
|
||||
table
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12)
|
||||
|
||||
th
|
||||
background-color: $color-primary
|
||||
color: $light-primary-text !important
|
||||
text-transform: uppercase
|
||||
|
||||
&:first-child
|
||||
border-top-left-radius: 4px
|
||||
|
||||
&:last-child
|
||||
border-top-right-radius: 4px
|
||||
|
||||
th, td
|
||||
padding: 0 1rem !important
|
||||
|
||||
tr.detail-row
|
||||
height: 0
|
||||
|
||||
tr.entity-row.can-expand:not(.expanded-row):hover
|
||||
background-color: map-get($theme-primary, 50)
|
||||
|
||||
tr.entity-row.can-expand:not(.expanded-row):active
|
||||
background-color: map-get($theme-primary, 100)
|
||||
|
||||
.entity-row td
|
||||
border-bottom-width: 0
|
||||
|
||||
.entity-detail
|
||||
overflow: hidden
|
||||
display: flex
|
||||
|
||||
.disabled button
|
||||
display: none
|
||||
|
||||
button
|
||||
text-transform: uppercase
|
||||
font-weight: 500
|
||||
|
||||
&.mat-accent
|
||||
color: white !important
|
||||
|
||||
div.empty
|
||||
color: $dark-secondary-text
|
||||
margin: auto
|
||||
|
||||
.action-bar
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: flex-end
|
||||
padding: 1.5rem 3rem
|
||||
|
||||
button
|
||||
margin-left: 1rem
|
||||
|
||||
.alert p
|
||||
margin-bottom: 0
|
||||
|
||||
.dark-background
|
||||
position: fixed
|
||||
width: 100%
|
||||
height: 100%
|
||||
top: 0
|
||||
left: 0
|
||||
background-color: black
|
||||
opacity: 0.05
|
||||
|
||||
.darker-background
|
||||
position: fixed
|
||||
width: 100%
|
||||
height: 100%
|
||||
top: 0
|
||||
left: 0
|
||||
background-color: black
|
||||
opacity: 0.4
|
||||
|
|
|
@ -114,7 +114,6 @@ class WebSecurityConfig(
|
|||
}
|
||||
|
||||
createUser(securityConfigurationProperties.root, "Root", "User", listOf(EmployeePermission.ADMIN))
|
||||
createUser(securityConfigurationProperties.common, "Common", "User", listOf(EmployeePermission.VIEW))
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
|
@ -131,13 +130,15 @@ class WebSecurityConfig(
|
|||
}
|
||||
|
||||
http
|
||||
// .addFilterBefore(CorsFilter())
|
||||
.cors()
|
||||
.and()
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/").permitAll()
|
||||
.antMatchers("/api/login").permitAll()
|
||||
.antMatchers("/api/employee/logout").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/employee/current").authenticated()
|
||||
.generateAuthorizations()
|
||||
.and()
|
||||
|
@ -166,6 +167,10 @@ class CorsFilter : Filter {
|
|||
}
|
||||
}
|
||||
|
||||
const val authorizationCookieName = "Authorization"
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>()
|
||||
|
||||
class JwtAuthenticationFilter(
|
||||
val authManager: AuthenticationManager,
|
||||
val employeeService: EmployeeService,
|
||||
|
@ -195,8 +200,8 @@ class JwtAuthenticationFilter(
|
|||
.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("Set-Cookie", "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict")
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
response.addHeader("X-Authentication-Expiration", "$expirationMs")
|
||||
}
|
||||
}
|
||||
|
@ -207,15 +212,18 @@ class JwtAuthorizationFilter(
|
|||
authenticationManager: AuthenticationManager
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
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)
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
val authorizationValue = if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName)
|
||||
if (authorizationValue != null && authorizationValue.startsWith("Bearer") && authorizationValue !in blacklistedJwtTokens) {
|
||||
val authenticationToken = getAuthentication(authorizationValue)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
} else {
|
||||
// Load common user if there is no valid authentication data
|
||||
getAuthenticationToken(securityConfigurationProperties.common!!.id!!.toString())
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
}
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
|
@ -231,7 +239,7 @@ class JwtAuthorizationFilter(
|
|||
}
|
||||
|
||||
private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken {
|
||||
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), true)
|
||||
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false)
|
||||
return UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
|
||||
}
|
||||
}
|
||||
|
@ -250,12 +258,22 @@ private enum class ControllerAuthorizations(
|
|||
val antMatcher: String,
|
||||
val permissions: Map<HttpMethod, EmployeePermission>
|
||||
) {
|
||||
SET_BROWSER_DEFAULT_GROUP("/api/employee/group/default/**", mapOf(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
|
||||
HttpMethod.POST to EmployeePermission.SET_BROWSER_DEFAULT_GROUP
|
||||
)),
|
||||
EMPLOYEES_FOR_GROUP("/api/employee/group/*/employees", mapOf(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE
|
||||
)),
|
||||
EMPLOYEE_GROUP("/api/employee/group/**", mapOf(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
|
||||
HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE_GROUP,
|
||||
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_GROUP,
|
||||
HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE_GROUP
|
||||
)),
|
||||
EMPLOYEE_PASSWORD("/api/employee/*/password", mapOf(
|
||||
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_PASSWORD
|
||||
)),
|
||||
EMPLOYEE("/api/employee/**", mapOf(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE,
|
||||
HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package dev.fyloz.trial.colorrecipesexplorer.core.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
@ -10,6 +11,7 @@ import javax.persistence.*
|
|||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
import kotlin.jvm.Transient
|
||||
|
||||
|
||||
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
|
||||
|
@ -19,7 +21,7 @@ private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
|
|||
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
|
||||
|
||||
@Entity
|
||||
class Employee(
|
||||
data class Employee(
|
||||
@Id
|
||||
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
|
||||
override val id: Long,
|
||||
|
@ -31,6 +33,9 @@ class Employee(
|
|||
@JsonIgnore
|
||||
val password: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@JsonIgnore
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
|
@ -40,17 +45,17 @@ class Employee(
|
|||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
@get:JsonIgnore
|
||||
val permissions: MutableList<EmployeePermission> = mutableListOf(),
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val excludedPermissions: MutableList<EmployeePermission> = mutableListOf(),
|
||||
|
||||
val lastLoginTime: LocalDateTime? = null
|
||||
) : IModel
|
||||
) : IModel {
|
||||
@JsonProperty("permissions")
|
||||
fun getFlattenedPermissions(): Iterable<EmployeePermission> = getPermissions()
|
||||
}
|
||||
|
||||
/** DTO for creating employees. The [Employee] entity doesn't allow to modify passwords. */
|
||||
/** DTO for creating employees. Allow a [password] a [groupId]. */
|
||||
data class EmployeeDto(
|
||||
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
@ -67,16 +72,11 @@ data class EmployeeDto(
|
|||
|
||||
@field:ManyToOne
|
||||
@Fetch(FetchMode.SELECT)
|
||||
var group: EmployeeGroup? = null,
|
||||
var groupId: Long? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
val permissions: MutableList<EmployeePermission> = mutableListOf(),
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val excludedPermissions: MutableList<EmployeePermission> = mutableListOf()
|
||||
val permissions: MutableList<EmployeePermission> = mutableListOf()
|
||||
)
|
||||
|
||||
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
|
||||
|
@ -88,20 +88,22 @@ data class EmployeeGroup(
|
|||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
override val id: Long? = null,
|
||||
|
||||
@Column(unique = true)
|
||||
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
|
||||
@field:Size(min = 3)
|
||||
@Column(unique = true)
|
||||
val name: String = "",
|
||||
|
||||
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
|
||||
val permissions: MutableList<EmployeePermission> = mutableListOf(),
|
||||
|
||||
@OneToMany
|
||||
@JsonIgnore
|
||||
val employees: MutableList<Employee> = mutableListOf()
|
||||
) : IModel
|
||||
) : IModel {
|
||||
fun getEmployeeCount() = employees.size
|
||||
}
|
||||
|
||||
|
||||
data class EmployeeLoginRequest(val id: Long, val password: String)
|
||||
|
@ -112,32 +114,37 @@ enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> =
|
|||
VIEW_EMPLOYEE,
|
||||
VIEW_EMPLOYEE_GROUP,
|
||||
VIEW(listOf(
|
||||
|
||||
)),
|
||||
|
||||
// Edit
|
||||
EDIT_EMPLOYEE,
|
||||
EDIT_EMPLOYEE_GROUP,
|
||||
EDIT_EMPLOYEE(listOf(VIEW_EMPLOYEE)),
|
||||
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_EMPLOYEE)),
|
||||
EDIT_EMPLOYEE_GROUP(listOf(VIEW_EMPLOYEE_GROUP)),
|
||||
EDIT(listOf(
|
||||
VIEW
|
||||
)),
|
||||
|
||||
// Remove
|
||||
REMOVE_EMPLOYEE,
|
||||
REMOVE_EMPLOYEE_GROUP,
|
||||
REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)),
|
||||
REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)),
|
||||
REMOVE(listOf(
|
||||
EDIT
|
||||
)),
|
||||
|
||||
// Others
|
||||
SET_BROWSER_DEFAULT_GROUP(listOf(
|
||||
VIEW_EMPLOYEE_GROUP
|
||||
)),
|
||||
|
||||
ADMIN(listOf(
|
||||
VIEW,
|
||||
EDIT,
|
||||
REMOVE,
|
||||
SET_BROWSER_DEFAULT_GROUP,
|
||||
|
||||
// Admin only permissions
|
||||
VIEW_EMPLOYEE,
|
||||
VIEW_EMPLOYEE_GROUP,
|
||||
EDIT_EMPLOYEE,
|
||||
EDIT_EMPLOYEE_GROUP,
|
||||
REMOVE_EMPLOYEE,
|
||||
REMOVE_EMPLOYEE_GROUP
|
||||
EDIT_EMPLOYEE_PASSWORD,
|
||||
REMOVE_EMPLOYEE_GROUP,
|
||||
));
|
||||
|
||||
operator fun contains(permission: EmployeePermission): Boolean {
|
||||
|
@ -153,8 +160,8 @@ fun Employee.getAuthorities(): MutableCollection<GrantedAuthority> {
|
|||
/** Gets [EmployeePermission]s of the given [Employee]. */
|
||||
fun Employee.getPermissions(): Iterable<EmployeePermission> {
|
||||
val grantedPermissions: MutableSet<EmployeePermission> = mutableSetOf()
|
||||
if (group != null) grantedPermissions.addAll(group!!.permissions.flatMap { it.flat() }.filter { excludedPermissions.isEmpty() || excludedPermissions.any { excludedPermission -> it !in excludedPermission } })
|
||||
grantedPermissions.addAll(permissions.flatMap { it.flat() }.filter { excludedPermissions.isEmpty() || excludedPermissions.any { excludedPermission -> it !in excludedPermission } })
|
||||
if (group != null) grantedPermissions.addAll(group!!.permissions.flatMap { it.flat() })
|
||||
grantedPermissions.addAll(permissions.flatMap { it.flat() })
|
||||
return grantedPermissions
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +1,65 @@
|
|||
package dev.fyloz.trial.colorrecipesexplorer.core.services
|
||||
|
||||
import dev.fyloz.trial.colorrecipesexplorer.core.configuration.SecurityConfigurationProperties
|
||||
import dev.fyloz.trial.colorrecipesexplorer.core.configuration.blacklistedJwtTokens
|
||||
import dev.fyloz.trial.colorrecipesexplorer.core.configuration.defaultGroupCookieName
|
||||
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.*
|
||||
import dev.fyloz.trial.colorrecipesexplorer.core.model.*
|
||||
import dev.fyloz.trial.colorrecipesexplorer.dao.EmployeeGroupRepository
|
||||
import dev.fyloz.trial.colorrecipesexplorer.dao.EmployeeRepository
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.time.LocalDateTime
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.transaction.Transactional
|
||||
|
||||
@Service
|
||||
class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEncoder: PasswordEncoder) :
|
||||
AbstractModelService<Employee, EmployeeRepository>(employeeRepository, Employee::class.java) {
|
||||
@Autowired
|
||||
lateinit var groupService: EmployeeGroupService
|
||||
|
||||
/** Check if an [Employee] with the given [firstName] and [lastName] exists. */
|
||||
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean {
|
||||
return repository.existsByFirstNameAndLastName(firstName, lastName)
|
||||
}
|
||||
|
||||
override fun getAll(): Collection<Employee> {
|
||||
return super.getAll().filter { !it.isSystemUser }
|
||||
return super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
|
||||
}
|
||||
|
||||
override fun getById(id: Long): Employee {
|
||||
return getById(id, true)
|
||||
return getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
}
|
||||
|
||||
/** Gets the employee with the given [id]. */
|
||||
fun getById(id: Long, ignoreSystemUsers: Boolean): Employee {
|
||||
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
|
||||
return super.getById(id).apply {
|
||||
if (ignoreSystemUsers && isSystemUser) throw EntityNotFoundRestException(id)
|
||||
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) throw EntityNotFoundRestException(id)
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets all employees which have the given [group]. */
|
||||
fun getByGroup(group: EmployeeGroup): Collection<Employee> {
|
||||
return repository.findByGroup(group).filter { !it.isSystemUser && !it.isDefaultGroupUser }
|
||||
}
|
||||
|
||||
/** Gets the default user of the given [group]. */
|
||||
fun getDefaultGroupUser(group: EmployeeGroup): Employee {
|
||||
return repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
|
||||
}
|
||||
|
||||
/** Saves the given [employee]. The password contained in the DTO will be hashed in the created [Employee]. */
|
||||
fun save(employee: EmployeeDto): Employee {
|
||||
return save(with(employee) {
|
||||
Employee(id, firstName, lastName, passwordEncoder.encode(password), false, group, permissions, excludedPermissions)
|
||||
Employee(id, firstName, lastName, passwordEncoder.encode(password), isDefaultGroupUser = false, isSystemUser = false, group = if (groupId != null) groupService.getById(groupId!!) else null, permissions = permissions)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,16 +72,16 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
|
|||
|
||||
/** Updates the last login time of the employee with the given [employeeId]. */
|
||||
fun updateLastLoginTime(employeeId: Long) {
|
||||
update(Employee(id = employeeId, lastLoginTime = LocalDateTime.now()), false)
|
||||
update(Employee(id = employeeId, lastLoginTime = LocalDateTime.now()), ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
|
||||
}
|
||||
|
||||
override fun update(entity: Employee): Employee {
|
||||
return update(entity, true)
|
||||
return update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
}
|
||||
|
||||
/** Updates de given [entity]. **/
|
||||
fun update(entity: Employee, ignoreSystemUsers: Boolean): Employee {
|
||||
val persistedEmployee = getById(entity.id, ignoreSystemUsers)
|
||||
fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
|
||||
val persistedEmployee = getById(entity.id, ignoreDefaultGroupUsers, ignoreSystemUsers)
|
||||
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
|
||||
if (this != null && id != entity.id)
|
||||
throw EntityAlreadyExistsRestException("${entity.firstName} ${entity.lastName}")
|
||||
|
@ -74,30 +93,121 @@ class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEn
|
|||
if (firstName.isNotBlank()) firstName else persistedEmployee.firstName,
|
||||
if (lastName.isNotBlank()) lastName else persistedEmployee.lastName,
|
||||
persistedEmployee.password,
|
||||
if (ignoreDefaultGroupUsers) false else persistedEmployee.isDefaultGroupUser,
|
||||
if (ignoreSystemUsers) false else persistedEmployee.isSystemUser,
|
||||
persistedEmployee.group,
|
||||
if (permissions.isNotEmpty()) permissions else persistedEmployee.permissions,
|
||||
if (excludedPermissions.isNotEmpty()) excludedPermissions else persistedEmployee.excludedPermissions,
|
||||
lastLoginTime ?: persistedEmployee.lastLoginTime
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** Updates the password of the employee with the given [id]. */
|
||||
fun updatePassword(id: Long, password: String) {
|
||||
val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
super.update(with(persistedEmployee) {
|
||||
Employee(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** Adds the given [permission] to the employee with the given [employeeId]. */
|
||||
fun addPermission(employeeId: Long, permission: EmployeePermission) = super.update(getById(employeeId).apply { permissions += permission })
|
||||
|
||||
/** Removes the given [permission] from the employee with the given [employeeId]. */
|
||||
fun removePermission(employeeId: Long, permission: EmployeePermission) = super.update(getById(employeeId).apply { permissions -= permission })
|
||||
|
||||
/** Adds the given [excludedPermission] to the employee with the given [employeeId]. */
|
||||
fun addExcludedPermission(employeeId: Long, excludedPermission: EmployeePermission) = super.update(getById(employeeId).apply { excludedPermissions += excludedPermission })
|
||||
|
||||
/** Removes the given [excludedPermission] to the employee with the given [employeeId]. */
|
||||
fun removeExcludedPermission(employeeId: Long, excludedPermission: EmployeePermission) = super.update(getById(employeeId).apply { excludedPermissions -= excludedPermission })
|
||||
/** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */
|
||||
fun logout(request: HttpServletRequest) {
|
||||
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
|
||||
if (authorizationCookie != null) {
|
||||
val authorizationToken = authorizationCookie.value
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
|
||||
blacklistedJwtTokens.add(authorizationToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
||||
|
||||
@Service
|
||||
class EmployeeGroupService(val employeeGroupRepository: EmployeeGroupRepository, val employeeService: EmployeeService) : AbstractModelService<EmployeeGroup, EmployeeGroupRepository>(employeeGroupRepository, EmployeeGroup::class.java) {
|
||||
/** Checks if a group with the given [name] exists. */
|
||||
fun existsByName(name: String): Boolean {
|
||||
return repository.existsByName(name)
|
||||
}
|
||||
|
||||
/** Gets all the employees of the group with the given [id]. */
|
||||
fun getEmployeesForGroup(id: Long): Collection<Employee> {
|
||||
return employeeService.getByGroup(getById(id))
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun save(entity: EmployeeGroup): EmployeeGroup {
|
||||
fun createDefaultGroupUser(group: EmployeeGroup) {
|
||||
employeeService.save(Employee(
|
||||
id = 1000000L + group.id!!,
|
||||
firstName = group.name,
|
||||
lastName = "Employee",
|
||||
password = employeeService.passwordEncoder.encode(group.name),
|
||||
isDefaultGroupUser = true,
|
||||
group = group
|
||||
))
|
||||
}
|
||||
|
||||
val group = super.save(entity)
|
||||
createDefaultGroupUser(group)
|
||||
return group
|
||||
}
|
||||
|
||||
override fun update(entity: EmployeeGroup): EmployeeGroup {
|
||||
val persistedGroup = getById(entity.id!!)
|
||||
with(repository.findByName(entity.name)) {
|
||||
if (this != null && id != entity.id)
|
||||
throw EntityAlreadyExistsRestException(entity.name)
|
||||
}
|
||||
|
||||
return super.update(with(entity) {
|
||||
EmployeeGroup(
|
||||
entity.id,
|
||||
if (name.isNotBlank()) entity.name else persistedGroup.name,
|
||||
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions,
|
||||
persistedGroup.employees
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun delete(entity: EmployeeGroup) {
|
||||
employeeService.delete(employeeService.getDefaultGroupUser(entity))
|
||||
super.delete(entity)
|
||||
}
|
||||
|
||||
/** Gets the default group cookie for the given HTTP [request]. */
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw EntityNotFoundRestException("defaultGroup")
|
||||
val defaultGroupUser = employeeService.getById(defaultGroupCookie.value.toLong(), ignoreDefaultGroupUsers = false, ignoreSystemUsers = true)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
||||
/** Sets the default group cookie for the given HTTP [response]. */
|
||||
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
|
||||
val group = getById(groupId)
|
||||
val defaultGroupUser = employeeService.getDefaultGroupUser(group)
|
||||
response.addHeader("Set-Cookie", "$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict")
|
||||
}
|
||||
|
||||
/** Adds the employee with the given [employeeId] to the group with the given [groupId]. */
|
||||
fun addEmployeeToGroup(groupId: Long, employeeId: Long) {
|
||||
addEmployeeToGroup(getById(groupId), employeeService.getById(employeeId))
|
||||
|
@ -140,17 +250,17 @@ class EmployeeGroupService(val employeeGroupRepository: EmployeeGroupRepository,
|
|||
class EmployeeUserDetailsService(val employeeService: EmployeeService, val securityConfigurationProperties: SecurityConfigurationProperties) : UserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserByEmployeeId(username.toLong())
|
||||
return loadUserByEmployeeId(username.toLong(), true)
|
||||
} catch (ex: EntityNotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
} catch (ex: EntityNotFoundRestException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads an [User] for the given [employeeId]. */
|
||||
fun loadUserByEmployeeId(employeeId: Long, allowCommonUser: Boolean = false): UserDetails {
|
||||
if (!allowCommonUser && employeeId == securityConfigurationProperties.common!!.id!!)
|
||||
throw UsernameNotFoundException(employeeId.toString())
|
||||
val employee = employeeService.getById(employeeId, false)
|
||||
fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails {
|
||||
val employee = employeeService.getById(employeeId, ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, ignoreSystemUsers = false)
|
||||
return User(employee.id.toString(), employee.password, employee.getAuthorities())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,15 @@ interface EmployeeRepository : JpaRepository<Employee, Long> {
|
|||
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
|
||||
|
||||
fun findByFirstNameAndLastName(firstName: String, lastName: String): Employee?
|
||||
|
||||
fun findByGroup(group: EmployeeGroup): Collection<Employee>
|
||||
|
||||
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: EmployeeGroup): Employee
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface EmployeeGroupRepository : JpaRepository<EmployeeGroup, Long>
|
||||
interface EmployeeGroupRepository : JpaRepository<EmployeeGroup, Long> {
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
fun findByName(name: String): EmployeeGroup?
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeGroupService
|
|||
import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeService
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.net.URI
|
||||
import java.security.Principal
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val EMPLOYEE_CONTROLLER_PATH = "api/employee"
|
||||
|
@ -25,7 +27,7 @@ class EmployeeController(employeeService: EmployeeService) :
|
|||
AbstractRestModelController<Employee, EmployeeService>(employeeService, EMPLOYEE_CONTROLLER_PATH) {
|
||||
@GetMapping("current")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getCurrent(loggedInEmployee: Principal): ResponseEntity<Employee> = ResponseEntity.ok(service.getById(loggedInEmployee.name.toLong(), false))
|
||||
fun getCurrent(loggedInEmployee: Principal): ResponseEntity<Employee> = ResponseEntity.ok(service.getById(loggedInEmployee.name.toLong(), ignoreDefaultGroupUsers = false, ignoreSystemUsers = false))
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
|
@ -40,9 +42,17 @@ class EmployeeController(employeeService: EmployeeService) :
|
|||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
override fun save(entity: Employee): ResponseEntity<Employee> = ResponseEntity.notFound().build()
|
||||
|
||||
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun updatePassword(@PathVariable id: Long, @RequestBody password: String): ResponseEntity<Void> {
|
||||
service.updatePassword(id, password)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
}
|
||||
|
||||
@PutMapping("{employeeId}/permissions/{permission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
|
||||
fun addPermission(@PathVariable employeeId: Long, @PathVariable permission: EmployeePermission): ResponseEntity<Void> {
|
||||
service.addPermission(employeeId, permission)
|
||||
return ResponseEntity
|
||||
|
@ -52,7 +62,6 @@ class EmployeeController(employeeService: EmployeeService) :
|
|||
|
||||
@DeleteMapping("{employeeId}/permissions/{permission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
|
||||
fun removePermission(@PathVariable employeeId: Long, @PathVariable permission: EmployeePermission): ResponseEntity<Void> {
|
||||
service.removePermission(employeeId, permission)
|
||||
return ResponseEntity
|
||||
|
@ -60,24 +69,11 @@ class EmployeeController(employeeService: EmployeeService) :
|
|||
.build()
|
||||
}
|
||||
|
||||
@PutMapping("{employeeId}/excludedPermissions/{excludedPermission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
|
||||
fun addExcludedPermission(@PathVariable employeeId: Long, @PathVariable excludedPermission: EmployeePermission): ResponseEntity<Void> {
|
||||
service.addExcludedPermission(employeeId, excludedPermission)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
}
|
||||
|
||||
@DeleteMapping("{employeeId}/excludedPermissions/{excludedPermission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
|
||||
fun removeExcludedPermission(@PathVariable employeeId: Long, @PathVariable excludedPermission: EmployeePermission): ResponseEntity<Void> {
|
||||
service.removeExcludedPermission(employeeId, excludedPermission)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
@GetMapping("logout")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun logout(request: HttpServletRequest): ResponseEntity<Void> {
|
||||
service.logout(request)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,6 +82,24 @@ class EmployeeController(employeeService: EmployeeService) :
|
|||
@Profile("rest")
|
||||
class GroupsController(groupService: EmployeeGroupService) :
|
||||
AbstractRestModelController<EmployeeGroup, EmployeeGroupService>(groupService, EMPLOYEE_GROUP_CONTROLLER_PATH) {
|
||||
@GetMapping("{id}/employees")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getEmployeesForGroup(@PathVariable id: Long): ResponseEntity<Collection<Employee>> = ResponseEntity.ok(service.getEmployeesForGroup(id))
|
||||
|
||||
@PostMapping("default/{groupId}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse): ResponseEntity<Void> {
|
||||
service.setResponseDefaultGroup(groupId, response)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
}
|
||||
|
||||
@GetMapping("default")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): ResponseEntity<EmployeeGroup> =
|
||||
ResponseEntity.ok(service.getRequestDefaultGroup(request))
|
||||
|
||||
@PutMapping("{groupId}/{employeeId}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun addEmployeeToGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity<Void> {
|
Loading…
Reference in New Issue