diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json index dac2081..37c25c2 100644 --- a/src/main/frontend/package-lock.json +++ b/src/main/frontend/package-lock.json @@ -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", diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index 25e03c9..167b4a9 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -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" diff --git a/src/main/frontend/src/app/app-routing.module.ts b/src/main/frontend/src/app/app-routing.module.ts index 5a09a55..3d0729e 100644 --- a/src/main/frontend/src/app/app-routing.module.ts +++ b/src/main/frontend/src/app/app-routing.module.ts @@ -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)], diff --git a/src/main/frontend/src/app/app.module.ts b/src/main/frontend/src/app/app.module.ts index 1a679dc..fdace87 100644 --- a/src/main/frontend/src/app/app.module.ts +++ b/src/main/frontend/src/app/app.module.ts @@ -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] diff --git a/src/main/frontend/src/app/modules/accounts/accounts.module.ts b/src/main/frontend/src/app/modules/accounts/accounts.module.ts index 33f0a16..a935cd7 100644 --- a/src/main/frontend/src/app/modules/accounts/accounts.module.ts +++ b/src/main/frontend/src/app/modules/accounts/accounts.module.ts @@ -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 { diff --git a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.html b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.html index 030ac9a..8917593 100644 --- a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.html +++ b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.html @@ -1,13 +1,13 @@
- - - Connexion au système - - -
-

Les identifiants entrés sont invalides.

-
-
+ + + + Connexion au système + + +
+

Les identifiants entrés sont invalides.

+
Numéro d'employé @@ -25,16 +25,16 @@ Un mot de passe est requis - -
- - - -
+
+ + + +
+ diff --git a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.sass b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.sass index c95ac6f..afd1937 100644 --- a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.sass +++ b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.sass @@ -1,11 +1,6 @@ mat-card width: 25rem - &.centered - margin: 50vh auto auto - position: relative - transform: translateY(-70%) - .alert p margin: 0 diff --git a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.ts b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.ts index 8cd46c3..024e06c 100644 --- a/src/main/frontend/src/app/modules/accounts/pages/login/login.component.ts +++ b/src/main/frontend/src/app/modules/accounts/pages/login/login.component.ts @@ -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({ diff --git a/src/main/frontend/src/app/modules/accounts/pages/logout/logout.component.ts b/src/main/frontend/src/app/modules/accounts/pages/logout/logout.component.ts index 283064c..8b180bd 100644 --- a/src/main/frontend/src/app/modules/accounts/pages/logout/logout.component.ts +++ b/src/main/frontend/src/app/modules/accounts/pages/logout/logout.component.ts @@ -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']) }) diff --git a/src/main/frontend/src/app/modules/accounts/services/account.service.ts b/src/main/frontend/src/app/modules/accounts/services/account.service.ts index 76809e6..99a3613 100644 --- a/src/main/frontend/src/app/modules/accounts/services/account.service.ts +++ b/src/main/frontend/src/app/modules/accounts/services/account.service.ts @@ -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() + private destroy$ = new Subject() 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(`${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) => { @@ -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('/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/current", true) .pipe( take(1), - takeUntil(this.$destroy) + takeUntil(this.destroy$) ) .subscribe({ next: employee => this.appState.authenticatedEmployee = employee, diff --git a/src/main/frontend/src/app/modules/colors/colors.module.ts b/src/main/frontend/src/app/modules/colors/colors.module.ts index 1692601..20e8b4e 100644 --- a/src/main/frontend/src/app/modules/colors/colors.module.ts +++ b/src/main/frontend/src/app/modules/colors/colors.module.ts @@ -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 { } diff --git a/src/main/frontend/src/app/modules/employees/employees-routing.module.ts b/src/main/frontend/src/app/modules/employees/employees-routing.module.ts new file mode 100644 index 0000000..f800e7c --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/employees-routing.module.ts @@ -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 { } diff --git a/src/main/frontend/src/app/modules/employees/employees.module.ts b/src/main/frontend/src/app/modules/employees/employees.module.ts new file mode 100644 index 0000000..47211be --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/employees.module.ts @@ -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 { } diff --git a/src/main/frontend/src/app/modules/employees/pages/add/add.component.html b/src/main/frontend/src/app/modules/employees/pages/add/add.component.html new file mode 100644 index 0000000..69f1ebe --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/add/add.component.html @@ -0,0 +1,60 @@ + + + Création d'un employé + + +
+

Une erreur est survenue

+
+ +
+ + Numéro d'employé + + + + Un numéro d'employé est requis + Le numéro d'employé doit être un nombre + + + + Prénom + + + + Un prénom est requis + + + + Nom + + + + Un nom est requis + + + + Mot de passe + + + + Un mot de passe est requis + Le mot de passe doit comprendre au moins 8 caractères + + + + Groupe + + Aucun + {{group.name}} + + + + +
+
+ + + + +
diff --git a/src/main/frontend/src/app/modules/employees/pages/add/add.component.sass b/src/main/frontend/src/app/modules/employees/pages/add/add.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/employees/pages/add/add.component.ts b/src/main/frontend/src/app/modules/employees/pages/add/add.component.ts new file mode 100644 index 0000000..bb04f4b --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/add/add.component.ts @@ -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 | 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 + } + } + ) + } + } +} diff --git a/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.html b/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.html new file mode 100644 index 0000000..4aa184b --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.html @@ -0,0 +1,54 @@ + + + Modification de l'employé #{{employee.id}} + + +
+

