diff --git a/bizmatch/package.json b/bizmatch/package.json index eac3bef..99d3f7f 100644 --- a/bizmatch/package.json +++ b/bizmatch/package.json @@ -37,6 +37,7 @@ "dayjs": "^1.11.11", "express": "^4.18.2", "jwt-decode": "^4.0.0", + "keycloak-angular": "^15.2.1", "keycloak-js": "^24.0.4", "memoize-one": "^6.0.0", "on-change": "^5.0.1", @@ -66,4 +67,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.3.3" } -} \ No newline at end of file +} diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index 2a97a95..60767db 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, HostListener } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; import { ConfirmationService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; @@ -10,7 +11,6 @@ import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model' import build from '../build'; import { FooterComponent } from './components/footer/footer.component'; import { HeaderComponent } from './components/header/header.component'; -import { KeycloakService } from './services/keycloak.service'; import { LoadingService } from './services/loading.service'; import { UserService } from './services/user.service'; import { createDefaultListingCriteria } from './utils/utils'; diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index 96c388b..284649c 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -3,11 +3,11 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; +import { KeycloakService } from 'keycloak-angular'; import { environment } from '../environments/environment'; import { routes } from './app.routes'; import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { KeycloakInitializerService } from './services/keycloak-initializer.service'; -import { KeycloakService } from './services/keycloak.service'; import { SelectOptionsService } from './services/select-options.service'; // provideClientHydration() export const appConfig: ApplicationConfig = { diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index 6e83032..ba318e4 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -2,7 +2,7 @@ import { Routes } from '@angular/router'; import { LogoutComponent } from './components/logout/logout.component'; import { NotFoundComponent } from './components/not-found/not-found.component'; -import { KeycloakAuthGuard } from './guards/auth.guard'; +import { AuthGuard } from './guards/auth.guard'; import { ListingCategoryGuard } from './guards/listing-category.guard'; import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component'; import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component'; @@ -65,62 +65,62 @@ export const routes: Routes = [ { path: 'account', component: AccountComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, { path: 'account/:id', component: AccountComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // Create, Update Listings { path: 'editBusinessListing/:id', component: EditBusinessListingComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, { path: 'createBusinessListing', component: EditBusinessListingComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, { path: 'editCommercialPropertyListing/:id', component: EditCommercialPropertyListingComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, { path: 'createCommercialPropertyListing', component: EditCommercialPropertyListingComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // My Listings { path: 'myListings', component: MyListingComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // My Favorites { path: 'myFavorites', component: FavoritesComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // EMAil Us { path: 'emailUs', component: EmailUsComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // Logout { path: 'logout', component: LogoutComponent, - canActivate: [KeycloakAuthGuard], + canActivate: [AuthGuard], }, // ######### // Pricing diff --git a/bizmatch/src/app/components/footer/footer.component.ts b/bizmatch/src/app/components/footer/footer.component.ts index f7a1098..705d8d5 100644 --- a/bizmatch/src/app/components/footer/footer.component.ts +++ b/bizmatch/src/app/components/footer/footer.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; import { SidebarModule } from 'primeng/sidebar'; -import { KeycloakService } from '../../services/keycloak.service'; import { SharedModule } from '../../shared/shared/shared.module'; @Component({ selector: 'footer', diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index 4cb60df..c216e1f 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { faUserGear } from '@fortawesome/free-solid-svg-icons'; +import { KeycloakService } from 'keycloak-angular'; import { MenuItem } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { MenubarModule } from 'primeng/menubar'; @@ -10,7 +11,6 @@ import { TabMenuModule } from 'primeng/tabmenu'; import { Observable } from 'rxjs'; import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../environments/environment'; -import { KeycloakService } from '../../services/keycloak.service'; import { map2User } from '../../utils/utils'; @Component({ selector: 'header', diff --git a/bizmatch/src/app/components/logout/logout.component.ts b/bizmatch/src/app/components/logout/logout.component.ts index 70ba9ee..2224433 100644 --- a/bizmatch/src/app/components/logout/logout.component.ts +++ b/bizmatch/src/app/components/logout/logout.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { KeycloakService } from '../../services/keycloak.service'; +import { KeycloakService } from 'keycloak-angular'; @Component({ selector: 'logout', diff --git a/bizmatch/src/app/guards/auth.guard.ts b/bizmatch/src/app/guards/auth.guard.ts index 920829c..e20509e 100644 --- a/bizmatch/src/app/guards/auth.guard.ts +++ b/bizmatch/src/app/guards/auth.guard.ts @@ -1,105 +1,24 @@ -// import { inject } from '@angular/core'; -// import { CanMatchFn, Router, UrlTree } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Router, UrlTree } from '@angular/router'; +import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular'; +import { KeycloakInitializerService } from '../services/keycloak-initializer.service'; -// // Services -// import { KeycloakInitializerService } from '../services/keycloak-initializer.service'; -// import { KeycloakService } from '../services/keycloak.service'; -import { KeycloakService } from '../services/keycloak.service'; -import { createLogger } from '../utils/utils'; -const logger = createLogger('authGuard'); -// export const authGuard: CanMatchFn = async (route, segments): Promise => { -// const router = inject(Router); -// const keycloakService = inject(KeycloakService); -// const keycloakInitializer = inject(KeycloakInitializerService); -// if (!keycloakInitializer.isInitialized()) { -// await keycloakInitializer.initialize(); -// } -// logger.info('###-> calling isLoggedIn()'); -// const authenticated = keycloakService.isLoggedIn(); -// if (!authenticated) { -// console.log(window.location.origin); -// console.log(window.location.href); -// keycloakService.login({ -// redirectUri: `${window.location.origin}${segments['url']}`, -// }); -// } - -// // Get the user Keycloak roles and the required from the route -// const roles: string[] = keycloakService.getUserRoles(true); -// const requiredRoles = route.data?.['roles']; - -// // Allow the user to proceed if no additional roles are required to access the route -// if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) { -// return true; -// } - -// const authorized = requiredRoles.every(role => roles.includes(role)); - -// if (authorized) { -// return true; -// } - -// return router.createUrlTree(['/home']); -// }; -/** - * @license - * Copyright Mauricio Gemelli Vigolo and contributors. - * - * Use of this source code is governed by a MIT-style license that can be - * found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md - */ - -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; - -/** - * A simple guard implementation out of the box. This class should be inherited and - * implemented by the application. The only method that should be implemented is #isAccessAllowed. - * The reason for this is that the authorization flow is usually not unique, so in this way you will - * have more freedom to customize your authorization flow. - */ -export class KeycloakAuthGuard implements CanActivate { - /** - * Indicates if the user is authenticated or not. - */ - protected authenticated: boolean; - /** - * Roles of the logged user. It contains the clientId and realm user roles. - */ - protected roles: string[]; - - constructor(protected router: Router, protected keycloakAngular: KeycloakService) {} - - /** - * CanActivate checks if the user is logged in and get the full list of roles (REALM + CLIENT) - * of the logged user. This values are set to authenticated and roles params. - * - * @param route - * @param state - */ - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - try { - this.authenticated = await this.keycloakAngular.isLoggedIn(); - this.roles = await this.keycloakAngular.getUserRoles(true); - - return await this.isAccessAllowed(route, state); - } catch (error) { - throw new Error('An error happened during access validation. Details:' + error); - } +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard extends KeycloakAuthGuard { + constructor(protected override readonly router: Router, protected readonly keycloak: KeycloakService, private keycloakInitializer: KeycloakInitializerService) { + super(router, keycloak); } - /** - * Create your own customized authorization flow in this method. From here you already known - * if the user is authenticated (this.authenticated) and the user roles (this.roles). - * - * Return a UrlTree if the user should be redirected to another route. - * - * @param route - * @param state - */ - async isAccessAllowed(route, state): Promise { - if (!this.authenticated) { + async isAccessAllowed(): Promise { + if (!this.keycloakInitializer.isInitialized()) { + await this.keycloakInitializer.initialize(); + } + const authenticated = this.keycloak.isLoggedIn(); + if (!authenticated) { await this.router.navigate(['/home']); } - return this.authenticated; + return authenticated; } } diff --git a/bizmatch/src/app/models/keycloak-event.ts b/bizmatch/src/app/models/keycloak-event.ts deleted file mode 100644 index 2018175..0000000 --- a/bizmatch/src/app/models/keycloak-event.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Mauricio Gemelli Vigolo and contributors. - * - * Use of this source code is governed by a MIT-style license that can be - * found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md - */ - -/** - * Keycloak event types, as described at the keycloak-js documentation: - * https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events - */ -export enum KeycloakEventType { - /** - * Called if there was an error during authentication. - */ - OnAuthError, - /** - * Called if the user is logged out - * (will only be called if the session status iframe is enabled, or in Cordova mode). - */ - OnAuthLogout, - /** - * Called if there was an error while trying to refresh the token. - */ - OnAuthRefreshError, - /** - * Called when the token is refreshed. - */ - OnAuthRefreshSuccess, - /** - * Called when a user is successfully authenticated. - */ - OnAuthSuccess, - /** - * Called when the adapter is initialized. - */ - OnReady, - /** - * Called when the access token is expired. If a refresh token is available the token - * can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow) - * you can redirect to login screen to obtain a new access token. - */ - OnTokenExpired, - /** - * Called when a AIA has been requested by the application. - */ - OnActionUpdate, -} - -/** - * Structure of an event triggered by Keycloak, contains it's type - * and arguments (if any). - */ -export interface KeycloakEvent { - /** - * Event type as described at {@link KeycloakEventType}. - */ - type: KeycloakEventType; - /** - * Arguments from the keycloak-js event function. - */ - args?: unknown; -} diff --git a/bizmatch/src/app/models/keycloak-options.ts b/bizmatch/src/app/models/keycloak-options.ts deleted file mode 100644 index 2cd06f2..0000000 --- a/bizmatch/src/app/models/keycloak-options.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Copyright Mauricio Gemelli Vigolo and contributors. - * - * Use of this source code is governed by a MIT-style license that can be - * found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md - */ - -import { HttpRequest } from '@angular/common/http'; - -/** - * HTTP Methods - */ -export type HttpMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - -/** - * ExcludedUrl type may be used to specify the url and the HTTP method that - * should not be intercepted by the KeycloakBearerInterceptor. - * - * Example: - * const excludedUrl: ExcludedUrl[] = [ - * { - * url: 'reports/public' - * httpMethods: ['GET'] - * } - * ] - * - * In the example above for URL reports/public and HTTP Method GET the - * bearer will not be automatically added. - * - * If the url is informed but httpMethod is undefined, then the bearer - * will not be added for all HTTP Methods. - */ -export interface ExcludedUrl { - url: string; - httpMethods?: HttpMethods[]; -} - -/** - * Similar to ExcludedUrl, contains the HTTP methods and a regex to - * include the url patterns. - * This interface is used internally by the KeycloakService. - */ -export interface ExcludedUrlRegex { - urlPattern: RegExp; - httpMethods?: HttpMethods[]; -} - -/** - * keycloak-angular initialization options. - */ -export interface KeycloakOptions { - /** - * Configs to init the keycloak-js library. If undefined, will look for a keycloak.json file - * at root of the project. - * If not undefined, can be a string meaning the url to the keycloak.json file or an object - * of {@link Keycloak.KeycloakConfig}. Use this configuration if you want to specify the keycloak server, - * realm, clientId. This is usefull if you have different configurations for production, stage - * and development environments. Hint: Make use of Angular environment configuration. - */ - config?: string | Keycloak.KeycloakConfig; - /** - * Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself. - */ - initOptions?: Keycloak.KeycloakInitOptions; - /** - * By default all requests made by Angular HttpClient will be intercepted in order to - * add the bearer in the Authorization Http Header. However, if this is a not desired - * feature, the enableBearerInterceptor must be false. - * - * Briefly, if enableBearerInterceptor === false, the bearer will not be added - * to the authorization header. - * - * The default value is true. - */ - enableBearerInterceptor?: boolean; - /** - * Forces the execution of loadUserProfile after the keycloak initialization considering that the - * user logged in. - * This option is recommended if is desirable to have the user details at the beginning, - * so after the login, the loadUserProfile function will be called and its value cached. - * - * The default value is true. - */ - loadUserProfileAtStartUp?: boolean; - /** - * @deprecated - * String Array to exclude the urls that should not have the Authorization Header automatically - * added. This library makes use of Angular Http Interceptor, to automatically add the Bearer - * token to the request. - */ - bearerExcludedUrls?: (string | ExcludedUrl)[]; - /** - * This value will be used as the Authorization Http Header name. The default value is - * **Authorization**. If the backend expects requests to have a token in a different header, you - * should change this value, i.e: **JWT-Authorization**. This will result in a Http Header - * Authorization as "JWT-Authorization: bearer ". - */ - authorizationHeaderName?: string; - /** - * This value will be included in the Authorization Http Header param. The default value is - * **Bearer**, which will result in a Http Header Authorization as "Authorization: Bearer ". - * - * If any other value is needed by the backend in the authorization header, you should change this - * value. - * - * Warning: this value must be in compliance with the keycloak server instance and the adapter. - */ - bearerPrefix?: string; - /** - * This value will be used to determine whether or not the token needs to be updated. If the token - * will expire is fewer seconds than the updateMinValidity value, then it will be updated. - * - * The default value is 20. - */ - updateMinValidity?: number; - /** - * A function that will tell the KeycloakBearerInterceptor whether to add the token to the request - * or to leave the request as it is. If the returned value is `true`, the request will have the token - * present on it. If it is `false`, the token will be left off the request. - * - * The default is a function that always returns `true`. - */ - shouldAddToken?: (request: HttpRequest) => boolean; - /** - * A function that will tell the KeycloakBearerInterceptor if the token should be considered for - * updating as a part of the request being made. If the returned value is `true`, the request will - * check the token's expiry time and if it is less than the number of seconds configured by - * updateMinValidity then it will be updated before the request is made. If the returned value is - * false, the token will not be updated. - * - * The default is a function that always returns `true`. - */ - shouldUpdateToken?: (request: HttpRequest) => boolean; -} diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts index 6309e64..3ec3d42 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; import { MessageService } from 'primeng/api'; import { GalleriaModule } from 'primeng/galleria'; @@ -9,7 +10,6 @@ import { BusinessListing, User } from '../../../../../../bizmatch-server/src/mod import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; import { HistoryService } from '../../../services/history.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { ListingsService } from '../../../services/listings.service'; import { MailService } from '../../../services/mail.service'; import { SelectOptionsService } from '../../../services/select-options.service'; diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts index 3919dbc..b6cf51e 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; import { MessageService } from 'primeng/api'; import { GalleriaModule } from 'primeng/galleria'; @@ -9,7 +10,6 @@ import { CommercialPropertyListing, User } from '../../../../../../bizmatch-serv import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; import { HistoryService } from '../../../services/history.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { ListingsService } from '../../../services/listings.service'; import { MailService } from '../../../services/mail.service'; import { SelectOptionsService } from '../../../services/select-options.service'; diff --git a/bizmatch/src/app/pages/details/details-user/details-user.component.ts b/bizmatch/src/app/pages/details/details-user/details-user.component.ts index fe1a3d7..e690eec 100644 --- a/bizmatch/src/app/pages/details/details-user/details-user.component.ts +++ b/bizmatch/src/app/pages/details/details-user/details-user.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; import { MessageService } from 'primeng/api'; import { GalleriaModule } from 'primeng/galleria'; import { Observable } from 'rxjs'; @@ -9,7 +10,6 @@ import { KeycloakUser, ListingCriteria } from '../../../../../../bizmatch-server import { environment } from '../../../../environments/environment'; import { HistoryService } from '../../../services/history.service'; import { ImageService } from '../../../services/image.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { ListingsService } from '../../../services/listings.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; diff --git a/bizmatch/src/app/pages/home/home.component.ts b/bizmatch/src/app/pages/home/home.component.ts index e43f34e..d9f2fcc 100644 --- a/bizmatch/src/app/pages/home/home.component.ts +++ b/bizmatch/src/app/pages/home/home.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; @@ -9,7 +10,6 @@ import { DropdownModule } from 'primeng/dropdown'; import { InputTextModule } from 'primeng/inputtext'; import { StyleClassModule } from 'primeng/styleclass'; import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; -import { KeycloakService } from '../../services/keycloak.service'; import { ListingsService } from '../../services/listings.service'; import { SelectOptionsService } from '../../services/select-options.service'; import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../utils/utils'; diff --git a/bizmatch/src/app/pages/menu-account/menu-account.component.ts b/bizmatch/src/app/pages/menu-account/menu-account.component.ts index b2ad2fa..5394747 100644 --- a/bizmatch/src/app/pages/menu-account/menu-account.component.ts +++ b/bizmatch/src/app/pages/menu-account/menu-account.component.ts @@ -4,11 +4,11 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faEnvelope } from '@fortawesome/free-regular-svg-icons'; import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons'; +import { KeycloakService } from 'keycloak-angular'; import { ButtonModule } from 'primeng/button'; import { DividerModule } from 'primeng/divider'; import { RippleModule } from 'primeng/ripple'; import { StyleClassModule } from 'primeng/styleclass'; -import { KeycloakService } from '../../services/keycloak.service'; @Component({ selector: 'menu-account', diff --git a/bizmatch/src/app/pages/pricing/pricing.component.ts b/bizmatch/src/app/pages/pricing/pricing.component.ts index 946b18f..b41e54a 100644 --- a/bizmatch/src/app/pages/pricing/pricing.component.ts +++ b/bizmatch/src/app/pages/pricing/pricing.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { KeycloakService } from '../../services/keycloak.service'; +import { KeycloakService } from 'keycloak-angular'; import { SharedModule } from '../../shared/shared/shared.module'; @Component({ diff --git a/bizmatch/src/app/pages/subscription/account/account.component.ts b/bizmatch/src/app/pages/subscription/account/account.component.ts index e1b2f09..d4af3df 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.ts +++ b/bizmatch/src/app/pages/subscription/account/account.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { AngularCropperjsModule } from 'angular-cropperjs'; +import { KeycloakService } from 'keycloak-angular'; import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DialogModule } from 'primeng/dialog'; @@ -17,7 +18,6 @@ import { environment } from '../../../../environments/environment'; import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component'; import { GeoService } from '../../../services/geo.service'; import { ImageService } from '../../../services/image.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { LoadingService } from '../../../services/loading.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { SubscriptionsService } from '../../../services/subscriptions.service'; diff --git a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts index 67f3b18..97fa66a 100644 --- a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts @@ -9,6 +9,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { AngularCropperjsModule } from 'angular-cropperjs'; import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop'; +import { KeycloakService } from 'keycloak-angular'; import { ConfirmationService, MessageService } from 'primeng/api'; import { CarouselModule } from 'primeng/carousel'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; @@ -23,7 +24,6 @@ import { InputNumberModule } from '../../../components/inputnumber/inputnumber.c import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { GeoService } from '../../../services/geo.service'; import { ImageService } from '../../../services/image.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { LoadingService } from '../../../services/loading.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; diff --git a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts index aac2101..6cc2144 100644 --- a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts @@ -10,6 +10,7 @@ import { HttpEventType } from '@angular/common/http'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { AngularCropperjsModule } from 'angular-cropperjs'; import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop'; +import { KeycloakService } from 'keycloak-angular'; import { ConfirmationService, MessageService } from 'primeng/api'; import { CarouselModule } from 'primeng/carousel'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; @@ -26,7 +27,6 @@ import { InputNumberModule } from '../../../components/inputnumber/inputnumber.c import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { GeoService } from '../../../services/geo.service'; import { ImageService } from '../../../services/image.service'; -import { KeycloakService } from '../../../services/keycloak.service'; import { LoadingService } from '../../../services/loading.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; diff --git a/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts b/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts index 369f0e9..492de0b 100644 --- a/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts +++ b/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; import { KeycloakUser, ListingType } from '../../../../../../bizmatch-server/src/models/main.model'; -import { KeycloakService } from '../../../services/keycloak.service'; import { ListingsService } from '../../../services/listings.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { SharedModule } from '../../../shared/shared/shared.module'; diff --git a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts index fb3aed9..f41403a 100644 --- a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectorRef, Component } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; import { ConfirmationService, MessageService } from 'primeng/api'; import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model'; -import { KeycloakService } from '../../../services/keycloak.service'; import { ListingsService } from '../../../services/listings.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; diff --git a/bizmatch/src/app/services/keycloak-initializer.service.ts b/bizmatch/src/app/services/keycloak-initializer.service.ts index 4df1332..9c36990 100644 --- a/bizmatch/src/app/services/keycloak-initializer.service.ts +++ b/bizmatch/src/app/services/keycloak-initializer.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; import { environment } from '../../environments/environment'; import { createLogger } from '../utils/utils'; -import { KeycloakService } from './keycloak.service'; const logger = createLogger('KeycloakInitializerService'); @Injectable({ providedIn: 'root' }) export class KeycloakInitializerService { diff --git a/bizmatch/src/app/services/keycloak.service.ts b/bizmatch/src/app/services/keycloak.service.ts deleted file mode 100644 index f26bc0c..0000000 --- a/bizmatch/src/app/services/keycloak.service.ts +++ /dev/null @@ -1,541 +0,0 @@ -/** - * @license - * Copyright Mauricio Gemelli Vigolo and contributors. - * - * Use of this source code is governed by a MIT-style license that can be - * found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md - */ - -import { HttpHeaders, HttpRequest } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import Keycloak from 'keycloak-js'; -import { Subject, from } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { KeycloakEvent, KeycloakEventType } from '../models/keycloak-event'; -import { ExcludedUrl, ExcludedUrlRegex, KeycloakOptions } from '../models/keycloak-options'; - -/** - * Service to expose existent methods from the Keycloak JS adapter, adding new - * functionalities to improve the use of keycloak in Angular v > 4.3 applications. - * - * This class should be injected in the application bootstrap, so the same instance will be used - * along the web application. - */ -@Injectable() -export class KeycloakService { - /** - * Keycloak-js instance. - */ - private _instance: Keycloak.KeycloakInstance; - /** - * User profile as KeycloakProfile interface. - */ - private _userProfile: Keycloak.KeycloakProfile; - /** - * Flag to indicate if the bearer will not be added to the authorization header. - */ - private _enableBearerInterceptor: boolean; - /** - * When the implicit flow is choosen there must exist a silentRefresh, as there is - * no refresh token. - */ - private _silentRefresh: boolean; - /** - * Indicates that the user profile should be loaded at the keycloak initialization, - * just after the login. - */ - private _loadUserProfileAtStartUp: boolean; - /** - * The bearer prefix that will be appended to the Authorization Header. - */ - private _bearerPrefix: string; - /** - * Value that will be used as the Authorization Http Header name. - */ - private _authorizationHeaderName: string; - /** - * @deprecated - * The excluded urls patterns that must skip the KeycloakBearerInterceptor. - */ - private _excludedUrls: ExcludedUrlRegex[]; - /** - * Observer for the keycloak events - */ - private _keycloakEvents$: Subject = new Subject(); - /** - * The amount of required time remaining before expiry of the token before the token will be refreshed. - */ - private _updateMinValidity: number; - /** - * Returns true if the request should have the token added to the headers by the KeycloakBearerInterceptor. - */ - shouldAddToken: (request: HttpRequest) => boolean; - /** - * Returns true if the request being made should potentially update the token. - */ - shouldUpdateToken: (request: HttpRequest) => boolean; - - /** - * Binds the keycloak-js events to the keycloakEvents Subject - * which is a good way to monitor for changes, if needed. - * - * The keycloakEvents returns the keycloak-js event type and any - * argument if the source function provides any. - */ - private bindsKeycloakEvents(): void { - this._instance.onAuthError = errorData => { - this._keycloakEvents$.next({ - args: errorData, - type: KeycloakEventType.OnAuthError, - }); - }; - - this._instance.onAuthLogout = () => { - this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthLogout }); - }; - - this._instance.onAuthRefreshSuccess = () => { - this._keycloakEvents$.next({ - type: KeycloakEventType.OnAuthRefreshSuccess, - }); - }; - - this._instance.onAuthRefreshError = () => { - this._keycloakEvents$.next({ - type: KeycloakEventType.OnAuthRefreshError, - }); - }; - - this._instance.onAuthSuccess = () => { - this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthSuccess }); - }; - - this._instance.onTokenExpired = () => { - this._keycloakEvents$.next({ - type: KeycloakEventType.OnTokenExpired, - }); - }; - - this._instance.onActionUpdate = state => { - this._keycloakEvents$.next({ - args: state, - type: KeycloakEventType.OnActionUpdate, - }); - }; - - this._instance.onReady = authenticated => { - this._keycloakEvents$.next({ - args: authenticated, - type: KeycloakEventType.OnReady, - }); - }; - } - - /** - * Loads all bearerExcludedUrl content in a uniform type: ExcludedUrl, - * so it becomes easier to handle. - * - * @param bearerExcludedUrls array of strings or ExcludedUrl that includes - * the url and HttpMethod. - */ - private loadExcludedUrls(bearerExcludedUrls: (string | ExcludedUrl)[]): ExcludedUrlRegex[] { - const excludedUrls: ExcludedUrlRegex[] = []; - for (const item of bearerExcludedUrls) { - let excludedUrl: ExcludedUrlRegex; - if (typeof item === 'string') { - excludedUrl = { urlPattern: new RegExp(item, 'i'), httpMethods: [] }; - } else { - excludedUrl = { - urlPattern: new RegExp(item.url, 'i'), - httpMethods: item.httpMethods, - }; - } - excludedUrls.push(excludedUrl); - } - return excludedUrls; - } - - /** - * Handles the class values initialization. - * - * @param options - */ - private initServiceValues({ - enableBearerInterceptor = true, - loadUserProfileAtStartUp = false, - bearerExcludedUrls = [], - authorizationHeaderName = 'Authorization', - bearerPrefix = 'Bearer', - initOptions, - updateMinValidity = 20, - shouldAddToken = () => true, - shouldUpdateToken = () => true, - }: KeycloakOptions): void { - this._enableBearerInterceptor = enableBearerInterceptor; - this._loadUserProfileAtStartUp = loadUserProfileAtStartUp; - this._authorizationHeaderName = authorizationHeaderName; - this._bearerPrefix = bearerPrefix.trim().concat(' '); - this._excludedUrls = this.loadExcludedUrls(bearerExcludedUrls); - this._silentRefresh = initOptions ? initOptions.flow === 'implicit' : false; - this._updateMinValidity = updateMinValidity; - this.shouldAddToken = shouldAddToken; - this.shouldUpdateToken = shouldUpdateToken; - } - - /** - * Keycloak initialization. It should be called to initialize the adapter. - * Options is an object with 2 main parameters: config and initOptions. The first one - * will be used to create the Keycloak instance. The second one are options to initialize the - * keycloak instance. - * - * @param options - * Config: may be a string representing the keycloak URI or an object with the - * following content: - * - url: Keycloak json URL - * - realm: realm name - * - clientId: client id - * - * initOptions: - * Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself. - * - * enableBearerInterceptor: - * Flag to indicate if the bearer will added to the authorization header. - * - * loadUserProfileInStartUp: - * Indicates that the user profile should be loaded at the keycloak initialization, - * just after the login. - * - * bearerExcludedUrls: - * String Array to exclude the urls that should not have the Authorization Header automatically - * added. - * - * authorizationHeaderName: - * This value will be used as the Authorization Http Header name. - * - * bearerPrefix: - * This value will be included in the Authorization Http Header param. - * - * tokenUpdateExcludedHeaders: - * Array of Http Header key/value maps that should not trigger the token to be updated. - * - * updateMinValidity: - * This value determines if the token will be refreshed based on its expiration time. - * - * @returns - * A Promise with a boolean indicating if the initialization was successful. - */ - public async init(options: KeycloakOptions = {}) { - this.initServiceValues(options); - const { config, initOptions } = options; - - this._instance = new Keycloak(config); - this.bindsKeycloakEvents(); - - const authenticated = await this._instance.init(initOptions); - - if (authenticated && this._loadUserProfileAtStartUp) { - await this.loadUserProfile(); - } - - return authenticated; - } - - /** - * Redirects to login form on (options is an optional object with redirectUri and/or - * prompt fields). - * - * @param options - * Object, where: - * - redirectUri: Specifies the uri to redirect to after login. - * - prompt:By default the login screen is displayed if the user is not logged-in to Keycloak. - * To only authenticate to the application if the user is already logged-in and not display the - * login page if the user is not logged-in, set this option to none. To always require - * re-authentication and ignore SSO, set this option to login . - * - maxAge: Used just if user is already authenticated. Specifies maximum time since the - * authentication of user happened. If user is already authenticated for longer time than - * maxAge, the SSO is ignored and he will need to re-authenticate again. - * - loginHint: Used to pre-fill the username/email field on the login form. - * - action: If value is 'register' then user is redirected to registration page, otherwise to - * login page. - * - locale: Specifies the desired locale for the UI. - * @returns - * A void Promise if the login is successful and after the user profile loading. - */ - public async login(options: Keycloak.KeycloakLoginOptions = {}) { - await this._instance.login(options); - - if (this._loadUserProfileAtStartUp) { - await this.loadUserProfile(); - } - } - - /** - * Redirects to logout. - * - * @param redirectUri - * Specifies the uri to redirect to after logout. - * @returns - * A void Promise if the logout was successful, cleaning also the userProfile. - */ - public async logout(redirectUri?: string) { - const options = { - redirectUri, - }; - - await this._instance.logout(options); - this._userProfile = undefined; - } - - /** - * Redirects to registration form. Shortcut for login with option - * action = 'register'. Options are same as for the login method but 'action' is set to - * 'register'. - * - * @param options - * login options - * @returns - * A void Promise if the register flow was successful. - */ - public async register(options: Keycloak.KeycloakLoginOptions = { action: 'register' }) { - await this._instance.register(options); - } - - /** - * Check if the user has access to the specified role. It will look for roles in - * realm and the given resource, but will not check if the user is logged in for better performance. - * - * @param role - * role name - * @param resource - * resource name. If not specified, `clientId` is used - * @returns - * A boolean meaning if the user has the specified Role. - */ - isUserInRole(role: string, resource?: string): boolean { - let hasRole: boolean; - hasRole = this._instance.hasResourceRole(role, resource); - if (!hasRole) { - hasRole = this._instance.hasRealmRole(role); - } - return hasRole; - } - - /** - * Return the roles of the logged user. The realmRoles parameter, with default value - * true, will return the resource roles and realm roles associated with the logged user. If set to false - * it will only return the resource roles. The resource parameter, if specified, will return only resource roles - * associated with the given resource. - * - * @param realmRoles - * Set to false to exclude realm roles (only client roles) - * @param resource - * resource name If not specified, returns roles from all resources - * @returns - * Array of Roles associated with the logged user. - */ - getUserRoles(realmRoles: boolean = true, resource?: string): string[] { - let roles: string[] = []; - - if (this._instance.resourceAccess) { - Object.keys(this._instance.resourceAccess).forEach(key => { - if (resource && resource !== key) { - return; - } - - const resourceAccess = this._instance.resourceAccess[key]; - const clientRoles = resourceAccess['roles'] || []; - roles = roles.concat(clientRoles); - }); - } - - if (realmRoles && this._instance.realmAccess) { - const realmRoles = this._instance.realmAccess['roles'] || []; - roles.push(...realmRoles); - } - - return roles; - } - - /** - * Check if user is logged in. - * - * @returns - * A boolean that indicates if the user is logged in. - */ - isLoggedIn(): boolean { - if (!this._instance) { - return false; - } - - return this._instance.authenticated; - } - - /** - * Returns true if the token has less than minValidity seconds left before - * it expires. - * - * @param minValidity - * Seconds left. (minValidity) is optional. Default value is 0. - * @returns - * Boolean indicating if the token is expired. - */ - isTokenExpired(minValidity: number = 0): boolean { - return this._instance.isTokenExpired(minValidity); - } - - /** - * If the token expires within _updateMinValidity seconds the token is refreshed. If the - * session status iframe is enabled, the session status is also checked. - * Returns a promise telling if the token was refreshed or not. If the session is not active - * anymore, the promise is rejected. - * - * @param minValidity - * Seconds left. (minValidity is optional, if not specified updateMinValidity - default 20 is used) - * @returns - * Promise with a boolean indicating if the token was succesfully updated. - */ - public async updateToken(minValidity = this._updateMinValidity) { - // TODO: this is a workaround until the silent refresh (issue #43) - // is not implemented, avoiding the redirect loop. - if (this._silentRefresh) { - if (this.isTokenExpired()) { - throw new Error('Failed to refresh the token, or the session is expired'); - } - - return true; - } - - if (!this._instance) { - throw new Error('Keycloak Angular library is not initialized.'); - } - - try { - return await this._instance.updateToken(minValidity); - } catch (error) { - return false; - } - } - - /** - * Loads the user profile. - * Returns promise to set functions to be invoked if the profile was loaded - * successfully, or if the profile could not be loaded. - * - * @param forceReload - * If true will force the loadUserProfile even if its already loaded. - * @returns - * A promise with the KeycloakProfile data loaded. - */ - public async loadUserProfile(forceReload = false) { - if (this._userProfile && !forceReload) { - return this._userProfile; - } - - if (!this._instance.authenticated) { - throw new Error('The user profile was not loaded as the user is not logged in.'); - } - - return (this._userProfile = await this._instance.loadUserProfile()); - } - - /** - * Returns the authenticated token. - */ - public async getToken() { - return this._instance.token; - } - - /** - * Returns the logged username. - * - * @returns - * The logged username. - */ - public getUsername() { - if (!this._userProfile) { - throw new Error('User not logged in or user profile was not loaded.'); - } - - return this._userProfile.username; - } - - /** - * Clear authentication state, including tokens. This can be useful if application - * has detected the session was expired, for example if updating token fails. - * Invoking this results in onAuthLogout callback listener being invoked. - */ - clearToken(): void { - this._instance.clearToken(); - } - - /** - * Adds a valid token in header. The key & value format is: - * Authorization Bearer . - * If the headers param is undefined it will create the Angular headers object. - * - * @param headers - * Updated header with Authorization and Keycloak token. - * @returns - * An observable with with the HTTP Authorization header and the current token. - */ - public addTokenToHeader(headers: HttpHeaders = new HttpHeaders()) { - return from(this.getToken()).pipe(map(token => (token ? headers.set(this._authorizationHeaderName, this._bearerPrefix + token) : headers))); - } - - /** - * Returns the original Keycloak instance, if you need any customization that - * this Angular service does not support yet. Use with caution. - * - * @returns - * The KeycloakInstance from keycloak-js. - */ - getKeycloakInstance(): Keycloak.KeycloakInstance { - return this._instance; - } - - /** - * @deprecated - * Returns the excluded URLs that should not be considered by - * the http interceptor which automatically adds the authorization header in the Http Request. - * - * @returns - * The excluded urls that must not be intercepted by the KeycloakBearerInterceptor. - */ - get excludedUrls(): ExcludedUrlRegex[] { - return this._excludedUrls; - } - - /** - * Flag to indicate if the bearer will be added to the authorization header. - * - * @returns - * Returns if the bearer interceptor was set to be disabled. - */ - get enableBearerInterceptor(): boolean { - return this._enableBearerInterceptor; - } - - /** - * Keycloak subject to monitor the events triggered by keycloak-js. - * The following events as available (as described at keycloak docs - - * https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events): - * - OnAuthError - * - OnAuthLogout - * - OnAuthRefreshError - * - OnAuthRefreshSuccess - * - OnAuthSuccess - * - OnReady - * - OnTokenExpire - * In each occurrence of any of these, this subject will return the event type, - * described at {@link KeycloakEventType} enum and the function args from the keycloak-js - * if provided any. - * - * @returns - * A subject with the {@link KeycloakEvent} which describes the event type and attaches the - * function args. - */ - get keycloakEvents$(): Subject { - return this._keycloakEvents$; - } -}