UNPKG

@studiohyperdrive/ngx-auth

Version:

A library of core authentication functionality used with @studiohyperdrive/types-auth.

1,050 lines (1,030 loc) 44.2 kB
import { BehaviorSubject, distinctUntilChanged, map, filter, tap, combineLatest, switchMap, of, Subject, takeUntil } from 'rxjs'; import * as i0 from '@angular/core'; import { InjectionToken, inject, TemplateRef, ViewContainerRef, ChangeDetectorRef, Input, Directive, Pipe, Injectable } from '@angular/core'; import { tap as tap$1, takeUntil as takeUntil$1 } from 'rxjs/operators'; import { ActivatedRoute, Router, createUrlTreeFromSnapshot } from '@angular/router'; import { provideHttpClient, withInterceptors, HttpClient } from '@angular/common/http'; import clean from 'obj-clean'; /** * An abstract service used by the directives, guards and other components of @studiohyperdrive/ngx-auth * * @template AuthenticationResponseType - The type of authentication response * @template SignInDataType - The data type used to sign in a user * @template SignoutDataType - The data type used to sign out a user * @template SignOutResponseType - The data type you get when signing out a user */ class NgxAuthenticationAbstractService { constructor() { /** * A subject to store the authentication response if no other state implementation was provided */ this.authenticationResponseSubject = new BehaviorSubject(undefined); /** * A subject to store whether we've authenticated already */ this.authenticationStatusSubject = new BehaviorSubject('unset'); /** * A subject to store global features that are available for all users, regardless of their authenticated state */ this.globalFeaturesSubject = new BehaviorSubject([]); /** * Whether an authentication attempt has been made */ this.hasAuthenticated$ = this.authenticationStatusSubject.pipe(distinctUntilChanged(), map((status) => status !== 'unset')); /** * Whether the user is authenticated */ this.isAuthenticated$ = this.authenticationStatusSubject.pipe(distinctUntilChanged(), map((status) => status === 'signed-in')); } /** * Stores the authentication response in the state * * @param response - The authentication response */ storeAuthenticationResponse(response) { this.authenticationResponseSubject.next(response); } /** * Returns the authentication response from the state */ getAuthenticationResponse() { return this.authenticationResponseSubject.asObservable(); } /** * The authenticated user */ get user$() { return this.getAuthenticationResponse().pipe(filter(Boolean), map((response) => response.user), distinctUntilChanged()); } /** * The session of the authenticated user */ get session$() { return this.getAuthenticationResponse().pipe(filter(Boolean), map(({ session }) => session), distinctUntilChanged()); } /** * The metadata of the authenticated user */ get metadata$() { return this.getAuthenticationResponse().pipe(filter(Boolean), map(({ metadata }) => metadata), distinctUntilChanged()); } /** * Signs in a user and stores the authentication response * * @param signInData - The data needed to sign in a user */ signIn(signInData) { // Iben: Perform the call to sign in a user return this.signInUser(signInData).pipe(tap((response) => { // Iben: Set the user as signed in this.authenticationStatusSubject.next('signed-in'); // Iben: Store the authentication response this.storeAuthenticationResponse(response); }), // Iben: Convert to void map(() => undefined)); } /** * Signs out a user and removes the stored authentication response * * @param signoutDataType - Optional data needed to sign out a use */ signOut(signoutDataType) { // Iben: Perform the call to sign out a user return this.signOutUser(signoutDataType).pipe(tap(() => { // Iben: Set the user as signed out this.authenticationStatusSubject.next('signed-out'); // Iben: Remove the stored authentication response this.storeAuthenticationResponse(undefined); })); } /** * Returns whether the user has the required features. * * @param requiredFeatures - An array of required features * @param shouldHaveAllFeatures - Whether all features in the array are required, by default true */ hasFeature(requiredFeatures, shouldHaveAllFeatures = true) { // Iben: Get the session return combineLatest([this.getSession(), this.globalFeaturesSubject.asObservable()]).pipe(map(([{ features }, globalFeatures]) => { const sessionFeatures = new Set([...(features || []), ...(globalFeatures || [])]); // Iben: Return whether the user has the required features // We cast to strings here to make the typing work return shouldHaveAllFeatures ? requiredFeatures.every((feature) => sessionFeatures.has(`${feature}`)) : requiredFeatures.some((feature) => sessionFeatures.has(`${feature}`)); })); } /** * Sets a set of global features that are always present, regardless of the authenticated state of the user * * @param features - A list of features */ setGlobalFeatures(features) { this.globalFeaturesSubject.next(features); } /** * Returns whether the user has the required permissions. * * @param requiredPermissions - An array of required permissions * @param shouldHaveAllPermissions - Whether all permissions in the array are required, by default true */ hasPermission(requiredPermissions, shouldHaveAllPermissions = true) { // Iben: Get the session return this.getSession().pipe(filter(Boolean), map(({ permissions }) => { const sessionPermissions = new Set([...permissions]); // Iben: Return whether the user has the required permissions return shouldHaveAllPermissions ? requiredPermissions.every((permission) => sessionPermissions.has(permission)) : requiredPermissions.some((permission) => sessionPermissions.has(permission)); })); } /** * Returns a session or an empty session depending on the authenticated state */ getSession() { return this.isAuthenticated$.pipe(switchMap((isAuthenticated) => { // Iben: If the user is authenticated, we return the session, if not, we return an empty version for the hasPermission and hasFeature methods // This ensures we always get a response return isAuthenticated ? this.session$ : of({ features: [], permissions: [], }); })); } } /** * Converts an single item to an array or returns the array as is. This is to help the guards, directives and pipes in this feature due to the typing in the abstract service */ const convertToArray = (item) => { return Array.isArray(item) ? item : [item]; }; /** * A token to provide the necessary service to the directives/guard */ const NgxAuthenticationServiceToken = new InjectionToken('NgxAuthenticationServiceToken'); /** * A token to provide the necessary urlHandler to the NgxAuthenticatedHttpClient */ const NgxAuthenticationUrlHandlerToken = new InjectionToken('NgxAuthenticationUrlHandlerToken'); /** * A token to provide the necessary handler to the NgxAuthenticatedHttpInterceptor */ const NgxAuthenticationInterceptorToken = new InjectionToken('NgxAuthenticationInterceptorToken'); /** * A directive that will render a part of the template based on whether the required feature(s) are provided. * * Based upon `*ngIf`. See https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_if.ts */ //TODO: Iben: Implement Cypress/PlayWright tests class NgxHasFeatureDirective { /** * A feature or list of features the item should have */ set ngxHasFeature(feature) { this.feature = feature; this.updateView(); } /** * The else template in case the feature is not enabled */ set ngxHasFeatureElse(ngTemplate) { this.elseTemplateRef = ngTemplate; this.elseViewRef = null; this.updateView(); } /** * Whether the feature should be enabled, by default this is true */ set ngxHasFeatureShouldHaveFeature(shouldHaveFeatureEnabled) { this.shouldHaveFeature = shouldHaveFeatureEnabled; this.updateView(); } /** * Whether all features should be enabled, by default this is true */ set ngxHasFeatureShouldHaveAllFeatures(shouldHaveAllFeatures) { this.shouldHaveAllFeatures = shouldHaveAllFeatures; this.updateView(); } constructor() { this.templateRef = inject(TemplateRef); this.viewContainer = inject(ViewContainerRef); this.authenticationService = inject(NgxAuthenticationServiceToken); this.cdRef = inject(ChangeDetectorRef); /** * The needed templateRefs */ this.thenTemplateRef = null; this.thenViewRef = null; this.elseTemplateRef = null; this.elseViewRef = null; /** * The (list of) feature(s) we need to check */ this.feature = []; /** * Whether the feature should be enabled */ this.shouldHaveFeature = true; /** * Whether all features should be enabled */ this.shouldHaveAllFeatures = true; const templateRef = this.templateRef; this.thenTemplateRef = templateRef; } ngOnDestroy() { this.dispose(); } /** * Updates the view and hides/renders the template as needed */ updateView() { // Iben: Dispose the current subscription this.dispose(); // Iben: Create a new onDestroyed handler this.destroyed$ = new Subject(); // Iben: Render the views based on the correct state this.authenticationService .hasFeature(convertToArray(this.feature), this.shouldHaveAllFeatures) .pipe(tap((hasFeature) => { // Iben: Clear the current view this.viewContainer.clear(); // Iben: Check if we should render the view const shouldRender = this.shouldHaveFeature ? hasFeature : !hasFeature; // Iben: Render the correct templates if (shouldRender) { this.viewContainer.clear(); this.elseViewRef = null; if (this.thenTemplateRef) { this.thenViewRef = this.viewContainer.createEmbeddedView(this.thenTemplateRef); } } else { if (!this.elseViewRef) { this.viewContainer.clear(); this.thenViewRef = null; if (this.elseTemplateRef) { this.elseViewRef = this.viewContainer.createEmbeddedView(this.elseTemplateRef); } } } // Iben: Detect the changes so that the view gets updated this.cdRef.detectChanges(); }), takeUntil(this.destroyed$)) .subscribe(); } /** * Dispose the current subscription */ dispose() { if (this.destroyed$) { this.destroyed$.next(); this.destroyed$.complete(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasFeatureDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.8", type: NgxHasFeatureDirective, isStandalone: true, selector: "[ngxHasFeature]", inputs: { ngxHasFeature: "ngxHasFeature", ngxHasFeatureElse: "ngxHasFeatureElse", ngxHasFeatureShouldHaveFeature: "ngxHasFeatureShouldHaveFeature", ngxHasFeatureShouldHaveAllFeatures: "ngxHasFeatureShouldHaveAllFeatures" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasFeatureDirective, decorators: [{ type: Directive, args: [{ selector: '[ngxHasFeature]', }] }], ctorParameters: () => [], propDecorators: { ngxHasFeature: [{ type: Input }], ngxHasFeatureElse: [{ type: Input }], ngxHasFeatureShouldHaveFeature: [{ type: Input }], ngxHasFeatureShouldHaveAllFeatures: [{ type: Input }] } }); /** * A directive that will render a part of the template based on whether the required permissions(s) are provided. * * Based upon `*ngIf`. See https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_if.ts */ //TODO: Iben: Implement Cypress/PlayWright tests class NgxHasPermissionDirective { /** * A permission or list of permissions the item should have */ set ngxHasPermission(permission) { this.permission = permission; this.updateView(); } /** * The else template in case the permission is not enabled */ set ngxHasPermissionElse(ngTemplate) { this.elseTemplateRef = ngTemplate; this.elseViewRef = null; this.updateView(); } /** * Whether the permission should be enabled, by default this is true */ set ngxHasPermissionShouldHavePermission(shouldHavePermissionEnabled) { this.shouldHavePermission = shouldHavePermissionEnabled; this.updateView(); } /** * Whether all permissions should be enabled, by default this is true */ set ngxHasPermissionShouldHaveAllPermissions(shouldHaveAllPermissions) { this.shouldHaveAllPermissions = shouldHaveAllPermissions; this.updateView(); } constructor() { this.viewContainer = inject(ViewContainerRef); this.authenticationService = inject(NgxAuthenticationServiceToken); this.cdRef = inject(ChangeDetectorRef); /** * The needed templateRefs */ this.thenTemplateRef = null; this.thenViewRef = null; this.elseTemplateRef = null; this.elseViewRef = null; /** * The (list of) permission(s) we need to check */ this.permission = []; /** * Whether the permission should be enabled */ this.shouldHavePermission = true; /** * Whether all permissions should be enabled */ this.shouldHaveAllPermissions = true; const templateRef = inject(TemplateRef); this.thenTemplateRef = templateRef; } ngOnDestroy() { this.dispose(); } /** * Updates the view and hides/renders the template as needed */ updateView() { // Iben: Dispose the current subscription this.dispose(); // Iben: Create a new onDestroyed handler this.destroyed$ = new Subject(); // Iben: Render the views based on the correct state this.authenticationService .hasPermission(convertToArray(this.permission), this.shouldHaveAllPermissions) .pipe(tap((hasPermission) => { // Iben: Clear the current view this.viewContainer.clear(); // Iben: Check if we should render the view const shouldRender = this.shouldHavePermission ? hasPermission : !hasPermission; // Iben: Render the correct templates if (shouldRender) { this.viewContainer.clear(); this.elseViewRef = null; if (this.thenTemplateRef) { this.thenViewRef = this.viewContainer.createEmbeddedView(this.thenTemplateRef); } } else { if (!this.elseViewRef) { this.viewContainer.clear(); this.thenViewRef = null; if (this.elseTemplateRef) { this.elseViewRef = this.viewContainer.createEmbeddedView(this.elseTemplateRef); } } } // Iben: Detect the changes so that the view gets updated this.cdRef.detectChanges(); }), takeUntil(this.destroyed$)) .subscribe(); } /** * Dispose the current subscription */ dispose() { if (this.destroyed$) { this.destroyed$.next(); this.destroyed$.complete(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasPermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.8", type: NgxHasPermissionDirective, isStandalone: true, selector: "[ngxHasPermission]", inputs: { ngxHasPermission: "ngxHasPermission", ngxHasPermissionElse: "ngxHasPermissionElse", ngxHasPermissionShouldHavePermission: "ngxHasPermissionShouldHavePermission", ngxHasPermissionShouldHaveAllPermissions: "ngxHasPermissionShouldHaveAllPermissions" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasPermissionDirective, decorators: [{ type: Directive, args: [{ selector: '[ngxHasPermission]', }] }], ctorParameters: () => [], propDecorators: { ngxHasPermission: [{ type: Input }], ngxHasPermissionElse: [{ type: Input }], ngxHasPermissionShouldHavePermission: [{ type: Input }], ngxHasPermissionShouldHaveAllPermissions: [{ type: Input }] } }); /** * * A directive that will render a part of the template based on whether the user is authenticated. * * Based upon `*ngIf`. See https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_if.ts */ class NgxIsAuthenticatedDirective { constructor() { this.authenticationService = inject(NgxAuthenticationServiceToken); this.viewContainer = inject(ViewContainerRef); /** * The needed templateRefs */ this.thenTemplateRef = null; this.thenViewRef = null; this.elseTemplateRef = null; this.elseViewRef = null; /** * Whether the user has to be authenticated */ this.shouldBeAuthenticated = true; const templateRef = inject(TemplateRef); this.thenTemplateRef = templateRef; } /** * Whether the user has to be authenticated */ set ngxIsAuthenticated(authenticated) { this.shouldBeAuthenticated = authenticated; this.updateView(); } /** * The else template in case the condition is not matched */ set ngxIsAuthenticatedElse(ngTemplate) { this.elseTemplateRef = ngTemplate; this.elseViewRef = null; this.updateView(); } ngOnDestroy() { this.dispose(); } updateView() { // Iben: Dispose the current subscription this.dispose(); // Iben: Create a new onDestroyed handler this.destroyed$ = new Subject(); // Iben: Render the views based on the correct state this.authenticationService.isAuthenticated$ .pipe(tap$1((isAuthenticated) => { // Iben: Check if we should render the view if ((isAuthenticated && this.shouldBeAuthenticated) || (!isAuthenticated && !this.shouldBeAuthenticated)) { if (!this.thenViewRef) { this.viewContainer.clear(); this.elseViewRef = null; if (this.thenTemplateRef) { this.thenViewRef = this.viewContainer.createEmbeddedView(this.thenTemplateRef); } } } else { if (!this.elseViewRef) { this.viewContainer.clear(); this.thenViewRef = null; if (this.elseTemplateRef) { this.elseViewRef = this.viewContainer.createEmbeddedView(this.elseTemplateRef); } } } }), takeUntil$1(this.destroyed$)) .subscribe(); } /** * Dispose the current subscription */ dispose() { if (this.destroyed$) { this.destroyed$.next(); this.destroyed$.complete(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxIsAuthenticatedDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.8", type: NgxIsAuthenticatedDirective, isStandalone: true, selector: "[ngxIsAuthenticated]", inputs: { ngxIsAuthenticated: "ngxIsAuthenticated", ngxIsAuthenticatedElse: "ngxIsAuthenticatedElse" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxIsAuthenticatedDirective, decorators: [{ type: Directive, args: [{ selector: '[ngxIsAuthenticated]', }] }], ctorParameters: () => [], propDecorators: { ngxIsAuthenticated: [{ type: Input }], ngxIsAuthenticatedElse: [{ type: Input }] } }); /** * Check if we can route to this route based on the provided feature * * @param routeSnapshot - The provided route snapshot */ const NgxHasFeatureGuard = (routeSnapshot) => { // Iben: Fetch all injectables const authenticationService = inject(NgxAuthenticationServiceToken); const route = inject(ActivatedRoute); const router = inject(Router); // Iben: Check if the feature is enabled for the environment const snapshot = routeSnapshot; const feature = snapshot.data?.feature; const allFeatures = snapshot.data?.shouldHaveAllFeatures === undefined ? true : snapshot.data?.shouldHaveAllFeatures; // Wouter: The path to redirect to when the right conditions are met. const redirectTo = snapshot.data?.redirect; /** * Whether we should navigate away if the feature exists. * * If this is set to `true`, a redirect path has been provided, and the feature is enabled, we navigate away * from the route this guard is set upon. * * If this is set to `false`, the feature flag must be enabled. If not, the guard will not allow for the route * this guard is set upon to be navigated to and will redirect to either the provided paths or the home page. * * Default value is `false`. */ const shouldNavigateOnFeature = Boolean(snapshot.data?.shouldNavigateOnFeature); // Iben: Early exit if there's no feature provided if (!feature) { return of(true); } // Wouter: Early exit if we should navigate when the feature is enabled but no navigation was provided if (shouldNavigateOnFeature && !redirectTo.length) { return of(true); } // Iben: If there's a feature provided, we check if we have the feature return combineLatest([ authenticationService.isAuthenticated$, authenticationService.hasFeature(convertToArray(feature), allFeatures), ]).pipe(filter(([featuresHaveBeenSet]) => featuresHaveBeenSet), tap(([, canNavigate]) => { // Wouter: Continue if we should navigate when the FF is enabled if (shouldNavigateOnFeature) { // Wouter: Continue only if the feature is enabled if (canNavigate) { // Wouter: Snapshot is needed to navigate relatively. return router.navigateByUrl(createUrlTreeFromSnapshot(routeSnapshot, redirectTo)); } } // Wouter: Continue if we should navigate when the feature is disabled // Wouter: If the feature is enabled, we shouldn't redirect. if (canNavigate) { return true; } // Iben: Redirect if the feature is disabled return router.navigate([...redirectTo], { relativeTo: route, }); }), // Wouter: If we should navigate when the feature exists, the guard returns false as we have already navigated away. // If we should allow this guard's route when the feature exists, we can safely do so. map(([, canNavigate]) => (shouldNavigateOnFeature ? !canNavigate : canNavigate))); }; /** * Check if we can route to this route based on the provided permission * * @param routeSnapshot - The provided route snapshot */ const NgxHasPermissionGuard = (routeSnapshot) => { // Iben: Fetch all injectables const authenticationService = inject(NgxAuthenticationServiceToken); const route = inject(ActivatedRoute); const router = inject(Router); // Iben: Check if the permission is enabled for the environment const snapshot = routeSnapshot; const permission = snapshot.data?.permission; const allPermissions = snapshot.data?.shouldHaveAllPermissions === undefined ? true : snapshot.data?.shouldHaveAllPermissions; // Wouter: The path to redirect to when the right conditions are met. const redirectTo = snapshot.data?.redirect; /** * Whether we should navigate away if the permission exists. * * If this is set to `true`, a redirect path has been provided, and the permission is enabled, we navigate away * from the route this guard is set upon. * * If this is set to `false`, the permission flag must be enabled. If not, the guard will not allow for the route * this guard is set upon to be navigated to and will redirect to either the provided paths or the home page. * * Default value is `false`. */ const shouldNavigateOnPermission = Boolean(snapshot.data?.shouldNavigateOnPermission); // Iben: Early exit if there's no permission provided if (!permission) { return of(true); } // Wouter: Early exit if we should navigate when the permission is enabled but no navigation was provided if (shouldNavigateOnPermission && !redirectTo.length) { return of(true); } // Iben: If there's a permission provided, we check if we have the permission return combineLatest([ authenticationService.isAuthenticated$, authenticationService.hasPermission(convertToArray(permission), allPermissions), ]).pipe(filter(([permissionsHaveBeenSet]) => permissionsHaveBeenSet), tap(([, canNavigate]) => { // Wouter: Continue if we should navigate when the FF is enabled if (shouldNavigateOnPermission) { // Wouter: Continue only if the permission is enabled if (canNavigate) { // Wouter: Snapshot is needed to navigate relatively. return router.navigateByUrl(createUrlTreeFromSnapshot(routeSnapshot, redirectTo)); } } // Wouter: Continue if we should navigate when the permission is disabled // Wouter: If the permission is enabled, we shouldn't redirect. if (canNavigate) { return true; } // Iben: Redirect if the permission is disabled return router.navigate([...redirectTo], { relativeTo: route, }); }), // Wouter: If we should navigate when the permission exists, the guard returns false as we have already navigated away. // If we should allow this guard's route when the permission exists, we can safely do so. map(([, canNavigate]) => (shouldNavigateOnPermission ? !canNavigate : canNavigate))); }; /** * Check if we can route to this route based on the provided permission * * @param routeSnapshot - The provided route snapshot */ const NgxIsAuthenticatedGuard = (routeSnapshot) => { // Iben: Fetch all injectables const authenticationService = inject(NgxAuthenticationServiceToken); const route = inject(ActivatedRoute); const router = inject(Router); // Iben: Check if the permission is enabled for the environment const snapshot = routeSnapshot; // Wouter: The path to redirect to when the right conditions are met. const redirectTo = snapshot.data?.redirect; /** * Whether we should navigate away if the user is not authenticated * Default value is `false`. */ const shouldBeAuthenticated = Boolean(snapshot.data?.shouldBeAuthenticated); // Iben: If there's a permission provided, we check if we have the permission return authenticationService.isAuthenticated$.pipe(map((isAuthenticated) => { // Wouter: Continue if we are authenticated and we should be authenticated or vice-versa if ((isAuthenticated && shouldBeAuthenticated) || (!isAuthenticated && !shouldBeAuthenticated)) { return true; } // Iben: Redirect if the previous check has failed router.navigate([...redirectTo], { relativeTo: route, }); return false; })); }; /** * A pipe that returns whether a (list of) feature(s) has been provided */ class NgxHasFeaturePipe { constructor() { this.authenticationService = inject(NgxAuthenticationServiceToken); this.cdRef = inject(ChangeDetectorRef); const cdRef = this.cdRef; // Iben: Use instance of cdRef like this to prevent memory leaks (see Angular async Pipe implementation) this.changeDetectorRef = cdRef; } ngOnDestroy() { // Iben: Call the dispose when the component is destroyed so we have no running subscriptions left this.dispose(); // Iben: Clear instance of cdRef like this to prevent memory leaks (see Angular async Pipe implementation) this.changeDetectorRef = null; } /** * Returns whether or not a feature is provided for the environment * * @param feature - The provided feature */ transform(feature) { this.subscribe(this.authenticationService.hasFeature(convertToArray(feature))); return this.hasFeature; } /** * Handles the changeDetection, latest value and dispose of the hasFeature observable * * @param observable - The hasFeature observable */ subscribe(observable) { // Iben: Dispose the current subscription this.dispose(); // Iben: Create a new destroyed subject to handle the destruction when needed this.destroyed$ = new Subject(); observable .pipe(tap((value) => { // Iben: Update the latest value when it a new value is provided this.hasFeature = value; // Iben: Mark the component as ready for check this.changeDetectorRef.markForCheck(); }), takeUntil(this.destroyed$)) .subscribe(); } /** * Dispose of the feature observable when existing */ dispose() { // Iben: In case there's a destroyed, we have an observable and we destroy the subscription and reset the observable if (this.destroyed$) { this.destroyed$.next(); this.destroyed$.complete(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasFeaturePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.8", ngImport: i0, type: NgxHasFeaturePipe, isStandalone: true, name: "ngxHasFeature", pure: false }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasFeaturePipe, decorators: [{ type: Pipe, args: [{ name: 'ngxHasFeature', pure: false, }] }], ctorParameters: () => [] }); /** * A pipe that returns whether a (list of) permission(s) has been provided */ class NgxHasPermissionPipe { constructor() { this.authenticationService = inject(NgxAuthenticationServiceToken); this.cdRef = inject(ChangeDetectorRef); const cdRef = this.cdRef; // Iben: Use instance of cdRef like this to prevent memory leaks (see Angular async Pipe implementation) this.changeDetectorRef = cdRef; } ngOnDestroy() { // Iben: Call the dispose when the component is destroyed so we have no running subscriptions left this.dispose(); // Iben: Clear instance of cdRef like this to prevent memory leaks (see Angular async Pipe implementation) this.changeDetectorRef = null; } /** * Returns whether or not a permission is provided for the environment * * @param permission - The provided permission */ transform(permission) { this.subscribe(this.authenticationService.hasPermission(convertToArray(permission))); return this.hasPermission; } /** * Handles the changeDetection, latest value and dispose of the hasPermission observable * * @param observable - The hasPermission observable */ subscribe(observable) { // Iben: Dispose the current subscription this.dispose(); // Iben: Create a new destroyed subject to handle the destruction when needed this.destroyed$ = new Subject(); observable .pipe(tap((value) => { // Iben: Update the latest value when it a new value is provided this.hasPermission = value; // Iben: Mark the component as ready for check this.changeDetectorRef.markForCheck(); }), takeUntil(this.destroyed$)) .subscribe(); } /** * Dispose of the permission observable when existing */ dispose() { // Iben: In case there's a destroyed, we have an observable and we destroy the subscription and reset the observable if (this.destroyed$) { this.destroyed$.next(); this.destroyed$.complete(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasPermissionPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.8", ngImport: i0, type: NgxHasPermissionPipe, isStandalone: true, name: "ngxHasPermission", pure: false }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxHasPermissionPipe, decorators: [{ type: Pipe, args: [{ name: 'ngxHasPermission', pure: false, }] }], ctorParameters: () => [] }); /** * An interceptor that will handle any request that needs to be authenticated * * @param request - The provided request * @param next - The HttpHandler */ function NgxAuthenticatedHttpInterceptor(request, next) { // Iben: Get the authenticatedCallHandler const authenticatedCallHandler = inject(NgxAuthenticationInterceptorToken); // Iben: If the request does not need to be made in an authenticated state or if no authenticatedCallHandler was provided, we return the request as is if (!request.withCredentials || !authenticatedCallHandler) { return next(request); } // Iben: Handle the authenticated call return next(authenticatedCallHandler(request)); } /** * Configures the provided implementation of the NgxAuthenticationAbstract service to the application * * @param configuration - The configuration with the provided service implementation */ const provideNgxAuthenticationConfiguration = (configuration) => { return [ { provide: NgxAuthenticationServiceToken, useExisting: configuration.service, }, // Iben: If the HttpClientConfiguration is provided, we assume the user wants to use the NgxAuthenticatedHttpClient ...(!configuration.httpClientConfiguration ? [] : [ { provide: NgxAuthenticationUrlHandlerToken, useValue: configuration.httpClientConfiguration.baseUrl, }, { provide: NgxAuthenticationInterceptorToken, useValue: configuration.httpClientConfiguration.authenticatedCallHandler, }, provideHttpClient(withInterceptors([ NgxAuthenticatedHttpInterceptor, ...(configuration.httpClientConfiguration.interceptors || []), ])), ]), ]; }; /** * Returns a mock version of the authentication service * * @param configuration - The configuration of the mock */ const NgxAuthenticationServiceMock = (configuration) => { return { hasFeature: configuration.hasFeatureSpy, hasPermission: configuration.hasPermissionSpy, signIn: configuration.signInSpy, signOut: configuration.signOutSpy, isAuthenticated$: configuration.hasAuthenticated .asObservable() .pipe(map((status) => status === 'signed-in')), hasAuthenticated$: configuration.hasAuthenticated .asObservable() .pipe(map((status) => status !== 'unset')), user$: configuration.authenticationResponse .asObservable() .pipe(map((response) => response?.user)), session$: configuration.authenticationResponse .asObservable() .pipe(map((response) => response?.session)), metadata$: configuration.authenticationResponse .asObservable() .pipe(map((response) => response?.metadata)), }; }; /** * Returns a mock authentication response */ const NgxAuthenticationResponseMock = { user: { name: 'Test', }, session: { features: ['A'], permissions: ['User'], }, metadata: { requestPassword: true, }, }; const NgxMockAuthenticatedHttpClient = (configuration) => { const defaultFunction = () => of(); return { get: configuration.get || defaultFunction, download: configuration.download || defaultFunction, delete: configuration.delete || defaultFunction, patch: configuration.patch || defaultFunction, post: configuration.post || defaultFunction, put: configuration.put || defaultFunction, }; }; /** * An opinionated wrapper of the HttpClient providing easy ways to make authenticated and unauthenticated calls */ class NgxAuthenticatedHttpClient { constructor() { this.httpClient = inject(HttpClient); const baseUrlHandler = inject(NgxAuthenticationUrlHandlerToken); // Iben: Setup the base url this.baseUrl = baseUrlHandler ? baseUrlHandler() : ''; } /** * Adds a base-url to every request * @param {string} url - The url of the request */ handleUrl(url) { return `${this.baseUrl}/${url}`; } /** * Constructs a GET request to the provided API * * @param url - The url of the API * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ get(url, params, withCredentials = true, context) { return this.httpClient.get(this.handleUrl(url), clean({ withCredentials, params, context })); } /** * Constructs a GET request tailored to downloading to the provided API * * @param url - The url of the API * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ download(url, params, withCredentials = true, context) { return this.httpClient .get(this.handleUrl(url), clean({ withCredentials, params, responseType: 'blob', observe: 'response', context, })) .pipe(map((response) => { return { fileType: response.headers.get('content-disposition').split('.')[1], blob: response.body, }; })); } /** * Constructs a DELETE request to the provided API * * @param url - The url of the API * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ delete(url, params, withCredentials = true, context) { return this.httpClient.delete(this.handleUrl(url), clean({ params, withCredentials, context })); } /** * Constructs a POST request to the provided API * * @param url - The url of the API * @param body - The body we wish to send * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ post(url, body, params, withCredentials = true, context) { return this.httpClient.post(this.handleUrl(url), body, clean({ params, withCredentials, context })); } /** * Constructs a PUT request to the provided API * * @param url - The url of the API * @param body - The body we wish to send * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ put(url, body, params, withCredentials = true, context) { return this.httpClient.put(this.handleUrl(url), body, clean({ params, withCredentials, context })); } /** * Constructs a PATCH request to the provided API * * @param url - The url of the API * @param body - The body we wish to send * @param params - An optional set of params we wish to send to the API * @param withCredentials - Whether the call is made by an authenticated user, by default true * @param context - An optional HTTPContext */ patch(url, body, params, withCredentials = true, context) { return this.httpClient.patch(this.handleUrl(url), body, clean({ params, withCredentials, context })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxAuthenticatedHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxAuthenticatedHttpClient, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NgxAuthenticatedHttpClient, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { NgxAuthenticatedHttpClient, NgxAuthenticationAbstractService, NgxAuthenticationResponseMock, NgxAuthenticationServiceMock, NgxHasFeatureDirective, NgxHasFeatureGuard, NgxHasFeaturePipe, NgxHasPermissionDirective, NgxHasPermissionGuard, NgxHasPermissionPipe, NgxIsAuthenticatedDirective, NgxIsAuthenticatedGuard, NgxMockAuthenticatedHttpClient, provideNgxAuthenticationConfiguration }; //# sourceMappingURL=studiohyperdrive-ngx-auth.mjs.map