Une erreur est survenue

+
+ +
+ + Numéro d'employé + + + + Un numéro d'employé est requis + Le numéro d'employé doit être un nombre + + + + Prénom + + + + Un prénom est requis + + + + Nom + + + + Un nom est requis + + + + Groupe + + Aucun + {{group.name}} + + + + +
+
+ + + + + + +
diff --git a/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.sass b/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.ts b/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.ts new file mode 100644 index 0000000..10aee43 --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/edit/edit.component.ts @@ -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 | 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) + } +} diff --git a/src/main/frontend/src/app/modules/employees/pages/list/list.component.html b/src/main/frontend/src/app/modules/employees/pages/list/list.component.html new file mode 100644 index 0000000..670196a --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/list/list.component.html @@ -0,0 +1,66 @@ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Numéro d'employé{{employee.id}}Nom{{employee.firstName}} {{employee.lastName}}Groupe + {{employee.group.name}} + Aucun + Dernière connexion + {{getDate(employee.lastLoginTime).toLocaleString()}} + Jamais + Permissions + {{employee.permissions.length}} + 0 + + + + + +
+ +
+
diff --git a/src/main/frontend/src/app/modules/employees/pages/list/list.component.sass b/src/main/frontend/src/app/modules/employees/pages/list/list.component.sass new file mode 100644 index 0000000..afa5e79 --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/list/list.component.sass @@ -0,0 +1,2 @@ +th, td + padding: 0 .7rem !important diff --git a/src/main/frontend/src/app/modules/employees/pages/list/list.component.ts b/src/main/frontend/src/app/modules/employees/pages/list/list.component.ts new file mode 100644 index 0000000..0e365bc --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/list/list.component.ts @@ -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 + 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) + } +} diff --git a/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.html b/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.html new file mode 100644 index 0000000..31c1aa6 --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.html @@ -0,0 +1,22 @@ + +
+ + Modification du mot de passe de l'employé #{{employee.id}} + + + + Mot de passe + + + + Un mot de passe est requis + Le mot de passe doit comprendre au moins 8 caractères + + + + + + + +
+
diff --git a/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.sass b/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.ts b/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.ts new file mode 100644 index 0000000..f0f12d8 --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/pages/password-edit/password-edit.component.ts @@ -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']) + } + ) + } + } +} diff --git a/src/main/frontend/src/app/modules/employees/services/employee.service.ts b/src/main/frontend/src/app/modules/employees/services/employee.service.ts new file mode 100644 index 0000000..05548cd --- /dev/null +++ b/src/main/frontend/src/app/modules/employees/services/employee.service.ts @@ -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() + + constructor( + private api: ApiService + ) { + } + + ngOnDestroy(): void { + this._destroy$.next(true) + this._destroy$.complete() + } + + get all(): Observable { + return this.api.get('/employee', true) + } + + get(id: number): Observable { + return this.api.get(`/employee/${id}`).pipe(takeUntil(this._destroy$)) + } + + save(id: number, firstName: string, lastName: string, password: string, group: number, permissions: EmployeePermission[]): Observable { + const employee = {id, firstName, lastName, password, group, permissions} + return this.api.post('/employee', employee).pipe(takeUntil(this._destroy$)) + } + + update(id: number, firstName: string, lastName: string, permissions: EmployeePermission[]): Observable { + const employee = {id, firstName, lastName, permissions} + return this.api.put('/employee', employee).pipe(takeUntil(this._destroy$)) + } + + updatePassword(id: number, password: string): Observable { + return this.api.put(`/employee/${id}/password`, password, true, {headers: {contentType: 'text/plain'}}) + } + + delete(id: number): Observable { + return this.api.delete(`/employee/${id}`) + } +} diff --git a/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.html b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.html new file mode 100644 index 0000000..f26acaa --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
Numéro d'employé{{employee.id}}Prénom{{employee.firstName}}Nom{{employee.lastName}} + +
test
+ + +
+

