Ajout du support complet des employés/groupes.

This commit is contained in:
FyloZ 2020-10-29 20:30:22 -04:00
parent df36da3536
commit 02588ae2f1
65 changed files with 1979 additions and 224 deletions

View File

@ -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",

View File

@ -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"

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [{ path: 'color', loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule) }, { 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)],

View File

@ -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]

View File

@ -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 {

View File

@ -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>

View File

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

View File

@ -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({

View File

@ -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'])
})

View File

@ -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,

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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>

View File

@ -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
}
}
)
}
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -0,0 +1,2 @@
th, td
padding: 0 .7rem !important

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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'])
}
)
}
}
}

View File

@ -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}`)
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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 {
}

View File

@ -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 { }

View File

@ -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>

View File

@ -0,0 +1,5 @@
mat-card
width: max-content
mat-checkbox
font-size: .8em

View File

@ -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)
}
}
)
}
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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}`)
}
}

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,6 @@
mat-card
z-index: 50
position: fixed
left: 50%
top: 50%
transform: translate(-50%, -50%)

View File

@ -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
}
}

View File

@ -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>

View File

@ -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">

View File

@ -1,5 +1,7 @@
header
background-color: black
position: relative
z-index: 99
nav
padding-bottom: 1px

View File

@ -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
) {
}
}

View File

@ -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>

View File

@ -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

View File

@ -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
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
.permissions-list
padding: 0 1rem 1rem
p
font-weight: bold
margin-bottom: .5em

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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]}
]
}

View File

@ -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$))
}
}

View File

@ -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 {
}

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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?
}

View File

@ -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> {