diff --git a/.gitignore b/.gitignore index 1029410..8ed083d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ speed-measure-plugin*.json !.vscode/launch.json !.vscode/extensions.json .history/* +.vs/* # misc /.sass-cache diff --git a/angular.json b/angular.json index 6a2e7da..571e72c 100644 --- a/angular.json +++ b/angular.json @@ -21,7 +21,8 @@ "aot": true, "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + "src/silent-refresh.html" ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", diff --git a/package-lock.json b/package-lock.json index 8bb3015..e443e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2202,6 +2202,14 @@ "dev": true, "optional": true }, + "angular-oauth2-oidc": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-9.2.2.tgz", + "integrity": "sha512-aMQXeujzhubvxGw3ujw9FGwTC+L8m7CXnzVntpNJRkJsgMiuZFrXzgeiG87tvAE61J+PlOVIb/UkJjYDgDVU6Q==", + "requires": { + "js-sha256": "^0.9.0" + } + }, "ansi-colors": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -2754,7 +2762,8 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true }, "base64id": { "version": "2.0.0", @@ -7824,20 +7833,6 @@ "source-map-support": "^0.5.5" } }, - "keycloak-angular": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-7.3.1.tgz", - "integrity": "sha512-lU1ErzCOOmvwmfM0rAf6Nlf65CkG1EpnXpGpdaeViCUcJOdU+ChMcjez78Ekv5qUFkNZsNccs2m5xUaU/1bqlw==" - }, - "keycloak-js": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-10.0.2.tgz", - "integrity": "sha512-7nkg4Ob1khHGcNbuK36AMndKUEuIQFpNlWU9ygWs7nSBPCI9VZ8dJjjXfKJHm0ewgcqLFGPIJ6bxxRlfcQ6sLg==", - "requires": { - "base64-js": "1.3.1", - "js-sha256": "0.9.0" - } - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", diff --git a/package.json b/package.json index 943d64e..2a9d939 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "@angular/platform-browser": "~9.1.9", "@angular/platform-browser-dynamic": "~9.1.9", "@angular/router": "~9.1.9", - "keycloak-angular": "^7.3.1", - "keycloak-js": "^10.0.2", + "angular-oauth2-oidc": "^9.2.2", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index fdbafe1..f6bbc70 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,7 +5,7 @@ import { Routes } from '@angular/router'; import { HomeComponent } from './home/'; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from './auth/auth.guard'; import { paths_collaborateurs, paths_demandes_delegation, paths_demandes_formation, paths_ep, paths_saisie_ep, paths_formation, paths_home, paths_referents } from '@shared/utils/paths'; @@ -25,7 +25,7 @@ const routes: Routes = [ { path: paths_home.path, component: HomeComponent, - canActivate: [KeycloakGuard], + canActivate: [AuthGuard], pathMatch: 'full' }, //chargement des chemins du module collaborateur à partir du routing de ce module @@ -68,6 +68,6 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], - providers: [KeycloakGuard] + providers: [AuthGuard] }) export class AppRoutingModule {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5449ae7..71b1ef1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,8 +2,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule, DoBootstrap } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; -import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular'; -import { RouterModule } from '@angular/router'; import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; @@ -24,49 +22,26 @@ import { DemandesFormationModule } from './demandes-formation'; import { DemandesDelegationModule } from './demandes-delegation'; import { EpSaisieModule } from "./ep-saisie"; import { EpModule } from "./ep" +import { AuthModule } from './auth/auth.module'; -import { environment } from '@env'; - -/** - * constante Keycloak qui pourra être utilisé dans tout le projet. - */ -let keycloakService: KeycloakService = new KeycloakService(); - @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, - KeycloakAngularModule, AppRoutingModule, + AuthModule.forRoot(), AppRoutingModule, HttpClientModule, ApiModule, HomeModule, CollaborateursModule, ReferentsModule, FormationsModule, DemandesFormationModule, DemandesDelegationModule, EpSaisieModule, EpModule + ], - providers: [ - { - provide: KeycloakService, - useValue: keycloakService - } - ], - entryComponents: [AppComponent] + bootstrap: [AppComponent] }) -export class AppModule implements DoBootstrap { - //Configuration de la connexion avec Keycloak - async ngDoBootstrap(app) { - const { keycloakConfig } = environment; - - try { - await keycloakService.init({ config: keycloakConfig }); - app.bootstrap(AppComponent); - - } catch (error) { - console.error('Keycloak init failed', error); - } - } +export class AppModule { } diff --git a/src/app/auth/auth-config.ts b/src/app/auth/auth-config.ts new file mode 100644 index 0000000..8760184 --- /dev/null +++ b/src/app/auth/auth-config.ts @@ -0,0 +1,23 @@ +import { AuthConfig } from 'angular-oauth2-oidc'; +import { environment } from '@env'; + +/** + * Configuration du serveur Keycloak. + */ +export const authConfig: AuthConfig = { + issuer: environment.keycloakConfig.issuer, + clientId: environment.keycloakConfig.clientId, + dummyClientSecret: environment.keycloakConfig.dummyClientSecret, + responseType: environment.keycloakConfig.responseType, + redirectUri: environment.keycloakConfig.redirectUri, + silentRefreshRedirectUri: environment.keycloakConfig.silentRefreshRedirectUri, + scope: environment.keycloakConfig.scope, + useSilentRefresh: environment.keycloakConfig.useSilentRefresh, + silentRefreshTimeout: environment.keycloakConfig.silentRefreshTimeout, + timeoutFactor: environment.keycloakConfig.timeoutFactor, + sessionChecksEnabled: environment.keycloakConfig.sessionChecksEnabled, + showDebugInformation: environment.keycloakConfig.showDebugInformation, + clearHashAfterLogin: environment.keycloakConfig.clearHashAfterLogin, + nonceStateSeparator : environment.keycloakConfig.nonceStateSeparator +}; + diff --git a/src/app/auth/auth-module-config.ts b/src/app/auth/auth-module-config.ts new file mode 100644 index 0000000..a43d24d --- /dev/null +++ b/src/app/auth/auth-module-config.ts @@ -0,0 +1,11 @@ +import { OAuthModuleConfig } from 'angular-oauth2-oidc'; + +/** + * Liste des urls qui sont autorisées à recevoir l'acces_token. + */ +export const authModuleConfig: OAuthModuleConfig = { + resourceServer: { + allowedUrls: ['https://localhost:44393/api'], + sendAccessToken: true, + } +}; diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts new file mode 100644 index 0000000..03539d1 --- /dev/null +++ b/src/app/auth/auth.guard.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { AuthService } from './auth.service'; + +/** + * Guard permettant de gérer les autorisations au niveau des routes. + */ +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.authService.canActivateProtectedRoutes$.pipe(tap(isLoggin => this.login(isLoggin))); + } + + /** + * Affiche la page de connexion si l'utilisateur n'est pas connecté. + * @param isLoggin Booléen permettant de savoir si l'utilisateur est connecté ou non + */ + private login(isLoggin: boolean) { + if (!isLoggin) { + this.authService.login(); + } + } +} diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts new file mode 100644 index 0000000..3a26511 --- /dev/null +++ b/src/app/auth/auth.module.ts @@ -0,0 +1,61 @@ +import { HttpClientModule } from '@angular/common/http'; +import { ModuleWithProviders, NgModule, Optional, SkipSelf, APP_INITIALIZER } from '@angular/core'; +import { AuthConfig, OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc'; +import { authConfig } from './auth-config'; +import { AuthGuard } from './auth.guard'; +import { authModuleConfig } from './auth-module-config'; +import { AuthService } from './auth.service'; + +/** + * Nous avons besoin d'une usine de stockage car le localStorage + * n'est pas disponible au moment de la compilation de l'AOT. (Ahead-of-time) + */ +export function storageFactory(): OAuthStorage { + return localStorage; +} + +/** + * Exécute la méthode qui permet d'afficher la page de connexion. + * @param authService Service d'authentification + */ +export function init_app(authService: AuthService) { + return () => authService.runInitialLoginSequence(); +} + +/** + * Module d'authentification. + */ +@NgModule({ + imports: [ + HttpClientModule, + OAuthModule.forRoot(), + ], + providers: [ + AuthService, + AuthGuard, + ], +}) +export class AuthModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: AuthModule, + providers: [ + { provide: AuthConfig, useValue: authConfig }, + { provide: OAuthModuleConfig, useValue: authModuleConfig }, + { provide: OAuthStorage, useFactory: storageFactory }, + { + provide: APP_INITIALIZER, + useFactory: init_app, // Affiche la page de connexion au démarrage de l'application + deps: [ AuthService ], + multi: true + } + ] + }; + } + + constructor (@Optional() @SkipSelf() parentModule: AuthModule) { + if (parentModule) { + throw new Error("AuthModule est déjà chargé. Importez-le dans uniquement l'AppModule."); + } + } +} diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts new file mode 100644 index 0000000..f717520 --- /dev/null +++ b/src/app/auth/auth.service.spec.ts @@ -0,0 +1,6 @@ + +describe('AuthService', () => { + it('should pass', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts new file mode 100644 index 0000000..f32ee00 --- /dev/null +++ b/src/app/auth/auth.service.ts @@ -0,0 +1,263 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + + private isAuthenticatedSubject$ = new BehaviorSubject(false); + private isDoneLoadingSubject$ = new ReplaySubject(); + + /** + * Observable permettant de savoir si l'utilisateur est authentifié. + */ + public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); + + /** + * Observable permettant de savoir si le chargement de la connexion initiale est terminé. + */ + public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable(); + + /** + * Retourne "true" si et seulement si tous les appels asynchrones de la connexion initiale + * sont terminés (ou comportent une erreur), et si l'utilisateur + * a fini par être authentifié. + * + * En résumé, il combine: + * + * - Le dernier état connu indiquant si l'utilisateur est autorisé + * - Si les appels ajax pour la connexion initiale ont tous été effectués + */ + public canActivateProtectedRoutes$: Observable = combineLatest([ + this.isAuthenticated$, this.isDoneLoading$ ]).pipe(map(values => values.every(b => b))); + + /** + * Navigue vers la page de connexion. + */ + private navigateToLoginPage() { + // Pour naviguer vers un composant + //this.router.navigateByUrl('/should-login'); + + // Pour naviguer vers la page de connexion du server Keycloak + this.login(); + } + + /** + * Constructeur du service d'authentification. + * @param oauthService Service d'authentification de la librairie angular-oauth2-oidc + * @param router Service permettant de naviguer et de manipuler les urls + */ + constructor(private oauthService: OAuthService, private router: Router) { + + /* + // Utile pour le débogage: + this.oauthService.events.subscribe(event => { + if (event instanceof OAuthErrorEvent) { + console.error('OAuthErrorEvent Object:', event); + } else { + console.warn('OAuthEvent Object:', event); + } + }); + */ + + // Ajout un évènement afin de gérer l'acces_token dans le cas où l'application est ouverte dans deux onglets différents. + // TODO: Pour améliorer cette configuration. Voir: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2 + window.addEventListener('storage', (event) => { + // La propriété "key" à la valeur "null" si l'événement a été causé par la méthode ".clear()". + if (event.key !== 'access_token' && event.key !== null) { + return; + } + + console.warn("Changements remarqués dans l'access_token (très probablement depuis un autre onglet). Mise à jour de l'observable isAuthenticated.") ; + this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); + + // Si l'acces_token n'est pas valide, on redirige l'utilisateur vers la page de connexion + if (!this.oauthService.hasValidAccessToken()) { + this.navigateToLoginPage(); + } + }); + + // Met à jour l'observable lorsque l'access_token est valide + this.oauthService.events + .subscribe(_ => { + this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); + }); + + // Charge le profil de l'utilisateur lorsque le token est reçu + this.oauthService.events + .pipe(filter(e => ['token_received'].includes(e.type))) + .subscribe(e => this.oauthService.loadUserProfile()); + + // Redirige l'utilisateur vers la page de connexion lorsque la session est terminée ou lorsque qu'il y a une erreur + this.oauthService.events + .pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type))) + .subscribe(e => this.navigateToLoginPage()); + + // Mise en place d'un rafraîchissement silencieux lorsque le jeton est sur le point d'expirer + this.oauthService.setupAutomaticSilentRefresh(); + } + + /** + * Lance la séquence de connexion initiale. + */ + public runInitialLoginSequence(): Promise { + if (location.hash) { + console.log("Fragment de hachage rencontré, tracé sous forme de tableau...") ; + console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('='))); + } + + // 0. CONFIGURATION DU CHARGEMENT: + // Il faut d'abord vérifier comment le serveur Keycloak est actuellement configuré: + return this.oauthService.loadDiscoveryDocument() + + // 1. HASH LOGIN: + // Essaye de se connecter via le fragment de hachage + // après avoir été redirigé depuis le serveur Keycloak depuis initImplicitFlow : + .then(() => this.oauthService.tryLogin()) + + .then(() => { + if (this.oauthService.hasValidAccessToken()) { + return Promise.resolve(); + } + + // 2. SILENT LOGIN: + // Effectue un rafraîchissement silencieux pour le flux implicite. + // Utilise cette méthode pour obtenir des nouveaux tokens quand/avant que les tokens existants expirent. + return this.oauthService.silentRefresh() + .then(() => Promise.resolve()) + .catch(result => { + // Sous-ensemble de situations de https://openid.net/specs/openid-connect-core-1_0.html#AuthError + // Seulement celles où il est raisonnablement certain que l'envoi de l'utilisateur au serveur Keycloak est utile. + const errorResponsesRequiringUserInteraction = [ + 'interaction_required', + 'login_required', + 'account_selection_required', + 'consent_required', + ]; + + if (result + && result.reason + && errorResponsesRequiringUserInteraction.indexOf(result.reason.error) >= 0) { + + // 3. DEMANDE DE CONNEXION: + // A ce stade, nous savons avec certitude que nous devons demander à + // l'utilisateur de se connecter, nous le redirigeons donc vers la page de connexion. + + // Activez cette option pour TOUJOURS forcer un utilisateur à se connecter. + this.login(); + + // Afficher un mesage d'avertissement: + //console.warn("Une interaction de l'utilisateur est nécessaire pour se connecter, nous allons attendre que l'utilisateur se connecte manuellement."); + + return Promise.resolve(); + } + + return Promise.reject(result); + }); + }) + + .then(() => { + // Met à jour l'observable isDoneLoadingSubject + this.isDoneLoadingSubject$.next(true); + + // Vérifie les valeurs "undefined" et "null" pour être sûr. + // Notre login actuel ne devrait jamais avoir cela, mais au cas + // où quelqu'un appellerait la méthode initImplicitFlow(undefined|null), cela pourrait arriver. + if (this.oauthService.state && this.oauthService.state !== 'undefined' && this.oauthService.state !== 'null') { + let stateUrl = this.oauthService.state; + if (stateUrl.startsWith('/') === false) { + stateUrl = decodeURIComponent(stateUrl); + } + console.log(`Il y a eu l'état de ${this.oauthService.state}, donc nous vous envoyons à ${stateUrl}`) ; + this.router.navigateByUrl(stateUrl); + } + }) + .catch(() => this.isDoneLoadingSubject$.next(true)); // Met à jour l'observable isDoneLoadingSubject + } + + /** + * Affiche la page de connexion. + * @param targetUrl Url de destination + */ + public login(targetUrl?: string) { + // Note : avant la version 9.1.0 de la librairie, il fallait + // passer le composant encodageURIC dans les arguments de la méthode. + this.oauthService.initLoginFlow(targetUrl || this.router.url); + } + + /** + * Supprime tous les tokens et déconnecte l'utilisateur. + */ + public logout() { this.oauthService.logOut(); } + + /** + * Permet d'obtenir des nouveaux tokens quand/avant que les tokens existants expirent. + */ + public refresh() { this.oauthService.silentRefresh(); } + + /** + * Vérifie si l'access_token est valide. + */ + public hasValidToken() { return this.oauthService.hasValidAccessToken(); } + + /** + * Premier rôle dans la liste des rôles de l'utilisateur. + */ + public get firstRole() { + if(this.identityClaims != null) + return this.oauthService.getIdentityClaims()['roles'][0]; + else + return; + } + + /** + * Liste des rôles de l'utilisateur. + */ + public get roles() { + if(this.identityClaims != null) + return this.oauthService.getIdentityClaims()['roles']; + else + return; + } + + /** + * Nom et prénom de l'utilisateur. + */ + public get userInfo() { + if(this.identityClaims != null) + return this.oauthService.getIdentityClaims()['name']; + else + return; + } + + + // Normalement, un service comme celui-ci ne les expose pas, + // mais pour le débogage, c'est logique. + + /** + * Access_token actuel. + */ + public get accessToken() { return this.oauthService.getAccessToken(); } + + /** + * Refresh_Token actuel. + */ + public get refreshToken() { return this.oauthService.getRefreshToken(); } + + /** + * Claims concernant l'utilisateur. + */ + public get identityClaims() { return this.oauthService.getIdentityClaims(); } + + /** + * Id_token actuel. + */ + public get idToken() { return this.oauthService.getIdToken(); } + + /** + * Url de déconnexion. + */ + public get logoutUrl() { return this.oauthService.logoutUrl; } +} diff --git a/src/app/collaborateurs/collaborateurs.routing.module.ts b/src/app/collaborateurs/collaborateurs.routing.module.ts index 6b99fdb..6296c3b 100644 --- a/src/app/collaborateurs/collaborateurs.routing.module.ts +++ b/src/app/collaborateurs/collaborateurs.routing.module.ts @@ -9,17 +9,16 @@ import { EditEvaluationComponent } from './formations-collaborateur/edit-evaluat import { paths_collaborateurs } from '@shared/utils/paths'; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; - +import { AuthGuard } from 'app/auth/auth.guard'; /** * Routes du module collaborateur */ const routes: Routes = [ - { path:'', component: CollaborateursComponent, pathMatch: 'full', canActivate: [KeycloakGuard] }, - { path:paths_collaborateurs.formations, component: FormationsCollaboateurComponent, canActivate: [KeycloakGuard] }, - { path:paths_collaborateurs.evaluation, component: EvaluationComponent, canActivate: [KeycloakGuard] }, - { path:paths_collaborateurs.edit, component: EditEvaluationComponent, canActivate: [KeycloakGuard] }, - { path:paths_collaborateurs.get, component: DetailsCollaborateurComponent, canActivate: [KeycloakGuard] } + { path:'', component: CollaborateursComponent, pathMatch: 'full', canActivate: [AuthGuard] }, + { path:paths_collaborateurs.formations, component: FormationsCollaboateurComponent, canActivate: [AuthGuard] }, + { path:paths_collaborateurs.evaluation, component: EvaluationComponent, canActivate: [AuthGuard] }, + { path:paths_collaborateurs.edit, component: EditEvaluationComponent, canActivate: [AuthGuard] }, + { path:paths_collaborateurs.get, component: DetailsCollaborateurComponent, canActivate: [AuthGuard] } ]; diff --git a/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.html b/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.html index 8096815..7db1129 100644 --- a/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.html +++ b/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.html @@ -24,7 +24,7 @@ Référent - {{ row.referent.prenom }} {{ row.referent.nom }} + {{ getReferent(row.referent) }} diff --git a/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.ts b/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.ts index b550eeb..955024c 100644 --- a/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.ts +++ b/src/app/collaborateurs/details-collaborateur/details-collaborateur.component.ts @@ -9,7 +9,7 @@ import {MatSort} from '@angular/material/sort'; import { CollaborateursService, EpService } from "@shared/api-swagger/api/api"; -import { EpInformationDTO, CollaborateurDTO } from "@shared/api-swagger/model/models"; +import { EpInformationDTO, CollaborateurDTO, ReferentDTO } from "@shared/api-swagger/model/models"; /** * Composant pour gérer l'affichage des détails d'un collaborateur et de ses EP @@ -93,13 +93,18 @@ export class DetailsCollaborateurComponent implements OnInit { this.idCollaborateur = this.route.snapshot.paramMap.get('id'); this.collaborateurSubscription = this.collaborateusrService.getCollaborateurById(this.idCollaborateur).subscribe( collaborateur => { - this.collaborateur = collaborateur[0]; + this.collaborateur = collaborateur; this.updateEP(); }, err => console.log(err) ); } + getReferent(referent : ReferentDTO) { + if(referent == undefined || referent == null) { return "Referent indisponible"} + return referent.prenom + " "+ referent.nom; + } + /** * Mise à jour du tableau des EP lors d'un changement de page du tableau, du nombre d'élément à afficher ou d'un tri. * La fonction est aussi appelé au début du chargement et à l'utilisation de la barre de recherche. @@ -108,8 +113,12 @@ export class DetailsCollaborateurComponent implements OnInit { this.epSubscription = this.epService.getEPByCollaborateur(this.asc, this.idCollaborateur, this.numPage, this.parPage, undefined, this.search, this.tri).subscribe( ep => { - this.nbEP = ep.length; - this.dataSource = new MatTableDataSource(ep); + console.log(ep); + if(ep != null) { + + this.nbEP = ep.length; + this.dataSource = new MatTableDataSource(ep); + } }, err => console.log(err) ); diff --git a/src/app/demandes-delegation/demandes-delegation.routing.module.ts b/src/app/demandes-delegation/demandes-delegation.routing.module.ts index 5a0b7ae..d1009bb 100644 --- a/src/app/demandes-delegation/demandes-delegation.routing.module.ts +++ b/src/app/demandes-delegation/demandes-delegation.routing.module.ts @@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router'; import { DemandesDelegationComponent } from "./demandes-delegation.component"; import { DemandeDelegationComponent } from "./details-demande-delegation/demande-delegation.component"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; import { paths_demandes_delegation } from "@shared/utils/paths"; @@ -17,12 +17,12 @@ const routes: Routes = [ path:'', component: DemandesDelegationComponent, pathMatch: 'full', - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] }, { path: paths_demandes_delegation.get, component: DemandeDelegationComponent, - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] } ]; diff --git a/src/app/demandes-formation/demandes-formation.routing.module.ts b/src/app/demandes-formation/demandes-formation.routing.module.ts index 162dca1..801f8a2 100644 --- a/src/app/demandes-formation/demandes-formation.routing.module.ts +++ b/src/app/demandes-formation/demandes-formation.routing.module.ts @@ -6,7 +6,7 @@ import { DemandesFormationComponent } from "./demandes-formation.component"; import { DemandeFormationComponent } from "./details-demande-formation/demande-formation.component"; import { NewDemandeFormationComponent } from "./new-demande-formation/new-demande-formation.component"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; import { paths_demandes_formation } from "@shared/utils/paths"; @@ -14,9 +14,9 @@ import { paths_demandes_formation } from "@shared/utils/paths"; * Routes du module demandes formation */ const routes: Routes = [ - { path:'', component: DemandesFormationComponent, pathMatch: 'full', canActivate: [KeycloakGuard] }, - { path:paths_demandes_formation.new, component: NewDemandeFormationComponent, canActivate: [KeycloakGuard] }, - { path:paths_demandes_formation.get, component: DemandeFormationComponent, canActivate: [KeycloakGuard] } + { path:'', component: DemandesFormationComponent, pathMatch: 'full', canActivate: [AuthGuard] }, + { path:paths_demandes_formation.new, component: NewDemandeFormationComponent, canActivate: [AuthGuard] }, + { path:paths_demandes_formation.get, component: DemandeFormationComponent, canActivate: [AuthGuard] } ]; diff --git a/src/app/ep-saisie/ep-saisie.routing.module.ts b/src/app/ep-saisie/ep-saisie.routing.module.ts index 6842c1d..f509262 100644 --- a/src/app/ep-saisie/ep-saisie.routing.module.ts +++ b/src/app/ep-saisie/ep-saisie.routing.module.ts @@ -7,7 +7,7 @@ import { EpsSaisieComponent } from "./eps-saisie/eps-saisie.component"; import { EpaSaisieComponent } from "./epa-saisie/epa-saisie.component"; import { EpaSixAnsSaisieComponent } from "./epa-six-ans-saisie/epa-six-ans-saisie.component"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; import { paths_saisie_ep } from "@shared/utils/paths"; @@ -17,11 +17,11 @@ import { paths_saisie_ep } from "@shared/utils/paths"; const routes: Routes = [ { path:'', component: EpSaisieComponent, - canActivate: [KeycloakGuard], + canActivate: [AuthGuard], children: [ - { path:paths_saisie_ep.epa, component: EpaSaisieComponent, canActivate: [KeycloakGuard] }, - { path:paths_saisie_ep.eps, component: EpsSaisieComponent, canActivate: [KeycloakGuard] }, - { path:paths_saisie_ep.epa6ans, component: EpaSixAnsSaisieComponent, canActivate: [KeycloakGuard] } + { path:paths_saisie_ep.epa, component: EpaSaisieComponent, canActivate: [AuthGuard] }, + { path:paths_saisie_ep.eps, component: EpsSaisieComponent, canActivate: [AuthGuard] }, + { path:paths_saisie_ep.epa6ans, component: EpaSixAnsSaisieComponent, canActivate: [AuthGuard] } ] } ]; diff --git a/src/app/ep/ep.routing.module.ts b/src/app/ep/ep.routing.module.ts index ad3ceaa..80b51bd 100644 --- a/src/app/ep/ep.routing.module.ts +++ b/src/app/ep/ep.routing.module.ts @@ -18,7 +18,7 @@ import { EpCommentaireAssistantComponent } from "./ep-commentaire-assistant/ep-c import { EpCommentaireReferentComponent } from "./ep-commentaire-referent/ep-commentaire-referent.component"; import { NewParticipantComponent } from "./ep-participants/new-participant/new-participant.component"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; import { paths_ep } from "@shared/utils/paths"; /** @@ -28,31 +28,31 @@ const routes: Routes = [ { path:'', component: EpComponent, - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] }, { path:paths_ep.consultation, component: EpConsultationComponent, - canActivate: [KeycloakGuard], + canActivate: [AuthGuard], children: [ - {path:paths_ep.salaire, component: EpAugmentationSalaireComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.choixdate, component: EpChoixDateComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.demandedelegation, component: EpDemandeDelegationComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.demandesformation, component: EpDemandesFormationComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.participants, component: EpParticipantsComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.propositionsdates, component: EpPropositionsDatesComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.signature, component: EpSignatureComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.epa, component: EpaComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.eps, component: EpsComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.epa6ans, component: EpaSixAnsComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.assistant, component: EpCommentaireAssistantComponent, canActivate: [KeycloakGuard]}, - {path:paths_ep.referent, component: EpCommentaireReferentComponent, canActivate: [KeycloakGuard]} + {path:paths_ep.salaire, component: EpAugmentationSalaireComponent, canActivate: [AuthGuard]}, + {path:paths_ep.choixdate, component: EpChoixDateComponent, canActivate: [AuthGuard]}, + {path:paths_ep.demandedelegation, component: EpDemandeDelegationComponent, canActivate: [AuthGuard]}, + {path:paths_ep.demandesformation, component: EpDemandesFormationComponent, canActivate: [AuthGuard]}, + {path:paths_ep.participants, component: EpParticipantsComponent, canActivate: [AuthGuard]}, + {path:paths_ep.propositionsdates, component: EpPropositionsDatesComponent, canActivate: [AuthGuard]}, + {path:paths_ep.signature, component: EpSignatureComponent, canActivate: [AuthGuard]}, + {path:paths_ep.epa, component: EpaComponent, canActivate: [AuthGuard]}, + {path:paths_ep.eps, component: EpsComponent, canActivate: [AuthGuard]}, + {path:paths_ep.epa6ans, component: EpaSixAnsComponent, canActivate: [AuthGuard]}, + {path:paths_ep.assistant, component: EpCommentaireAssistantComponent, canActivate: [AuthGuard]}, + {path:paths_ep.referent, component: EpCommentaireReferentComponent, canActivate: [AuthGuard]} ] }, { path:paths_ep.newparticipant, component: NewParticipantComponent, - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] }, ]; diff --git a/src/app/formations/formations.routing.module.ts b/src/app/formations/formations.routing.module.ts index 60cb4b0..7bd2c64 100644 --- a/src/app/formations/formations.routing.module.ts +++ b/src/app/formations/formations.routing.module.ts @@ -7,17 +7,17 @@ import { FormationComponent } from "./details-formation/formation.component"; import { NewFormationComponent } from "./new-formation/new-formation.component"; import { EditFormationComponent } from "./edit-formation/edit-formation.component"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; import { paths_formation } from "@shared/utils/paths"; /** * Routes du module formation */ const routes: Routes = [ - { path:'', component: FormationsComponent, pathMatch: 'full', canActivate: [KeycloakGuard] }, - { path:paths_formation.edit, component: EditFormationComponent, canActivate: [KeycloakGuard] }, - { path:paths_formation.new, component: NewFormationComponent, canActivate: [KeycloakGuard] }, - { path:paths_formation.get, component: FormationComponent, canActivate: [KeycloakGuard] } + { path:'', component: FormationsComponent, pathMatch: 'full', canActivate: [AuthGuard] }, + { path:paths_formation.edit, component: EditFormationComponent, canActivate: [AuthGuard] }, + { path:paths_formation.new, component: NewFormationComponent, canActivate: [AuthGuard] }, + { path:paths_formation.get, component: FormationComponent, canActivate: [AuthGuard] } ]; diff --git a/src/app/home/home-assistante/home-assistante.component.ts b/src/app/home/home-assistante/home-assistante.component.ts index 99a4765..3daff7b 100644 --- a/src/app/home/home-assistante/home-assistante.component.ts +++ b/src/app/home/home-assistante/home-assistante.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit, OnDestroy, ViewChild, ViewChildren, AfterViewInit } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; import { Observable, Subscription } from 'rxjs'; import {MatTableDataSource} from '@angular/material/table'; @@ -9,6 +8,7 @@ import {MatSort} from '@angular/material/sort'; import { EpInformationDTO, CollaborateurDTO } from "@shared/api-swagger/model/models"; import { EpService } from "@shared/api-swagger/api/api"; +import { AuthService } from 'app/auth/auth.service'; /** @@ -88,7 +88,7 @@ export class HomeAssistanteComponent implements OnInit, AfterViewInit { */ chargement = true; - constructor(public keycloakService : KeycloakService, private service:EpService) { + constructor(public authService : AuthService, private service:EpService) { } /** diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 1e746a6..8f46a16 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import { AuthService } from 'app/auth/auth.service'; import { Role } from '@shared/utils/roles'; @@ -22,10 +22,9 @@ export class HomeComponent implements OnInit { * Le rôle de l'utilisateur. */ userRole : string; - constructor(private keycloakService : KeycloakService) { - let clientId = environment.keycloakConfig.clientId; - //récupérer les informations Keycloak de l'utilisateur - this.userRole = this.keycloakService.getKeycloakInstance().resourceAccess[clientId]["roles"][0]; + + constructor(private authService: AuthService) { + this.userRole = authService.firstRole; } ngOnInit() { diff --git a/src/app/referents/referents.routing.module.ts b/src/app/referents/referents.routing.module.ts index bc8ec31..3f59cc7 100644 --- a/src/app/referents/referents.routing.module.ts +++ b/src/app/referents/referents.routing.module.ts @@ -7,7 +7,7 @@ import { DetailsReferentComponent } from "./details-referent/details-referent.co import { paths_referents } from "@shared/utils/paths"; -import { KeycloakGuard } from '@shared/guards/keycloakguard'; +import { AuthGuard } from 'app/auth/auth.guard'; /** * Routes du module référents @@ -17,12 +17,12 @@ const routes: Routes = [ path:'', component: ReferentsComponent, pathMatch: 'full', - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] }, { path:paths_referents.get, component: DetailsReferentComponent, - canActivate: [KeycloakGuard] + canActivate: [AuthGuard] } ]; diff --git a/src/app/shared/guards/keycloakguard.ts b/src/app/shared/guards/keycloakguard.ts deleted file mode 100644 index f0aa3da..0000000 --- a/src/app/shared/guards/keycloakguard.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { KeycloakService, KeycloakAuthGuard } from 'keycloak-angular'; - - -/** - * KeycloakGuard est la classe qui va gérer l'authentification et les droits d'accès en fonction des rôles Keycloak. - */ -@Injectable() -export class KeycloakGuard extends KeycloakAuthGuard { - constructor(protected router: Router, protected keycloakAngular: KeycloakService) { - super(router, keycloakAngular); - } - - isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - return new Promise(async (resolve, reject) => { - if (!this.authenticated) { - this.keycloakAngular.login(); - return; - } - - const requiredRoles = route.data.roles; - if (!requiredRoles || requiredRoles.length === 0) { - return resolve(true); - } else { - if (!this.roles || this.roles.length === 0) { - resolve(false); - } - let granted: boolean = false; - for (const requiredRole of requiredRoles) { - if (this.roles.indexOf(requiredRole) > -1) { - granted = true; - break; - } - } - resolve(granted); - } - }); - } -} diff --git a/src/app/shared/nav-menu/nav-menu.component.ts b/src/app/shared/nav-menu/nav-menu.component.ts index 362d2b0..e412255 100644 --- a/src/app/shared/nav-menu/nav-menu.component.ts +++ b/src/app/shared/nav-menu/nav-menu.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import { AuthService } from 'app/auth/auth.service'; import { Role } from '@shared/utils/roles'; @@ -27,17 +27,16 @@ export class NavMenuComponent { * Les informations (nom+prénom) de l'utilisateur. */ userInfo : string; - constructor(private keycloakService : KeycloakService){ - let clientId = environment.keycloakConfig.clientId; - this.userRole = this.keycloakService.getKeycloakInstance().resourceAccess[clientId]["roles"][0]; - let profil = keycloakService.getKeycloakInstance().profile; - this.userInfo = profil.firstName+" "+profil.lastName; - } + constructor(private authService : AuthService){ + this.userRole = authService.firstRole; + this.userInfo = authService.userInfo; + } + //isExpanded = false; - async logout() { - await this.keycloakService.logout(); + logout() { + this.authService.logout(); } } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index d528f8d..5b7cede 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -2,13 +2,39 @@ // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. -import { KeycloakConfig } from 'keycloak-angular'; +import { AuthConfig } from 'angular-oauth2-oidc'; -// Add here your keycloak setup infos -const keycloakConfig: KeycloakConfig = { - url: 'http://localhost:8080/auth', - realm: 'Apside', - clientId: 'GestionEPA' +const keycloakConfig: AuthConfig = { + // Url of the Identity Provider + issuer: 'http://localhost:8080/auth/realms/Apside', + + // URL of the SPA to redirect the user to after login + redirectUri: 'http://localhost:4200', + + // The SPA's id. The SPA is registerd with this id at the auth-server + clientId: 'GestionEPA', + + // Just needed if your auth server demands a secret. In general, this + // is a sign that the auth server is not configured with SPAs in mind + // and it might not enforce further best practices vital for security + // such applications. + dummyClientSecret: 'f27746f4-e603-441e-a256-3ddd5b19ba54', + + responseType: 'code', + silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html' , + + // set the scope for the permissions the client should request + // The first four are defined by OIDC. + // Important: Request offline_access to get a refresh token + // The api scope is a usecase specific one + scope: 'openid profile email', + showDebugInformation: true, + useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes + silentRefreshTimeout: 5000, // For faster testing + timeoutFactor: 0.25, // For faster testing + sessionChecksEnabled: true, + clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, + nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by IdentityServer's URI encoding }; export const environment = { diff --git a/src/silent-refresh.html b/src/silent-refresh.html new file mode 100644 index 0000000..6adbc4e --- /dev/null +++ b/src/silent-refresh.html @@ -0,0 +1,31 @@ + + + + + + +