git.psu.edu will be upgraded to critical security release 13.7.4 Monday, 11/18 between 9 and 10pm. Please email support@git.psu.edu if you have trouble with anything Gitlab-related. Please see the git.psu.edu Yammer group for more information.

Commit caaa4eac authored by Shane Eckenrode's avatar Shane Eckenrode

Merge branch 'develop' into 'master'

Develop

See merge request !39
parents 9766a141 05aaa99e
Pipeline #112786 passed with stages
in 3 minutes and 16 seconds
......@@ -3,6 +3,6 @@ include:
ref: master
file: eio-swe-angular-library/.gitlab-ci.yml
variables:
SONAR_ENABLED: "false"
SONAR_ENABLED: "true"
DEMO_ENABLED: "false"
DIST_LIB: dist/libs/utils
......@@ -337,34 +337,6 @@
}
}
},
"utils-loading-events": {
"projectType": "library",
"root": "libs/utils/loading-events",
"sourceRoot": "libs/utils/loading-events/src",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/loading-events/tsconfig.lib.json", "libs/utils/loading-events/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/loading-events/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/loading-events/jest.config.js",
"tsConfig": "libs/utils/loading-events/tsconfig.spec.json",
"setupFile": "libs/utils/loading-events/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-ngrx": {
"projectType": "library",
"root": "libs/utils/ngrx",
......@@ -476,6 +448,34 @@
"styleext": "scss"
}
}
},
"utils-security-ngrx": {
"projectType": "library",
"root": "libs/utils/security-ngrx",
"sourceRoot": "libs/utils/security-ngrx/src",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/security-ngrx/tsconfig.lib.json", "libs/utils/security-ngrx/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/security-ngrx/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/security-ngrx/jest.config.js",
"tsConfig": "libs/utils/security-ngrx/tsconfig.spec.json",
"setupFile": "libs/utils/security-ngrx/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
}
},
"cli": {
......
......@@ -30,3 +30,7 @@ let obj = {
```
## yes-no pipe
## external-link directive
Usage `<a utExternalLink href="">`. Will expand to `<a utExternalLink target="_blank" rel="noopener noreferrer" href="">`
......@@ -3,6 +3,8 @@ export * from './lib/before-unload/before-unload.module';
export * from './lib/caps-lock/caps-lock.directive';
export * from './lib/caps-lock/caps-lock.module';
export * from './lib/coalescing-component-factory-resolver/coalescing-component-factory.resolver';
export * from './lib/external-link/external-link.directive';
export * from './lib/external-link/external-link.module';
export * from './lib/ng-let/ng-let.directive';
export * from './lib/ng-let/ng-let.module';
export * from './lib/safe-pipe/safe-pipe.module';
......
import { Component } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ExternalLinkDirective } from './external-link.directive';
@Component({
template: '<a utExternalLink href="https://google.com">Google</a>'
})
class TestComponent {}
describe('ExternalLinkDirective', () => {
describe('driver component', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, ExternalLinkDirective]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create driver component', () => {
expect(component).toBeDefined();
});
it('should add target and rel attributes', () => {
const el = fixture.debugElement.query(By.directive(ExternalLinkDirective));
expect(el.componentInstance).toBeDefined();
expect(el.attributes['target']).toEqual('_blank');
expect(el.attributes['rel']).toEqual('noopener noreferrer');
});
});
});
import { Directive, ElementRef, OnInit, Renderer2 } from '@angular/core';
@Directive({
selector: '[utExternalLink]'
})
export class ExternalLinkDirective implements OnInit {
constructor(private el: ElementRef, private renderer: Renderer2) {}
public ngOnInit(): void {
this.renderer.setAttribute(this.el.nativeElement, 'target', '_blank');
this.renderer.setAttribute(this.el.nativeElement, 'rel', 'noopener noreferrer');
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { LoggerModule } from '@psu/utils/logger';
import { LoadingEvents } from './loading-events';
import { CommonModule } from '@angular/common';
import { ExternalLinkDirective } from './external-link.directive';
@NgModule({
imports: [CommonModule, LoggerModule],
providers: [LoadingEvents]
declarations: [ExternalLinkDirective],
imports: [CommonModule],
exports: [ExternalLinkDirective]
})
export class LoadingEventsModule {}
export class ExternalLinkModule {}
import { isPlatformBrowser } from '@angular/common';
import { AfterViewInit, Directive, ElementRef, Inject, PLATFORM_ID } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, Directive, ElementRef, Inject, PLATFORM_ID } from '@angular/core';
@Directive({
selector: '[utAutoFocus]'
})
export class FocusDirective implements AfterViewInit {
constructor(private element: ElementRef, @Inject(PLATFORM_ID) private platform: Object) {}
constructor(
private element: ElementRef,
@Inject(PLATFORM_ID) private platform: Object,
private changeDetectorRef: ChangeDetectorRef
) {}
public ngAfterViewInit(): void {
if (isPlatformBrowser(this.platform)) {
this.element.nativeElement.focus();
this.changeDetectorRef.detectChanges();
}
}
}
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { Logger } from '@psu/utils/logger';
@Injectable({ providedIn: 'root' })
export class LoadingEvents {
private doc: Document;
private isAppReady: boolean;
constructor(@Inject(DOCUMENT) doc: any) {
constructor(@Inject(DOCUMENT) doc: any, private log: Logger) {
this.doc = doc;
this.isAppReady = false;
}
......@@ -15,9 +16,8 @@ export class LoadingEvents {
if (this.isAppReady) {
return;
}
const bubbles = true;
const cancelable = false;
this.doc.dispatchEvent(this.createEvent('appready', bubbles, cancelable));
this.log.debug('appready event triggered');
this.doc.dispatchEvent(this.createEvent('appready', true, false));
this.isAppReady = true;
}
......
import { Directive, Input } from '@angular/core';
import { Directive, Input, OnChanges, Self, SimpleChanges } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[utDisableControl]'
})
export class DisableControlDirective {
constructor(private ngControl: NgControl) {}
export class DisableControlDirective implements OnChanges {
constructor(@Self() private ngControl: NgControl) {}
@Input('utDisableControl')
public set disableControl(condition: boolean) {
const action = !!condition ? 'disable' : 'enable';
this.ngControl.control[action]();
public disableControl: boolean;
public ngOnChanges(changes: SimpleChanges): void {
if (changes && changes['disableControl']) {
const action = !!this.disableControl ? 'disable' : 'enable';
this.ngControl.control[action]();
}
}
}
# @psu/utils/loading-events
This library is used to dispatch a `CustomEvent` to the DOM when the application
is done bootstrapping. Our index.html files show a loading banner outside of Angular.
This library is used to communicate that the app is ready to load, which fades out
the loading banner and fades in the application.
## Running unit tests
Run `nx test utils-loading-events` to execute the unit tests.
export { LoadingEvents } from './lib/loading-events';
export { LoadingEventsModule } from './lib/loading-events.module';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { Logger } from '@psu/utils/logger';
@Injectable()
export class LoadingEvents {
private doc: Document;
private isAppReady: boolean;
constructor(@Inject(DOCUMENT) doc: any, private log: Logger) {
this.doc = doc;
this.isAppReady = false;
}
public triggerAppReady(): void {
if (this.isAppReady) {
return;
}
this.log.debug('appready event triggered');
this.doc.dispatchEvent(this.createEvent('appready', true, false));
this.isAppReady = true;
}
private createEvent(eventType: string, bubbles: boolean, cancelable: boolean): Event {
const customEvent: any = new CustomEvent(eventType, {
bubbles,
cancelable
});
return customEvent;
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { LoadingEventsModule } from '@psu/utils/loading-events';
import { LoggerModule } from '@psu/utils/logger';
import { PropertiesService } from './properties.service';
@NgModule({
imports: [CommonModule, LoggerModule, LoadingEventsModule],
imports: [CommonModule, LoggerModule],
providers: [PropertiesService]
})
export class PropertiesModule {}
import { HttpRequest } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { async, TestBed } from '@angular/core/testing';
import { NO_CACHE_HEADER } from '@psu/utils/browser';
import { LoadingEvents } from '@psu/utils/loading-events';
import { LoadingEvents, NO_CACHE_HEADER } from '@psu/utils/browser';
import { Logger } from '@psu/utils/logger';
import * as mockdate from 'mockdate';
import { Mock } from 'ts-mocks';
......
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpBackend, HttpClient, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable, Injector, Optional } from '@angular/core';
import { NO_CACHE_HEADER } from '@psu/utils/browser';
import { LoadingEvents } from '@psu/utils/loading-events';
import { LoadingEvents, NO_CACHE_HEADER } from '@psu/utils/browser';
import { Logger } from '@psu/utils/logger';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
......@@ -21,14 +20,19 @@ export class PropertiesService<T> {
public _properties: T;
private httpClient: HttpClient;
constructor(
private log: Logger,
private httpClient: HttpClient,
httpBackend: HttpBackend,
private injector: Injector,
@Optional()
@Inject(PROPERTIES_CONFIG)
private config: PropertiesConfig
) {}
) {
// NOTE: This bypasses all HTTP interceptors, by design.
this.httpClient = new HttpClient(httpBackend);
}
public load(appPropertyKey: string): Promise<T> {
const path = `${this.getBasePath()}${appPropertyKey}${propertiesExtension}`;
......@@ -37,12 +41,9 @@ export class PropertiesService<T> {
// params: {
// d: new Date().getTime().toString()
// },
headers: new HttpHeaders().append(NO_CACHE_HEADER, 'true')
headers: new HttpHeaders().append(NO_CACHE_HEADER, 'true'),
})
.pipe(
tap(this.extractData.bind(this)),
catchError(this.handleError.bind(this))
)
.pipe(tap(this.extractData.bind(this)), catchError(this.handleError.bind(this)))
.toPromise();
}
......
import { Directive, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectorRef, Directive, HostBinding, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { ResponsiveService } from './responsive.service';
......@@ -19,7 +19,7 @@ export class ResponsiveDirective implements OnInit, OnDestroy {
private destroyed$ = new Subject<any>();
constructor(private responsiveService: ResponsiveService) {}
constructor(private responsiveService: ResponsiveService, @SkipSelf() protected cdRef: ChangeDetectorRef) {}
public ngOnInit(): void {
this.responsiveService.currentScreenSize$
......@@ -32,6 +32,7 @@ export class ResponsiveDirective implements OnInit, OnDestroy {
this.isSmallTablet = size === ScreenSize.TABLET_SM;
this.isTablet = size === ScreenSize.TABLET;
this.isDesktop = size === ScreenSize.DESKTOP;
this.cdRef.markForCheck();
});
}
......
# @psu/utils/security-ngrx
NgRx extensions for @psu/utils/security.
## Class Interfaces
This library makes use of class interfaces for dependency injection of vendor-specific authentication functionality.
External libraries such as @psu/msal-oidc will provide implementation of these interfaces.
### SecurityEventListener
`SecurityEventListener` defines an interface for a service that handles authentication related events that should
trigger NgRx actions.
### Providers
This library exports a helper function to allow you to provide implementations for these class interfaces -
`provideSecurityEventListener()`.
```
providers: [
...,
VendorSecurityEventListener,
provideSecurityEventListener(VendorSecurityEventListener),
]
```
## Facade
The `SecurityFacade` abstracts away most details of the NgRx implementation and provides helper functions and properties
for apps to use. It injects the `SecurityEventListener` and listens for login success, failure, etc. events, and
then dispatches corresponding actions against the store.
module.exports = {
name: 'utils-loading-events',
name: 'utils-security-ngrx',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/loading-events',
coverageDirectory: '../../../coverage/libs/utils/security-ngrx',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
......
export * from './lib/providers';
export * from './lib/security-event.listener';
export * from './lib/security-ngrx.module';
export * from './lib/security.actions';
export * from './lib/security.facade';
export * from './lib/security.selectors';
import { Provider, Type } from '@angular/core';
import { SecurityEventListener } from './security-event.listener';
export function provideSecurityEventListener(impl: Type<SecurityEventListener>): Provider {
return { provide: SecurityEventListener, useExisting: impl };
}
import { User } from '@psu/utils/security';
import { Observable } from 'rxjs';
/**
* This is a "class interface" for a security event service.
*
* This allows our authentication implementation (oauth2-server, azure, hydra, etc) to be
* decoupled from the internal logic in this library.
*/
export abstract class SecurityEventListener {
/**
* An observable of type User that should emit whenever a user has succesfully logged in.
*/
readonly loginSuccessful$: Observable<User>;
/**
* An observable of type string that should emit whenever a user has failed to log in.
*/
readonly loginFailure$: Observable<string>;
}
import { async, TestBed } from '@angular/core/testing';
import { LoadingEventsModule } from './loading-events.module';
import { SecurityNgrxModule } from './security-ngrx.module';
describe('LoadingEventsModule', () => {
describe('SecurityNgrxModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [LoadingEventsModule]
imports: [SecurityNgrxModule]
}).compileComponents();
}));
it('should create', () => {
expect(LoadingEventsModule).toBeDefined();
expect(SecurityNgrxModule).toBeDefined();
});
});
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { SecurityEffects } from './security.effects';
import { securityReducer, securitySliceName } from './security.reducer';
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature(securitySliceName, securityReducer),
EffectsModule.forFeature([SecurityEffects])
]
})
export class SecurityNgrxModule {}
import { createAction, props } from '@ngrx/store';
import { User } from '@psu/utils/security';
const PREFIX = '@psu/utils/security';
export const login = createAction(`${PREFIX} Login`, props<{ targetUrl: string }>());
export const loginSuccessful = createAction(`${PREFIX} Login Successful`, props<{ user: User }>());
export const loginFailure = createAction(`${PREFIX} Login Failure`, props<{ message?: string }>());
export const logout = createAction(`${PREFIX} Logout`);
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { AuthService } from '@psu/utils/security';
import { ReplaySubject } from 'rxjs';
import { Mock } from 'ts-mocks';
import { login, logout } from './security.actions';
import { SecurityEffects } from './security.effects';
describe('SecurityEffects', () => {
let effects: SecurityEffects;
let actions$: ReplaySubject<Action>;
let authService: Mock<AuthService>;
beforeEach(() => {
actions$ = new ReplaySubject(1);
authService = new Mock<AuthService>({
login: () => {},
logout: () => {}
});
TestBed.configureTestingModule({
providers: [SecurityEffects, provideMockActions(actions$), { provide: AuthService, useValue: authService.Object }]
});
effects = TestBed.get(SecurityEffects);
});
it('should create', () => {
expect(effects).toBeDefined();
});
it('login action should call auth service login method', () => {
let result: Action;
effects.login$.subscribe(r => (result = r));
actions$.next(login({ targetUrl: '/authed' }));
expect(result).toEqual({ type: 'NO_ACTION' });
expect(authService.Object.login).toHaveBeenCalledWith('/authed');
});
it('logout action should call auth service logout method', () => {
let result: Action;
effects.logout$.subscribe(r => (result = r));
actions$.next(logout());
expect(result).toEqual({ type: 'NO_ACTION' });
expect(authService.Object.logout).toHaveBeenCalled();
});
});
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { AuthService } from '@psu/utils/security';
import { map, tap } from 'rxjs/operators';
import { login, logout } from './security.actions';
@Injectable({ providedIn: 'root' })
export class SecurityEffects {
public login$ = createEffect(
() =>
this.actions$.pipe(
ofType(login),
map(action => action.targetUrl),
tap(targetUrl => this.authService.login(targetUrl)),
map(() => ({ type: 'NO_ACTION' }))
),
{ dispatch: false }