Il n'y a aucun employé dans ce groupe

+
+
+
diff --git a/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.sass b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.sass new file mode 100644 index 0000000..cde651d --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.sass @@ -0,0 +1,2 @@ +.d-flex + gap: 2rem diff --git a/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.ts b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.ts new file mode 100644 index 0000000..d9133f0 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/components/employees-list/employees-list.component.ts @@ -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 | null + columns = ['id', 'firstName', 'lastName', 'edit'] + + private _destroy$ = new Subject() + + 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) + } +} diff --git a/src/main/frontend/src/app/modules/groups/groups-routing.module.ts b/src/main/frontend/src/app/modules/groups/groups-routing.module.ts new file mode 100644 index 0000000..5f93f42 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/groups-routing.module.ts @@ -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 { +} diff --git a/src/main/frontend/src/app/modules/groups/groups.module.ts b/src/main/frontend/src/app/modules/groups/groups.module.ts new file mode 100644 index 0000000..75a5f24 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/groups.module.ts @@ -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 { } diff --git a/src/main/frontend/src/app/modules/groups/pages/add/add.component.html b/src/main/frontend/src/app/modules/groups/pages/add/add.component.html new file mode 100644 index 0000000..37169fb --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/add/add.component.html @@ -0,0 +1,27 @@ + + + Création d'un groupe + + +
+

Une erreur est survenue

+
+ +
+ + Nom + + + + Un nom est requis + Le nom d'un groupe doit comprendre au moins 3 caractères + + + +
+
+ + + + +
diff --git a/src/main/frontend/src/app/modules/groups/pages/add/add.component.sass b/src/main/frontend/src/app/modules/groups/pages/add/add.component.sass new file mode 100644 index 0000000..5586bfb --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/add/add.component.sass @@ -0,0 +1,5 @@ +mat-card + width: max-content + +mat-checkbox + font-size: .8em diff --git a/src/main/frontend/src/app/modules/groups/pages/add/add.component.ts b/src/main/frontend/src/app/modules/groups/pages/add/add.component.ts new file mode 100644 index 0000000..c448a6e --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/add/add.component.ts @@ -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) + } + } + ) + } + } +} diff --git a/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.html b/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.html new file mode 100644 index 0000000..2f45204 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.html @@ -0,0 +1,33 @@ + + + Modifier le groupe {{group.name}} + + +
+

Une erreur est survenue

+
+ +
+ + Nom + + + + Un nom est requis + Le nom d'un groupe doit comprendre au moins 3 caractères + + + +
+
+ + + + + + +
+ diff --git a/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.sass b/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.ts b/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.ts new file mode 100644 index 0000000..f50737b --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/edit/edit.component.ts @@ -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) + } +} diff --git a/src/main/frontend/src/app/modules/groups/pages/list/list.component.html b/src/main/frontend/src/app/modules/groups/pages/list/list.component.html new file mode 100644 index 0000000..302a5a6 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/list/list.component.html @@ -0,0 +1,66 @@ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nom{{group.name}}Nombre de permissions{{group.permissions.length}}Nombre d'employés{{group.employeeCount}} + + + + +
+ +
+
diff --git a/src/main/frontend/src/app/modules/groups/pages/list/list.component.sass b/src/main/frontend/src/app/modules/groups/pages/list/list.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts b/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts new file mode 100644 index 0000000..9b58e88 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts @@ -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 + 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) + } +} diff --git a/src/main/frontend/src/app/modules/groups/services/group.service.ts b/src/main/frontend/src/app/modules/groups/services/group.service.ts new file mode 100644 index 0000000..81b0fc4 --- /dev/null +++ b/src/main/frontend/src/app/modules/groups/services/group.service.ts @@ -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 { + return this.api.get('/employee/group') + } + + get(id: number): Observable { + return this.api.get(`/employee/group/${id}`); + } + + get defaultGroup(): Observable { + return this.api.get('/employee/group/default') + } + + setDefaultGroup(value: EmployeeGroup): Observable { + return this.api.post(`/employee/group/default/${value.id}`, {}) + } + + getEmployeesForGroup(id: number): Observable { + return this.api.get(`/employee/group/${id}/employees`) + } + + addEmployeeToGroup(id: number, employee: Employee): Observable { + return this.api.put(`/employee/group/${id}/${employee.id}`) + } + + removeEmployeeFromGroup(employee: Employee): Observable { + return this.api.delete(`/employee/group/${employee.group.id}/${employee.id}`) + } + + save(name: string, permissions: EmployeePermission[]): Observable { + const group = {name, permissions} + return this.api.post('/employee/group', group) + } + + update(id: number, name: string, permissions: EmployeePermission[]): Observable { + const group = {id, name, permissions} + return this.api.put('/employee/group', group) + } + + delete(id: number): Observable { + return this.api.delete(`/employee/group/${id}`) + } +} diff --git a/src/main/frontend/src/app/modules/shared/app-state.ts b/src/main/frontend/src/app/modules/shared/app-state.ts index 26ad68c..66e36fc 100644 --- a/src/main/frontend/src/app/modules/shared/app-state.ts +++ b/src/main/frontend/src/app/modules/shared/app-state.ts @@ -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 diff --git a/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.html b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.html new file mode 100644 index 0000000..214ca49 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.html @@ -0,0 +1,16 @@ +
+
+ + + + Confirmation + + + {{message}} + + + + + + +
diff --git a/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.sass b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.sass new file mode 100644 index 0000000..a1f24e6 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.sass @@ -0,0 +1,6 @@ +mat-card + z-index: 50 + position: fixed + left: 50% + top: 50% + transform: translate(-50%, -50%) diff --git a/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.ts b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.ts new file mode 100644 index 0000000..cd8e137 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/confirm-box/confirm-box.component.ts @@ -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() + @Output() confirm = new EventEmitter() + + visible = false + + emitCancel() { + this.visible = false + this.cancel.emit() + } + + emitConfirm() { + this.visible = false + this.confirm.emit() + } + + show() { + this.visible = true + } + + hide() { + this.visible = false + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/employee-info/employee-info.component.html b/src/main/frontend/src/app/modules/shared/components/employee-info/employee-info.component.html index e8bf432..fb386ba 100644 --- a/src/main/frontend/src/app/modules/shared/components/employee-info/employee-info.component.html +++ b/src/main/frontend/src/app/modules/shared/components/employee-info/employee-info.component.html @@ -1,7 +1,7 @@ -
- +
+
- +
diff --git a/src/main/frontend/src/app/modules/shared/components/header/header.component.html b/src/main/frontend/src/app/modules/shared/components/header/header.component.html index a0858a7..b9a6043 100644 --- a/src/main/frontend/src/app/modules/shared/components/header/header.component.html +++ b/src/main/frontend/src/app/modules/shared/components/header/header.component.html @@ -5,7 +5,6 @@ diff --git a/src/main/frontend/src/app/modules/shared/components/header/header.component.sass b/src/main/frontend/src/app/modules/shared/components/header/header.component.sass index f1e12fc..ecf2389 100644 --- a/src/main/frontend/src/app/modules/shared/components/header/header.component.sass +++ b/src/main/frontend/src/app/modules/shared/components/header/header.component.sass @@ -1,5 +1,7 @@ header background-color: black + position: relative + z-index: 99 nav padding-bottom: 1px diff --git a/src/main/frontend/src/app/modules/shared/components/header/header.component.ts b/src/main/frontend/src/app/modules/shared/components/header/header.component.ts index 90df0f2..39a5efe 100644 --- a/src/main/frontend/src/app/modules/shared/components/header/header.component.ts +++ b/src/main/frontend/src/app/modules/shared/components/header/header.component.ts @@ -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() + 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 + ) { } } diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.html b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.html new file mode 100644 index 0000000..21e6656 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.html @@ -0,0 +1,23 @@ +
+

{{title}}

+
+ + + + +
+ Un group doit avoir au moins une permission +
+ + +
+ + {{permission.description}} + +
+
diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.sass b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.sass new file mode 100644 index 0000000..3ddfccc --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.sass @@ -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 diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.ts b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.ts new file mode 100644 index 0000000..7f3e0d7 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-field/permissions-field.component.ts @@ -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 + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.html b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.html new file mode 100644 index 0000000..530ebfb --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.html @@ -0,0 +1,12 @@ +
+
+

Permissions

+ +
+
+ + + + {{permission}} + + diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.sass b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.sass new file mode 100644 index 0000000..66535ea --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.sass @@ -0,0 +1,6 @@ +.permissions-list + padding: 0 1rem 1rem + + p + font-weight: bold + margin-bottom: .5em diff --git a/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.ts b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.ts new file mode 100644 index 0000000..837edd0 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/permissions-list/permissions-list.component.ts @@ -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) + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/subscribing.component.ts b/src/main/frontend/src/app/modules/shared/components/subscribing.component.ts new file mode 100644 index 0000000..7cd70f3 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/subscribing.component.ts @@ -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() + + subscribe(observable: Observable, 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() + } +} diff --git a/src/main/frontend/src/app/modules/shared/model/employee.ts b/src/main/frontend/src/app/modules/shared/model/employee.ts index 7189614..f826b7e 100644 --- a/src/main/frontend/src/app/modules/shared/model/employee.ts +++ b/src/main/frontend/src/app/modules/shared/model/employee.ts @@ -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]} + ] } diff --git a/src/main/frontend/src/app/modules/shared/service/api.service.ts b/src/main/frontend/src/app/modules/shared/service/api.service.ts index 5f0f8ee..fec9103 100644 --- a/src/main/frontend/src/app/modules/shared/service/api.service.ts +++ b/src/main/frontend/src/app/modules/shared/service/api.service.ts @@ -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() + constructor( private http: HttpClient, private appState: AppState, @@ -16,31 +19,36 @@ export class ApiService { ) { } - get(url: string, needAuthentication = false, options: any = {}): Observable { + ngOnDestroy(): void { + this._destroy$.next(true) + this._destroy$.complete() + } + + get(url: string, needAuthentication = true, options: any = {}): Observable { if (this.checkAuthenticated(needAuthentication, options)) { // @ts-ignore - return this.http.get(environment.apiUrl + url, options) + return this.http.get(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$)) } } - post(url: string, body: any, needAuthentication = true, options: any = {}): Observable { + post(url: string, body: any = {}, needAuthentication = true, options: any = {}): Observable { if (this.checkAuthenticated(needAuthentication, options)) { // @ts-ignore - return this.http.post(environment.apiUrl + url, body, options) + return this.http.post(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$)) } } - put(url: string, body: any, needAuthentication = true, options: any = {}): Observable { + put(url: string, body: any = {}, needAuthentication = true, options: any = {}): Observable { if (this.checkAuthenticated(needAuthentication, options)) { // @ts-ignore - return this.http.put(environment.apiUrl + url, body, options) + return this.http.put(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$)) } } delete(url: string, needAuthentication = true, options: any = {}): Observable { if (this.checkAuthenticated(needAuthentication, options)) { // @ts-ignore - return this.http.delete(environment.apiUrl + url, options) + return this.http.delete(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$)) } } diff --git a/src/main/frontend/src/app/modules/shared/shared.module.ts b/src/main/frontend/src/app/modules/shared/shared.module.ts index 0369918..8e08257 100644 --- a/src/main/frontend/src/app/modules/shared/shared.module.ts +++ b/src/main/frontend/src/app/modules/shared/shared.module.ts @@ -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 { +} diff --git a/src/main/frontend/src/styles.sass b/src/main/frontend/src/styles.sass index a903f5e..d02630c 100644 --- a/src/main/frontend/src/styles.sass +++ b/src/main/frontend/src/styles.sass @@ -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 diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/configuration/WebSecurityConfig.kt b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/configuration/WebSecurityConfig.kt index 2c2258b..dfcd2d6 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/configuration/WebSecurityConfig.kt +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/configuration/WebSecurityConfig.kt @@ -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() + 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 ) { + 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, diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/model/AccountsModel.kt b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/model/AccountsModel.kt index cff6a66..e1c6b97 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/model/AccountsModel.kt +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/model/AccountsModel.kt @@ -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 = mutableListOf(), - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - @Fetch(FetchMode.SUBSELECT) - val excludedPermissions: MutableList = mutableListOf(), - val lastLoginTime: LocalDateTime? = null -) : IModel +) : IModel { + @JsonProperty("permissions") + fun getFlattenedPermissions(): Iterable = 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 = mutableListOf(), - - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - @Fetch(FetchMode.SUBSELECT) - val excludedPermissions: MutableList = mutableListOf() + val permissions: MutableList = 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 = mutableListOf(), @OneToMany @JsonIgnore val employees: MutableList = 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 = 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 { /** Gets [EmployeePermission]s of the given [Employee]. */ fun Employee.getPermissions(): Iterable { val grantedPermissions: MutableSet = 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 } diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/services/AccountsService.kt b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/services/AccountsService.kt index 42e93c6..e3edb72 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/services/AccountsService.kt +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/core/services/AccountsService.kt @@ -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(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 { - 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 { + 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(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 { + 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()) } } diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/dao/EmployeeRepository.kt b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/dao/EmployeeRepository.kt index 9a6c240..03f1aaa 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/dao/EmployeeRepository.kt +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/dao/EmployeeRepository.kt @@ -10,7 +10,15 @@ interface EmployeeRepository : JpaRepository { fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean fun findByFirstNameAndLastName(firstName: String, lastName: String): Employee? + + fun findByGroup(group: EmployeeGroup): Collection + + fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: EmployeeGroup): Employee } @Repository -interface EmployeeGroupRepository : JpaRepository +interface EmployeeGroupRepository : JpaRepository { + fun existsByName(name: String): Boolean + + fun findByName(name: String): EmployeeGroup? +} diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountsControllers.kt similarity index 74% rename from src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountControllers.kt rename to src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountsControllers.kt index b276038..4339a11 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/rest/AccountsControllers.kt @@ -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(employeeService, EMPLOYEE_CONTROLLER_PATH) { @GetMapping("current") @ResponseStatus(HttpStatus.OK) - fun getCurrent(loggedInEmployee: Principal): ResponseEntity = ResponseEntity.ok(service.getById(loggedInEmployee.name.toLong(), false)) + fun getCurrent(loggedInEmployee: Principal): ResponseEntity = 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 = ResponseEntity.notFound().build() + @PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE]) + @ResponseStatus(HttpStatus.NO_CONTENT) + fun updatePassword(@PathVariable id: Long, @RequestBody password: String): ResponseEntity { + 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 { 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 { 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 { - 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 { - service.removeExcludedPermission(employeeId, excludedPermission) - return ResponseEntity - .noContent() - .build() + @GetMapping("logout") + @ResponseStatus(HttpStatus.OK) + fun logout(request: HttpServletRequest): ResponseEntity { + service.logout(request) + return ResponseEntity.ok().build() } } @@ -86,6 +82,24 @@ class EmployeeController(employeeService: EmployeeService) : @Profile("rest") class GroupsController(groupService: EmployeeGroupService) : AbstractRestModelController(groupService, EMPLOYEE_GROUP_CONTROLLER_PATH) { + @GetMapping("{id}/employees") + @ResponseStatus(HttpStatus.OK) + fun getEmployeesForGroup(@PathVariable id: Long): ResponseEntity> = ResponseEntity.ok(service.getEmployeesForGroup(id)) + + @PostMapping("default/{groupId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse): ResponseEntity { + service.setResponseDefaultGroup(groupId, response) + return ResponseEntity + .noContent() + .build() + } + + @GetMapping("default") + @ResponseStatus(HttpStatus.OK) + fun getRequestDefaultGroup(request: HttpServletRequest): ResponseEntity = + ResponseEntity.ok(service.getRequestDefaultGroup(request)) + @PutMapping("{groupId}/{employeeId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun addEmployeeToGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity {