From 05712150c788ad2a0666c671bb155eaad3a668ad Mon Sep 17 00:00:00 2001 From: Oliver Heywood Date: Thu, 2 Aug 2018 11:32:24 -0500 Subject: [PATCH 01/11] renaming oc-form-error, oc-geography, and oc-line-item so they arn't confused with the sdk. --- .../src/app/auth/containers/login/login.component.ts | 4 ++-- .../reset-password/reset-password.component.spec.ts | 4 ++-- .../containers/reset-password/reset-password.component.ts | 4 ++-- .../checkout-address/checkout-address.component.spec.ts | 8 ++++---- .../order-approval-details.component.spec.ts | 2 +- src/UI/Buyer/src/app/order/order.module.ts | 2 +- .../address-form/address-form.component.spec.ts | 8 ++++---- .../components/address-form/address-form.component.ts | 8 ++++---- .../credit-card-form/credit-card-form.component.spec.ts | 4 ++-- .../credit-card-form/credit-card-form.component.ts | 4 ++-- .../shared/containers/register/register.component.spec.ts | 4 ++-- .../app/shared/containers/register/register.component.ts | 4 ++-- src/UI/Buyer/src/app/shared/index.ts | 6 +++--- .../shared/services/base-resolve/base-resolve.service.ts | 2 +- .../form-error.service.spec.ts} | 8 ++++---- .../form-error.service.ts} | 2 +- .../geography.models.ts} | 0 .../geography.service.spec.ts} | 8 ++++---- .../geography.service.ts} | 4 ++-- .../line-item.service.spec.ts} | 0 .../line-item.service.ts} | 0 src/UI/Buyer/src/app/shared/shared.module.ts | 6 +++--- 22 files changed, 46 insertions(+), 46 deletions(-) rename src/UI/Buyer/src/app/shared/services/{oc-form-error/oc-form-error.service.spec.ts => form-error/form-error.service.spec.ts} (94%) rename src/UI/Buyer/src/app/shared/services/{oc-form-error/oc-form-error.service.ts => form-error/form-error.service.ts} (95%) rename src/UI/Buyer/src/app/shared/services/{oc-geography/oc-geography.models.ts => geography/geography.models.ts} (100%) rename src/UI/Buyer/src/app/shared/services/{oc-geography/oc-geography.service.spec.ts => geography/geography.service.spec.ts} (55%) rename src/UI/Buyer/src/app/shared/services/{oc-geography/oc-geography.service.ts => geography/geography.service.ts} (99%) rename src/UI/Buyer/src/app/shared/services/{oc-line-item/oc-line-item.service.spec.ts => line-item/line-item.service.spec.ts} (100%) rename src/UI/Buyer/src/app/shared/services/{oc-line-item/oc-line-item.service.ts => line-item/line-item.service.ts} (100%) diff --git a/src/UI/Buyer/src/app/auth/containers/login/login.component.ts b/src/UI/Buyer/src/app/auth/containers/login/login.component.ts index 44a580a5..477930d6 100644 --- a/src/UI/Buyer/src/app/auth/containers/login/login.component.ts +++ b/src/UI/Buyer/src/app/auth/containers/login/login.component.ts @@ -26,13 +26,13 @@ export class LoginComponent implements OnInit { private appAuthService: AppAuthService, private ocTokenService: OcTokenService, private router: Router, - private fb: FormBuilder, + private formBuilder: FormBuilder, private appStateService: AppStateService, @Inject(applicationConfiguration) private appConfig: AppConfig ) {} ngOnInit() { - this.form = this.fb.group({ + this.form = this.formBuilder.group({ username: '', password: '', rememberMe: false, diff --git a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.spec.ts b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.spec.ts index d92ddb2c..2975ab17 100644 --- a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.spec.ts +++ b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.spec.ts @@ -17,7 +17,7 @@ import { } from '@ordercloud/angular-sdk'; import { CookieModule } from 'ngx-cookie'; import { ToastrService } from 'ngx-toastr'; -import { OcFormErrorService } from '@app-buyer/shared'; +import { AppFormErrorService } from '@app-buyer/shared'; describe('ResetPasswordComponent', () => { let component: ResetPasswordComponent; @@ -47,7 +47,7 @@ describe('ResetPasswordComponent', () => { { provide: Router, useValue: router }, { provide: ActivatedRoute, useValue: activatedRoute }, { provide: ToastrService, useValue: toastrService }, - { provide: OcFormErrorService, useValue: formErrorService }, + { provide: AppFormErrorService, useValue: formErrorService }, { provide: applicationConfiguration, useValue: new InjectionToken('app.config'), diff --git a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts index 93925b91..f63e055d 100644 --- a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts +++ b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts @@ -7,7 +7,7 @@ import { Router, ActivatedRoute } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; // ordercloud -import { OcMatchFieldsValidator, OcFormErrorService } from '@app-buyer/shared'; +import { OcMatchFieldsValidator, AppFormErrorService } from '@app-buyer/shared'; import { applicationConfiguration, AppConfig, @@ -30,7 +30,7 @@ export class ResetPasswordComponent implements OnInit { private toasterService: ToastrService, private formBuilder: FormBuilder, private ocPasswordResetService: OcPasswordResetService, - private formErrorService: OcFormErrorService, + private formErrorService: AppFormErrorService, @Inject(applicationConfiguration) private appConfig: AppConfig ) {} diff --git a/src/UI/Buyer/src/app/checkout/containers/checkout-address/checkout-address.component.spec.ts b/src/UI/Buyer/src/app/checkout/containers/checkout-address/checkout-address.component.spec.ts index a8a6b677..1da7a089 100644 --- a/src/UI/Buyer/src/app/checkout/containers/checkout-address/checkout-address.component.spec.ts +++ b/src/UI/Buyer/src/app/checkout/containers/checkout-address/checkout-address.component.spec.ts @@ -7,11 +7,11 @@ import { of, BehaviorSubject, Subject } from 'rxjs'; import { OcMeService, OcOrderService } from '@ordercloud/angular-sdk'; import { AppStateService, - OcFormErrorService, + AppFormErrorService, ModalService, } from '@app-buyer/shared'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { OcGeographyService } from '@app-buyer/shared/services/oc-geography/oc-geography.service'; +import { AppGeographyService } from '@app-buyer/shared/services/geography/geography.service'; import { CheckoutSectionBaseComponent } from '@app-buyer/checkout/components/checkout-section-base/checkout-section-base.component'; describe('CheckoutAddressComponent', () => { @@ -70,12 +70,12 @@ describe('CheckoutAddressComponent', () => { ], imports: [ReactiveFormsModule], providers: [ - OcFormErrorService, + AppFormErrorService, { provide: ModalService, useValue: modalService }, { provide: OcMeService, useValue: meService }, { provide: OcOrderService, useValue: orderService }, { provide: AppStateService, useValue: appStateService }, - OcGeographyService, + AppGeographyService, ], schemas: [NO_ERRORS_SCHEMA], // Ignore template errors: remove if tests are added to test template }).compileComponents(); diff --git a/src/UI/Buyer/src/app/order/containers/order-approval-details/order-approval-details.component.spec.ts b/src/UI/Buyer/src/app/order/containers/order-approval-details/order-approval-details.component.spec.ts index 0c8de15c..62444207 100644 --- a/src/UI/Buyer/src/app/order/containers/order-approval-details/order-approval-details.component.spec.ts +++ b/src/UI/Buyer/src/app/order/containers/order-approval-details/order-approval-details.component.spec.ts @@ -1,6 +1,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { OrderApprovalDetailsComponent } from './order-approval-details.component'; +import { OrderApprovalDetailsComponent } from '@app-buyer/order/containers/order-approval-details/order-approval-details.component'; import { OcOrderService } from '@ordercloud/angular-sdk'; import { ActivatedRoute, diff --git a/src/UI/Buyer/src/app/order/order.module.ts b/src/UI/Buyer/src/app/order/order.module.ts index 3f4fea85..3f9f29a3 100644 --- a/src/UI/Buyer/src/app/order/order.module.ts +++ b/src/UI/Buyer/src/app/order/order.module.ts @@ -13,7 +13,7 @@ import { OrderResolve } from '@app-buyer/order/order.resolve'; import { OrderShipmentsComponent } from '@app-buyer/order/containers/order-shipments/order-shipments.component'; import { ShipmentsResolve } from '@app-buyer/order/shipments.resolve'; import { OrderAprovalComponent } from '@app-buyer/order/containers/order-approval/order-approval.component'; -import { OrderApprovalDetailsComponent } from './containers/order-approval-details/order-approval-details.component'; +import { OrderApprovalDetailsComponent } from '@app-buyer/order/containers/order-approval-details/order-approval-details.component'; @NgModule({ imports: [SharedModule, OrderRoutingModule], diff --git a/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.spec.ts b/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.spec.ts index 98ef6c64..6e48fdbe 100644 --- a/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.spec.ts +++ b/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.spec.ts @@ -2,10 +2,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AddressFormComponent } from '@app-buyer/shared/components/address-form/address-form.component'; import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { OcGeographyService } from '@app-buyer/shared'; +import { AppGeographyService } from '@app-buyer/shared'; import { of } from 'rxjs'; import { OcMeService } from '@ordercloud/angular-sdk'; -import { OcFormErrorService } from '@app-buyer/shared'; +import { AppFormErrorService } from '@app-buyer/shared'; describe('AddressFormComponent', () => { let component: AddressFormComponent; @@ -25,9 +25,9 @@ describe('AddressFormComponent', () => { declarations: [AddressFormComponent], imports: [ReactiveFormsModule], providers: [ - OcGeographyService, + AppGeographyService, FormBuilder, - { provide: OcFormErrorService, useValue: formErrorService }, + { provide: AppFormErrorService, useValue: formErrorService }, { provide: OcMeService, useValue: meService }, ], }).compileComponents(); diff --git a/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.ts b/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.ts index 7d920411..7686c1e5 100644 --- a/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.ts +++ b/src/UI/Buyer/src/app/shared/components/address-form/address-form.component.ts @@ -3,8 +3,8 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // 3rd party import { BuyerAddress } from '@ordercloud/angular-sdk'; -import { OcGeographyService } from '@app-buyer/shared/services/oc-geography/oc-geography.service'; -import { OcFormErrorService } from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; +import { AppGeographyService } from '@app-buyer/shared/services/geography/geography.service'; +import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; @Component({ selector: 'shared-address-form', @@ -20,9 +20,9 @@ export class AddressFormComponent implements OnInit { addressForm: FormGroup; constructor( - private ocGeography: OcGeographyService, + private ocGeography: AppGeographyService, private formBuilder: FormBuilder, - private formErrorService: OcFormErrorService + private formErrorService: AppFormErrorService ) { this.stateOptions = this.ocGeography.getStates().map((s) => s.abbreviation); this.countryOptions = this.ocGeography.getCountries(); diff --git a/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.spec.ts b/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.spec.ts index ae07a973..a8e20328 100644 --- a/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.spec.ts +++ b/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.spec.ts @@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CreditCardFormComponent } from '@app-buyer/shared/components/credit-card-form/credit-card-form.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { OcFormErrorService } from '@app-buyer/shared'; +import { AppFormErrorService } from '@app-buyer/shared'; describe('CreditCardFormComponent', () => { let component: CreditCardFormComponent; @@ -18,7 +18,7 @@ describe('CreditCardFormComponent', () => { TestBed.configureTestingModule({ declarations: [CreditCardFormComponent], imports: [FontAwesomeModule, ReactiveFormsModule], - providers: [{ provide: OcFormErrorService, useValue: formErrorService }], + providers: [{ provide: AppFormErrorService, useValue: formErrorService }], }).compileComponents(); })); diff --git a/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.ts b/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.ts index 64e44515..1f863470 100644 --- a/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.ts +++ b/src/UI/Buyer/src/app/shared/components/credit-card-form/credit-card-form.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { CreateCardDetails } from '@app-buyer/shared'; -import { OcFormErrorService } from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; +import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { faCcVisa, @@ -17,7 +17,7 @@ import { export class CreditCardFormComponent implements OnInit { constructor( private formBuilder: FormBuilder, - private formErrorService: OcFormErrorService + private formErrorService: AppFormErrorService ) {} @Output() formSubmitted = new EventEmitter(); diff --git a/src/UI/Buyer/src/app/shared/containers/register/register.component.spec.ts b/src/UI/Buyer/src/app/shared/containers/register/register.component.spec.ts index 6c6ccd8e..72a05f54 100644 --- a/src/UI/Buyer/src/app/shared/containers/register/register.component.spec.ts +++ b/src/UI/Buyer/src/app/shared/containers/register/register.component.spec.ts @@ -13,7 +13,7 @@ import { ToastrService } from 'ngx-toastr'; import { PhoneFormatPipe, AppStateService, - OcFormErrorService, + AppFormErrorService, } from '@app-buyer/shared'; import { of, Subject } from 'rxjs'; @@ -63,7 +63,7 @@ describe('RegisterComponent', () => { declarations: [PhoneFormatPipe, RegisterComponent], imports: [ReactiveFormsModule, CookieModule.forRoot()], providers: [ - { provide: OcFormErrorService, useValue: formErrorService }, + { provide: AppFormErrorService, useValue: formErrorService }, { provide: Router, useValue: router }, { provide: OcTokenService, useValue: tokenService }, { provide: OcMeService, useValue: meService }, diff --git a/src/UI/Buyer/src/app/shared/containers/register/register.component.ts b/src/UI/Buyer/src/app/shared/containers/register/register.component.ts index 3386d8bc..e2bd5d82 100644 --- a/src/UI/Buyer/src/app/shared/containers/register/register.component.ts +++ b/src/UI/Buyer/src/app/shared/containers/register/register.component.ts @@ -10,7 +10,7 @@ import { } from '@app-buyer/config/app.config'; import { OcMatchFieldsValidator } from '@app-buyer/shared/validators/oc-match-fields/oc-match-fields.validator'; import { AppStateService } from '@app-buyer/shared/services/app-state/app-state.service'; -import { OcFormErrorService } from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; +import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; @Component({ selector: 'shared-register', @@ -30,7 +30,7 @@ export class RegisterComponent implements OnInit { private router: Router, private appStateService: AppStateService, private activatedRoute: ActivatedRoute, - private formErrorService: OcFormErrorService, + private formErrorService: AppFormErrorService, @Inject(applicationConfiguration) private appConfig: AppConfig ) { this.appName = this.appConfig.appname; diff --git a/src/UI/Buyer/src/app/shared/index.ts b/src/UI/Buyer/src/app/shared/index.ts index bf3d0ea9..0a97e8c2 100644 --- a/src/UI/Buyer/src/app/shared/index.ts +++ b/src/UI/Buyer/src/app/shared/index.ts @@ -23,9 +23,9 @@ export * from '@app-buyer/shared/guards/is-logged-in/is-logged-in.guard'; export * from '@app-buyer/shared/services/app-state/app-state.service'; export * from '@app-buyer/shared/services/authorize-net/authorize-net.service'; export * from '@app-buyer/shared/services/base-resolve/base-resolve.service'; -export * from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; -export * from '@app-buyer/shared/services/oc-geography/oc-geography.service'; -export * from '@app-buyer/shared/services/oc-line-item/oc-line-item.service'; +export * from '@app-buyer/shared/services/form-error/form-error.service'; +export * from '@app-buyer/shared/services/geography/geography.service'; +export * from '@app-buyer/shared/services/line-item/line-item.service'; export * from '@app-buyer/shared/services/modal/modal.service'; // validators diff --git a/src/UI/Buyer/src/app/shared/services/base-resolve/base-resolve.service.ts b/src/UI/Buyer/src/app/shared/services/base-resolve/base-resolve.service.ts index a3fbd949..21901adc 100644 --- a/src/UI/Buyer/src/app/shared/services/base-resolve/base-resolve.service.ts +++ b/src/UI/Buyer/src/app/shared/services/base-resolve/base-resolve.service.ts @@ -13,7 +13,7 @@ import { ListLineItem, LineItem, } from '@ordercloud/angular-sdk'; -import { AppLineItemService } from '@app-buyer/shared/services/oc-line-item/oc-line-item.service'; +import { AppLineItemService } from '@app-buyer/shared/services/line-item/line-item.service'; import { AppStateService } from '@app-buyer/shared/services/app-state/app-state.service'; import * as jwtDecode from 'jwt-decode'; diff --git a/src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.spec.ts b/src/UI/Buyer/src/app/shared/services/form-error/form-error.service.spec.ts similarity index 94% rename from src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.spec.ts rename to src/UI/Buyer/src/app/shared/services/form-error/form-error.service.spec.ts index 9299b2e7..547719a4 100644 --- a/src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.spec.ts +++ b/src/UI/Buyer/src/app/shared/services/form-error/form-error.service.spec.ts @@ -1,18 +1,18 @@ import { TestBed, inject } from '@angular/core/testing'; -import { OcFormErrorService } from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; +import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; import { FormGroup, Validators, FormControl } from '@angular/forms'; describe('OcFormErrorService', () => { let service; beforeEach(() => { TestBed.configureTestingModule({ - providers: [OcFormErrorService], + providers: [AppFormErrorService], }); }); it('should be created', inject( - [OcFormErrorService], - (_service: OcFormErrorService) => { + [AppFormErrorService], + (_service: AppFormErrorService) => { service = _service; expect(service).toBeTruthy(); } diff --git a/src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.ts b/src/UI/Buyer/src/app/shared/services/form-error/form-error.service.ts similarity index 95% rename from src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.ts rename to src/UI/Buyer/src/app/shared/services/form-error/form-error.service.ts index d8883ceb..0021808a 100644 --- a/src/UI/Buyer/src/app/shared/services/oc-form-error/oc-form-error.service.ts +++ b/src/UI/Buyer/src/app/shared/services/form-error/form-error.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { FormControl, FormGroup, AbstractControl } from '@angular/forms'; @Injectable() -export class OcFormErrorService { +export class AppFormErrorService { displayFormErrors(form: FormGroup) { Object.keys(form.controls).forEach((key) => { form.get(key).markAsDirty(); diff --git a/src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.models.ts b/src/UI/Buyer/src/app/shared/services/geography/geography.models.ts similarity index 100% rename from src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.models.ts rename to src/UI/Buyer/src/app/shared/services/geography/geography.models.ts diff --git a/src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.spec.ts b/src/UI/Buyer/src/app/shared/services/geography/geography.service.spec.ts similarity index 55% rename from src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.spec.ts rename to src/UI/Buyer/src/app/shared/services/geography/geography.service.spec.ts index f499bdbc..9f66d35b 100644 --- a/src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.spec.ts +++ b/src/UI/Buyer/src/app/shared/services/geography/geography.service.spec.ts @@ -1,17 +1,17 @@ import { TestBed, inject } from '@angular/core/testing'; -import { OcGeographyService } from '@app-buyer/shared/services/oc-geography/oc-geography.service'; +import { AppGeographyService } from '@app-buyer/shared/services/geography/geography.service'; describe('OcGeographyService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [OcGeographyService], + providers: [AppGeographyService], }); }); it('should be created', inject( - [OcGeographyService], - (service: OcGeographyService) => { + [AppGeographyService], + (service: AppGeographyService) => { expect(service).toBeTruthy(); } )); diff --git a/src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.ts b/src/UI/Buyer/src/app/shared/services/geography/geography.service.ts similarity index 99% rename from src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.ts rename to src/UI/Buyer/src/app/shared/services/geography/geography.service.ts index 0d17ebcd..6dd9238a 100644 --- a/src/UI/Buyer/src/app/shared/services/oc-geography/oc-geography.service.ts +++ b/src/UI/Buyer/src/app/shared/services/geography/geography.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'; import { StateDefinition, CountryDefinition, -} from '@app-buyer/shared/services/oc-geography/oc-geography.models'; +} from '@app-buyer/shared/services/geography/geography.models'; @Injectable({ providedIn: 'root', }) -export class OcGeographyService { +export class AppGeographyService { getStates(): StateDefinition[] { return [ { label: 'Alabama', abbreviation: 'AL', country: 'US' }, diff --git a/src/UI/Buyer/src/app/shared/services/oc-line-item/oc-line-item.service.spec.ts b/src/UI/Buyer/src/app/shared/services/line-item/line-item.service.spec.ts similarity index 100% rename from src/UI/Buyer/src/app/shared/services/oc-line-item/oc-line-item.service.spec.ts rename to src/UI/Buyer/src/app/shared/services/line-item/line-item.service.spec.ts diff --git a/src/UI/Buyer/src/app/shared/services/oc-line-item/oc-line-item.service.ts b/src/UI/Buyer/src/app/shared/services/line-item/line-item.service.ts similarity index 100% rename from src/UI/Buyer/src/app/shared/services/oc-line-item/oc-line-item.service.ts rename to src/UI/Buyer/src/app/shared/services/line-item/line-item.service.ts diff --git a/src/UI/Buyer/src/app/shared/shared.module.ts b/src/UI/Buyer/src/app/shared/shared.module.ts index acb7dd07..67fd73ce 100644 --- a/src/UI/Buyer/src/app/shared/shared.module.ts +++ b/src/UI/Buyer/src/app/shared/shared.module.ts @@ -30,9 +30,9 @@ import { import { BaseResolve } from '@app-buyer/shared/resolves/base.resolve'; import { SharedRoutingModule } from '@app-buyer/shared/shared-routing.module'; import { BaseResolveService } from '@app-buyer/shared/services/base-resolve/base-resolve.service'; -import { AppLineItemService } from '@app-buyer/shared/services/oc-line-item/oc-line-item.service'; +import { AppLineItemService } from '@app-buyer/shared/services/line-item/line-item.service'; import { AuthorizeNetService } from '@app-buyer/shared/services/authorize-net/authorize-net.service'; -import { OcFormErrorService } from '@app-buyer/shared/services/oc-form-error/oc-form-error.service'; +import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; import { ModalService } from '@app-buyer/shared/services/modal/modal.service'; // pipes @@ -189,7 +189,7 @@ export class SharedModule { AuthorizeNetService, BaseResolve, BaseResolveService, - OcFormErrorService, + AppFormErrorService, AppLineItemService, ModalService, PhoneFormatPipe, From 9189d9f1f975017c9f1ab33ebfb5524d5ee15dc1 Mon Sep 17 00:00:00 2001 From: Oliver Heywood Date: Thu, 2 Aug 2018 12:07:52 -0500 Subject: [PATCH 02/11] changing oc-match-fields and oc-product-quantity also --- .../reset-password.component.ts | 7 +++- .../quantity-input.component.ts | 10 ++--- src/UI/Buyer/src/app/shared/index.ts | 4 +- .../match-fields.validator.spec.ts} | 0 .../match-fields.validator.ts} | 2 +- .../product-quantity.validator.spec.ts} | 34 ++++++++-------- .../product.quantity.validator.ts} | 4 +- src/UI/Buyer/src/assets/oc-products.js | 39 ------------------- 8 files changed, 32 insertions(+), 68 deletions(-) rename src/UI/Buyer/src/app/shared/validators/{oc-match-fields/oc-match-fields.validator.spec.ts => match-fields/match-fields.validator.spec.ts} (100%) rename src/UI/Buyer/src/app/shared/validators/{oc-match-fields/oc-match-fields.validator.ts => match-fields/match-fields.validator.ts} (94%) rename src/UI/Buyer/src/app/shared/validators/{oc-product-quantity/oc-product-quantity.validator.spec.ts => product-quantity/product-quantity.validator.spec.ts} (63%) rename src/UI/Buyer/src/app/shared/validators/{oc-product-quantity/oc-product.quantity.validator.ts => product-quantity/product.quantity.validator.ts} (91%) delete mode 100644 src/UI/Buyer/src/assets/oc-products.js diff --git a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts index f63e055d..72af9dee 100644 --- a/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts +++ b/src/UI/Buyer/src/app/auth/containers/reset-password/reset-password.component.ts @@ -7,7 +7,10 @@ import { Router, ActivatedRoute } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; // ordercloud -import { OcMatchFieldsValidator, AppFormErrorService } from '@app-buyer/shared'; +import { + AppMatchFieldsValidator, + AppFormErrorService, +} from '@app-buyer/shared'; import { applicationConfiguration, AppConfig, @@ -44,7 +47,7 @@ export class ResetPasswordComponent implements OnInit { password: '', passwordConfirm: '', }, - { validator: OcMatchFieldsValidator('password', 'passwordConfirm') } + { validator: AppMatchFieldsValidator('password', 'passwordConfirm') } ); } diff --git a/src/UI/Buyer/src/app/shared/components/quantity-input/quantity-input.component.ts b/src/UI/Buyer/src/app/shared/components/quantity-input/quantity-input.component.ts index 3304f477..a89d6cc8 100644 --- a/src/UI/Buyer/src/app/shared/components/quantity-input/quantity-input.component.ts +++ b/src/UI/Buyer/src/app/shared/components/quantity-input/quantity-input.component.ts @@ -9,9 +9,9 @@ import { import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { BuyerProduct } from '@ordercloud/angular-sdk'; import { - OcMinProductQty, - OcMaxProductQty, -} from '@app-buyer/shared/validators/oc-product-quantity/oc-product.quantity.validator'; + AppMinProductQty, + AppMaxProductQty, +} from '@app-buyer/shared/validators/product-quantity/product.quantity.validator'; import { ToastrService } from 'ngx-toastr'; import { AddToCartEvent } from '@app-buyer/shared/models/add-to-cart-event.interface'; import { debounceTime, takeWhile } from 'rxjs/operators'; @@ -40,8 +40,8 @@ export class QuantityInputComponent implements OnInit, OnDestroy { this.existingQty || this.getDefaultQty(), [ Validators.required, - OcMinProductQty(this.product), - OcMaxProductQty(this.product), + AppMinProductQty(this.product), + AppMaxProductQty(this.product), ], ], }); diff --git a/src/UI/Buyer/src/app/shared/index.ts b/src/UI/Buyer/src/app/shared/index.ts index 0a97e8c2..3e5f814a 100644 --- a/src/UI/Buyer/src/app/shared/index.ts +++ b/src/UI/Buyer/src/app/shared/index.ts @@ -29,8 +29,8 @@ export * from '@app-buyer/shared/services/line-item/line-item.service'; export * from '@app-buyer/shared/services/modal/modal.service'; // validators -export * from '@app-buyer/shared/validators/oc-match-fields/oc-match-fields.validator'; -export * from '@app-buyer/shared/validators/oc-product-quantity/oc-product.quantity.validator'; +export * from '@app-buyer/shared/validators/match-fields/match-fields.validator'; +export * from '@app-buyer/shared/validators/product-quantity/product.quantity.validator'; // modules export * from '@app-buyer/shared/shared-routing.module'; diff --git a/src/UI/Buyer/src/app/shared/validators/oc-match-fields/oc-match-fields.validator.spec.ts b/src/UI/Buyer/src/app/shared/validators/match-fields/match-fields.validator.spec.ts similarity index 100% rename from src/UI/Buyer/src/app/shared/validators/oc-match-fields/oc-match-fields.validator.spec.ts rename to src/UI/Buyer/src/app/shared/validators/match-fields/match-fields.validator.spec.ts diff --git a/src/UI/Buyer/src/app/shared/validators/oc-match-fields/oc-match-fields.validator.ts b/src/UI/Buyer/src/app/shared/validators/match-fields/match-fields.validator.ts similarity index 94% rename from src/UI/Buyer/src/app/shared/validators/oc-match-fields/oc-match-fields.validator.ts rename to src/UI/Buyer/src/app/shared/validators/match-fields/match-fields.validator.ts index 58453fe2..3663cd9c 100644 --- a/src/UI/Buyer/src/app/shared/validators/oc-match-fields/oc-match-fields.validator.ts +++ b/src/UI/Buyer/src/app/shared/validators/match-fields/match-fields.validator.ts @@ -3,7 +3,7 @@ import { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms'; // takes the name of two form controls and validates that values are the same // form controls must be part of the same form group see app-reset-password-form component for example -export function OcMatchFieldsValidator( +export function AppMatchFieldsValidator( firstFieldName: string, secondFieldName: string ): ValidatorFn { diff --git a/src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product-quantity.validator.spec.ts b/src/UI/Buyer/src/app/shared/validators/product-quantity/product-quantity.validator.spec.ts similarity index 63% rename from src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product-quantity.validator.spec.ts rename to src/UI/Buyer/src/app/shared/validators/product-quantity/product-quantity.validator.spec.ts index 08c5f242..2e45e2a1 100644 --- a/src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product-quantity.validator.spec.ts +++ b/src/UI/Buyer/src/app/shared/validators/product-quantity/product-quantity.validator.spec.ts @@ -1,36 +1,36 @@ import { - OcMinProductQty, - OcMaxProductQty, -} from '@app-buyer/shared/validators/oc-product-quantity/oc-product.quantity.validator'; + AppMinProductQty, + AppMaxProductQty, +} from '@app-buyer/shared/validators/product-quantity/product.quantity.validator'; import { FormControl } from '@angular/forms'; describe('OcMinProductQty Validator', () => { const mockProduct = { PriceSchedule: { MinQuantity: 2 } }; it('should not error on an empty string', () => { - expect(OcMinProductQty({})(new FormControl(''))).toBeNull(); + expect(AppMinProductQty({})(new FormControl(''))).toBeNull(); }); it('should not error on null', () => { - expect(OcMinProductQty({})(new FormControl(null))).toBeNull(); + expect(AppMinProductQty({})(new FormControl(null))).toBeNull(); }); it('should return a validation error on small values', () => { - expect(OcMinProductQty(mockProduct)(new FormControl(1))).toEqual({ + expect(AppMinProductQty(mockProduct)(new FormControl(1))).toEqual({ OcMinProductQty: { min: 2, actual: 1 }, }); }); it('should not error on big values', () => { - expect(OcMinProductQty(mockProduct)(new FormControl(3))).toBe(null); + expect(AppMinProductQty(mockProduct)(new FormControl(3))).toBe(null); }); it('should not error on equal values', () => { - expect(OcMinProductQty(mockProduct)(new FormControl(2))).toBe(null); + expect(AppMinProductQty(mockProduct)(new FormControl(2))).toBe(null); }); }); describe('OcMaxProductQty Validator', () => { it('should not error on empty string', () => { - expect(OcMaxProductQty({})(new FormControl(''))).toBeNull(); + expect(AppMaxProductQty({})(new FormControl(''))).toBeNull(); }); it('should not error on null', () => { - expect(OcMaxProductQty({})(new FormControl(null))).toBeNull(); + expect(AppMaxProductQty({})(new FormControl(null))).toBeNull(); }); describe('QuantityAvailable greater than MaxQuantity', () => { const mockProduct = { @@ -38,15 +38,15 @@ describe('OcMaxProductQty Validator', () => { PriceSchedule: { MaxQuantity: 10 }, }; it('should not error on quantity less than QuantityAvailable', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(1))).toBeNull(); + expect(AppMaxProductQty(mockProduct)(new FormControl(1))).toBeNull(); }); it('should return a validation error on value larger than MaxQuantity', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(8))).toEqual({ + expect(AppMaxProductQty(mockProduct)(new FormControl(8))).toEqual({ OcMaxProductQty: { max: 7, actual: 8 }, }); }); it('should not error on quantity equal to QuantityAvailable', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(1))).toBe(null); + expect(AppMaxProductQty(mockProduct)(new FormControl(1))).toBe(null); }); }); describe('MaxQuantity greater than QuantityAvailable', () => { @@ -55,15 +55,15 @@ describe('OcMaxProductQty Validator', () => { PriceSchedule: { MaxQuantity: 7 }, }; it('should not error on quantity less than QuantityAvailable', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(1))).toBeNull(); + expect(AppMaxProductQty(mockProduct)(new FormControl(1))).toBeNull(); }); it('should return a validation error on value larger than MaxQuantity', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(8))).toEqual({ + expect(AppMaxProductQty(mockProduct)(new FormControl(8))).toEqual({ OcMaxProductQty: { max: 7, actual: 8 }, }); }); it('should not error on quantity equal to QuantityAvailable', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(1))).toBe(null); + expect(AppMaxProductQty(mockProduct)(new FormControl(1))).toBe(null); }); }); describe('QuantityAvailable of zero should produce error', () => { @@ -71,7 +71,7 @@ describe('OcMaxProductQty Validator', () => { Inventory: { QuantityAvailable: 0 }, }; it('should return a validation error on value larger than zero', () => { - expect(OcMaxProductQty(mockProduct)(new FormControl(1))).toEqual({ + expect(AppMaxProductQty(mockProduct)(new FormControl(1))).toEqual({ OcMaxProductQty: { max: 0, actual: 1 }, }); }); diff --git a/src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product.quantity.validator.ts b/src/UI/Buyer/src/app/shared/validators/product-quantity/product.quantity.validator.ts similarity index 91% rename from src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product.quantity.validator.ts rename to src/UI/Buyer/src/app/shared/validators/product-quantity/product.quantity.validator.ts index da5155dd..d3ed2e73 100644 --- a/src/UI/Buyer/src/app/shared/validators/oc-product-quantity/oc-product.quantity.validator.ts +++ b/src/UI/Buyer/src/app/shared/validators/product-quantity/product.quantity.validator.ts @@ -5,7 +5,7 @@ import { BuyerProduct } from '@ordercloud/angular-sdk'; * validate against the min quantity defined by an * ordercloud product's price schedule */ -export function OcMinProductQty(product: BuyerProduct): ValidatorFn { +export function AppMinProductQty(product: BuyerProduct): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const minQty = getMinQty(product); @@ -31,7 +31,7 @@ function getMinQty(product: BuyerProduct): number { * validate against the max quantity defined by an * ordercloud product's price schedule */ -export function OcMaxProductQty(product: BuyerProduct): ValidatorFn { +export function AppMaxProductQty(product: BuyerProduct): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const maxQty = getMaxQty(product); diff --git a/src/UI/Buyer/src/assets/oc-products.js b/src/UI/Buyer/src/assets/oc-products.js deleted file mode 100644 index 93b3ca38..00000000 --- a/src/UI/Buyer/src/assets/oc-products.js +++ /dev/null @@ -1,39 +0,0 @@ -angular.module('orderCloud').factory('ocProducts', OrderCloudProductsService); - -function OrderCloudProductsService(OrderCloudSDK, $uibModal) { - return { - Search: _search, - Related: _listRelatedProducts, - Featured: _listFeaturedProducts, - }; - - function _search(catalogID) { - return $uibModal.open({ - backdrop: 'static', - templateUrl: 'common/services/oc-products/productSearch.modal.html', - controller: 'ProductSearchModalCtrl', - controllerAs: '$ctrl', - size: '-full-screen c-productsearch-modal', - resolve: { - CatalogID: function() { - return catalogID; - }, - }, - }).result; - } - - function _listRelatedProducts(relatedProductIDs, parameters) { - if (!relatedProductIDs) return null; - parameters = angular.extend(parameters || {}, { - filters: { ID: relatedProductIDs.join('|') }, - }); - return OrderCloudSDK.Me.ListProducts(parameters); - } - - function _listFeaturedProducts(parameters) { - parameters = angular.extend(parameters || {}, { - filters: { 'xp.Featured': true }, - }); - return OrderCloudSDK.Me.ListProducts(parameters); - } -} From 3c81df75900f40b5540462262d7506526581ceed Mon Sep 17 00:00:00 2001 From: Oliver Heywood Date: Thu, 2 Aug 2018 12:10:41 -0500 Subject: [PATCH 03/11] Forgot this. --- .../src/app/shared/containers/register/register.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UI/Buyer/src/app/shared/containers/register/register.component.ts b/src/UI/Buyer/src/app/shared/containers/register/register.component.ts index e2bd5d82..2b4177ad 100644 --- a/src/UI/Buyer/src/app/shared/containers/register/register.component.ts +++ b/src/UI/Buyer/src/app/shared/containers/register/register.component.ts @@ -8,9 +8,9 @@ import { applicationConfiguration, AppConfig, } from '@app-buyer/config/app.config'; -import { OcMatchFieldsValidator } from '@app-buyer/shared/validators/oc-match-fields/oc-match-fields.validator'; import { AppStateService } from '@app-buyer/shared/services/app-state/app-state.service'; import { AppFormErrorService } from '@app-buyer/shared/services/form-error/form-error.service'; +import { AppMatchFieldsValidator } from '@app-buyer/shared/validators/match-fields/match-fields.validator'; @Component({ selector: 'shared-register', @@ -61,7 +61,7 @@ export class RegisterComponent implements OnInit { const validatorObj = this.shouldAllowUpdate ? {} - : { validator: OcMatchFieldsValidator('Password', 'ConfirmPassword') }; + : { validator: AppMatchFieldsValidator('Password', 'ConfirmPassword') }; if (!this.shouldAllowUpdate) { Object.assign(formObj, { Password: ['', [Validators.required, Validators.minLength(8)]], From 8ed370687bf9be4658510bf9cd814771d776c968 Mon Sep 17 00:00:00 2001 From: Oliver Heywood Date: Fri, 3 Aug 2018 10:01:41 -0500 Subject: [PATCH 04/11] Login and Logout working on seller! --- src/UI/Seller/src/app/app-routing.module.ts | 22 ++ src/UI/Seller/src/app/app.component.html | 4 +- src/UI/Seller/src/app/app.component.ts | 10 +- src/UI/Seller/src/app/app.module.ts | 42 ++- .../src/app/auth/auth-routing.module.ts | 20 ++ src/UI/Seller/src/app/auth/auth.module.ts | 23 ++ .../forgot-password.component.html | 30 ++ .../forgot-password.component.scss | 0 .../forgot-password.component.spec.ts | 89 ++++++ .../forgot-password.component.ts | 53 ++++ .../containers/login/login.component.html | 48 ++++ .../containers/login/login.component.scss | 0 .../containers/login/login.component.spec.ts | 117 ++++++++ .../auth/containers/login/login.component.ts | 66 +++++ .../reset-password.component.html | 40 +++ .../reset-password.component.scss | 0 .../reset-password.component.spec.ts | 117 ++++++++ .../reset-password.component.ts | 81 ++++++ src/UI/Seller/src/app/auth/index.ts | 15 + .../auto-append-token.interceptor.spec.ts | 69 +++++ .../auto-append-token.interceptor.ts | 38 +++ .../refresh-token.interceptor.spec.ts | 140 ++++++++++ .../refresh-token.interceptor.ts | 71 +++++ .../auth/services/app-auth.service.spec.ts | 262 ++++++++++++++++++ .../src/app/auth/services/app-auth.service.ts | 133 +++++++++ .../src/app/config/error-handling.config.ts | 51 ++++ .../src/app/config/ordercloud-sdk.config.ts | 14 + .../app/layout/header/header.component.html | 16 +- .../src/app/layout/header/header.component.ts | 20 ++ .../src/app/layout/home/home.component.html | 7 +- src/UI/Seller/src/app/layout/layout.module.ts | 5 +- .../guards/has-token/has-token.guard.spec.ts | 152 ++++++++++ .../guards/has-token/has-token.guard.ts | 64 +++++ src/UI/Seller/src/app/shared/index.ts | 15 + .../shared/models/decoded-token.interface.ts | 53 ++++ .../app-state/app-state.service.spec.ts | 18 ++ .../services/app-state/app-state.service.ts | 13 + .../form-error/form-error.service.spec.ts | 110 ++++++++ .../services/form-error/form-error.service.ts | 27 ++ .../src/app/shared/shared-routing.module.ts | 9 + src/UI/Seller/src/app/shared/shared.module.ts | 56 ++++ .../match-fields.validator.spec.ts | 0 .../match-fields/match-fields.validator.ts | 23 ++ src/UI/Seller/src/environments/environment.ts | 2 +- src/UI/package-lock.json | 17 +- src/UI/package.json | 2 - 46 files changed, 2144 insertions(+), 20 deletions(-) create mode 100644 src/UI/Seller/src/app/app-routing.module.ts create mode 100644 src/UI/Seller/src/app/auth/auth-routing.module.ts create mode 100644 src/UI/Seller/src/app/auth/auth.module.ts create mode 100644 src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.html create mode 100644 src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.scss create mode 100644 src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.spec.ts create mode 100644 src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.ts create mode 100644 src/UI/Seller/src/app/auth/containers/login/login.component.html create mode 100644 src/UI/Seller/src/app/auth/containers/login/login.component.scss create mode 100644 src/UI/Seller/src/app/auth/containers/login/login.component.spec.ts create mode 100644 src/UI/Seller/src/app/auth/containers/login/login.component.ts create mode 100644 src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.html create mode 100644 src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.scss create mode 100644 src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.spec.ts create mode 100644 src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.ts create mode 100644 src/UI/Seller/src/app/auth/index.ts create mode 100644 src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.spec.ts create mode 100644 src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.ts create mode 100644 src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.spec.ts create mode 100644 src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.ts create mode 100644 src/UI/Seller/src/app/auth/services/app-auth.service.spec.ts create mode 100644 src/UI/Seller/src/app/auth/services/app-auth.service.ts create mode 100644 src/UI/Seller/src/app/config/error-handling.config.ts create mode 100644 src/UI/Seller/src/app/config/ordercloud-sdk.config.ts create mode 100644 src/UI/Seller/src/app/shared/guards/has-token/has-token.guard.spec.ts create mode 100644 src/UI/Seller/src/app/shared/guards/has-token/has-token.guard.ts create mode 100644 src/UI/Seller/src/app/shared/index.ts create mode 100644 src/UI/Seller/src/app/shared/models/decoded-token.interface.ts create mode 100644 src/UI/Seller/src/app/shared/services/app-state/app-state.service.spec.ts create mode 100644 src/UI/Seller/src/app/shared/services/app-state/app-state.service.ts create mode 100644 src/UI/Seller/src/app/shared/services/form-error/form-error.service.spec.ts create mode 100644 src/UI/Seller/src/app/shared/services/form-error/form-error.service.ts create mode 100644 src/UI/Seller/src/app/shared/shared-routing.module.ts create mode 100644 src/UI/Seller/src/app/shared/shared.module.ts create mode 100644 src/UI/Seller/src/app/shared/validators/match-fields/match-fields.validator.spec.ts create mode 100644 src/UI/Seller/src/app/shared/validators/match-fields/match-fields.validator.ts diff --git a/src/UI/Seller/src/app/app-routing.module.ts b/src/UI/Seller/src/app/app-routing.module.ts new file mode 100644 index 00000000..b4ccda62 --- /dev/null +++ b/src/UI/Seller/src/app/app-routing.module.ts @@ -0,0 +1,22 @@ +// components +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { HasTokenGuard as HasToken } from '@app-seller/shared'; +import { HomeComponent } from '@app-seller/layout/home/home.component'; + +const routes: Routes = [ + { + path: '', + canActivate: [HasToken], + children: [ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { path: 'home', component: HomeComponent }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/src/UI/Seller/src/app/app.component.html b/src/UI/Seller/src/app/app.component.html index 29ff7c9d..ffdc7ffd 100644 --- a/src/UI/Seller/src/app/app.component.html +++ b/src/UI/Seller/src/app/app.component.html @@ -1,2 +1,2 @@ - - + +k \ No newline at end of file diff --git a/src/UI/Seller/src/app/app.component.ts b/src/UI/Seller/src/app/app.component.ts index ba8e2e0e..4134928e 100644 --- a/src/UI/Seller/src/app/app.component.ts +++ b/src/UI/Seller/src/app/app.component.ts @@ -1,8 +1,16 @@ import { Component } from '@angular/core'; +import { AppStateService } from '@app-seller/shared'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) -export class AppComponent {} +export class AppComponent { + isLoggedIn$: Observable; + + constructor(private appStateService: AppStateService) { + this.isLoggedIn$ = this.appStateService.isLoggedIn; + } +} diff --git a/src/UI/Seller/src/app/app.module.ts b/src/UI/Seller/src/app/app.module.ts index e974fc6d..6a07f083 100644 --- a/src/UI/Seller/src/app/app.module.ts +++ b/src/UI/Seller/src/app/app.module.ts @@ -7,11 +7,49 @@ import { applicationConfiguration, ocAppConfig, } from '@app-seller/config/app.config'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { AutoAppendTokenInterceptor, AuthModule } from '@app-seller/auth'; +import { RefreshTokenInterceptor } from '@app-seller/auth'; +import { AppRoutingModule } from '@app-seller/app-routing.module'; +import { OrderCloudModule } from '@ordercloud/angular-sdk'; +import { OcSDKConfig } from '@app-seller/config/ordercloud-sdk.config'; +import { CookieModule } from 'ngx-cookie'; +import { ToastrModule } from 'ngx-toastr'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SharedModule } from '@app-seller/shared'; @NgModule({ declarations: [AppComponent], - imports: [BrowserModule, LayoutModule], - providers: [{ provide: applicationConfiguration, useValue: ocAppConfig }], + imports: [ + // This app + AppRoutingModule, + AuthModule, + + // Third party + BrowserAnimationsModule, + BrowserModule, + LayoutModule, + OrderCloudModule.forRoot(OcSDKConfig), + CookieModule.forRoot(), + ToastrModule.forRoot(), + SharedModule.forRoot(), + ], + providers: [ + { + provide: applicationConfiguration, + useValue: ocAppConfig, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AutoAppendTokenInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: RefreshTokenInterceptor, + multi: true, + }, + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/UI/Seller/src/app/auth/auth-routing.module.ts b/src/UI/Seller/src/app/auth/auth-routing.module.ts new file mode 100644 index 00000000..cf26de03 --- /dev/null +++ b/src/UI/Seller/src/app/auth/auth-routing.module.ts @@ -0,0 +1,20 @@ +// core services +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +// auth components +import { LoginComponent } from '@app-seller/auth/containers/login/login.component'; +import { ForgotPasswordComponent } from '@app-seller/auth/containers/forgot-password/forgot-password.component'; +import { ResetPasswordComponent } from '@app-seller/auth/containers/reset-password/reset-password.component'; + +const routes: Routes = [ + { path: 'login', component: LoginComponent }, + { path: 'forgot-password', component: ForgotPasswordComponent }, + { path: 'reset-password', component: ResetPasswordComponent }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AuthRoutingModule {} diff --git a/src/UI/Seller/src/app/auth/auth.module.ts b/src/UI/Seller/src/app/auth/auth.module.ts new file mode 100644 index 00000000..9a9bef26 --- /dev/null +++ b/src/UI/Seller/src/app/auth/auth.module.ts @@ -0,0 +1,23 @@ +// core services +import { NgModule } from '@angular/core'; +import { SharedModule } from '@app-seller/shared'; + +// components +import { LoginComponent } from '@app-seller/auth/containers/login/login.component'; +import { ForgotPasswordComponent } from '@app-seller/auth/containers/forgot-password/forgot-password.component'; +import { ResetPasswordComponent } from '@app-seller/auth/containers/reset-password/reset-password.component'; + +// routing +import { AuthRoutingModule } from '@app-seller/auth/auth-routing.module'; +import { AppAuthService } from '@app-seller/auth/services/app-auth.service'; + +@NgModule({ + imports: [AuthRoutingModule, SharedModule], + declarations: [ + LoginComponent, + ForgotPasswordComponent, + ResetPasswordComponent, + ], + providers: [AppAuthService], +}) +export class AuthModule {} diff --git a/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.html b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.html new file mode 100644 index 00000000..40831108 --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.html @@ -0,0 +1,30 @@ +
+

{{appConfig.appname}}

+
+
+

Forgot Password

+
+
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.scss b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.spec.ts b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 00000000..1d08e5cf --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,89 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { HttpClientModule } from '@angular/common/http'; +import { InjectionToken } from '@angular/core'; + +import { ForgotPasswordComponent } from '@app-seller/auth/containers/forgot-password/forgot-password.component'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; + +import { + Configuration, + OcPasswordResetService, + OcTokenService, +} from '@ordercloud/angular-sdk'; +import { CookieModule } from 'ngx-cookie'; +import { ToastrService } from 'ngx-toastr'; + +describe('ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + + const router = { navigateByUrl: jasmine.createSpy('navigateByUrl') }; + const ocPasswordService = { + SendVerificationCode: jasmine + .createSpy('SendVerificationCode') + .and.returnValue(of(true)), + }; + const toastrService = { success: jasmine.createSpy('success') }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ForgotPasswordComponent], + imports: [ReactiveFormsModule, CookieModule.forRoot(), HttpClientModule], + providers: [ + OcTokenService, + { provide: Router, useValue: router }, + { provide: OcPasswordResetService, useValue: ocPasswordService }, + { provide: ToastrService, useValue: toastrService }, + { + provide: applicationConfiguration, + useValue: new InjectionToken('app.config'), + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + describe('ngOnInit', () => { + const formbuilder = new FormBuilder(); + beforeEach(() => { + component.ngOnInit(); + }); + it('should set the form values to empty strings', () => { + expect(component.resetEmailForm.value).toEqual({ + email: '', + }); + }); + }); + describe('onSubmit', () => { + beforeEach(() => { + component['appConfig'].clientID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + + component.onSubmit(); + }); + it('should call the PasswordService SendVerificationCode method, Toaster success method, and route to login', () => { + expect(ocPasswordService.SendVerificationCode).toHaveBeenCalledWith({ + Email: '', + ClientID: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + URL: 'http://localhost:9876', + }); + expect(toastrService.success).toHaveBeenCalledWith( + 'Password Reset Email Sent!' + ); + expect(router.navigateByUrl).toHaveBeenCalledWith('/login'); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.ts b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.ts new file mode 100644 index 00000000..a5c12e8a --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/forgot-password/forgot-password.component.ts @@ -0,0 +1,53 @@ +// angular +import { Component, OnInit, Inject } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +// angular libs +import { ToastrService } from 'ngx-toastr'; + +// ordercloud +import { OcPasswordResetService } from '@ordercloud/angular-sdk'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; + +@Component({ + selector: 'auth-forgot-password', + templateUrl: './forgot-password.component.html', + styleUrls: ['./forgot-password.component.scss'], +}) +export class ForgotPasswordComponent implements OnInit { + resetEmailForm: FormGroup; + + constructor( + private ocPasswordResetService: OcPasswordResetService, + private router: Router, + private formBuilder: FormBuilder, + private toasterService: ToastrService, + @Inject(applicationConfiguration) private appConfig: AppConfig + ) {} + + ngOnInit() { + this.resetEmailForm = this.formBuilder.group({ email: '' }); + } + + onSubmit() { + this.ocPasswordResetService + .SendVerificationCode({ + Email: this.resetEmailForm.get('email').value, + ClientID: this.appConfig.clientID, + URL: window.location.origin, + }) + .subscribe( + () => { + this.toasterService.success('Password Reset Email Sent!'); + this.router.navigateByUrl('/login'); + }, + (error) => { + throw error; + } + ); + } +} diff --git a/src/UI/Seller/src/app/auth/containers/login/login.component.html b/src/UI/Seller/src/app/auth/containers/login/login.component.html new file mode 100644 index 00000000..12fb18a2 --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/login/login.component.html @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/src/UI/Seller/src/app/auth/containers/login/login.component.scss b/src/UI/Seller/src/app/auth/containers/login/login.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/UI/Seller/src/app/auth/containers/login/login.component.spec.ts b/src/UI/Seller/src/app/auth/containers/login/login.component.spec.ts new file mode 100644 index 00000000..38b4045a --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/login/login.component.spec.ts @@ -0,0 +1,117 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { InjectionToken, DebugElement } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; +import { of, BehaviorSubject } from 'rxjs'; + +import { LoginComponent } from '@app-buyer/auth/containers/login/login.component'; +import { + applicationConfiguration, + AppConfig, +} from '@app-buyer/config/app.config'; + +import { + Configuration, + OcAuthService, + OcTokenService, +} from '@ordercloud/angular-sdk'; +import { CookieModule } from 'ngx-cookie'; +import { AppAuthService } from '@app-buyer/auth'; +import { AppErrorHandler } from '@app-buyer/config/error-handling.config'; +import { AppStateService } from '@app-buyer/shared'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + + const router = { navigateByUrl: jasmine.createSpy('navigateByUrl') }; + const ocTokenService = { + SetAccess: jasmine.createSpy('SetAccess'), + SetRefresh: jasmine.createSpy('Refresh'), + }; + const response = { access_token: '123456', refresh_token: 'refresh123456' }; + const ocAuthService = { + Login: jasmine.createSpy('Login').and.returnValue(of(response)), + }; + const appAuthService = { + setRememberStatus: jasmine.createSpy('setRememberStatus'), + }; + const appStateService = { isAnonSubject: new BehaviorSubject(false) }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LoginComponent], + imports: [ReactiveFormsModule, CookieModule.forRoot(), HttpClientModule], + providers: [ + AppErrorHandler, + { provide: AppStateService, useValue: appStateService }, + { provide: AppAuthService, useValue: appAuthService }, + { provide: Router, useValue: router }, + { provide: OcTokenService, useValue: ocTokenService }, + { provide: OcAuthService, useValue: ocAuthService }, + { + provide: applicationConfiguration, + useValue: new InjectionToken('app.config'), + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + describe('ngOnInit', () => { + beforeEach(() => { + component.ngOnInit(); + }); + it('should set the form values to empty strings', () => { + expect(component.form.value).toEqual({ + username: '', + password: '', + rememberMe: false, + }); + }); + }); + describe('onSubmit', () => { + beforeEach(() => { + component['appConfig'].clientID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + component['appConfig'].scope = ['testScope']; + }); + it('should call the OcAuthService Login method, OcTokenService SetAccess method, and route to home', () => { + component.onSubmit(); + expect(ocAuthService.Login).toHaveBeenCalledWith( + '', + '', + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + ['testScope'] + ); + expect(ocTokenService.SetAccess).toHaveBeenCalledWith( + response.access_token + ); + expect(router.navigateByUrl).toHaveBeenCalledWith('/home'); + }); + + it('should call set refresh token and set rememberStatus if rememberMe is true', () => { + component.form.controls['rememberMe'].setValue(true); + component.onSubmit(); + expect(ocTokenService.SetRefresh).toHaveBeenCalledWith('refresh123456'); + expect(appAuthService.setRememberStatus).toHaveBeenCalledWith(true); + }); + }); + + describe('showRegisterLink', () => { + it('should be false when user is anonymous', () => { + appStateService.isAnonSubject.next(true); + expect(component.showRegisterLink()).toEqual(false); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/containers/login/login.component.ts b/src/UI/Seller/src/app/auth/containers/login/login.component.ts new file mode 100644 index 00000000..7bd5a3e6 --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/login/login.component.ts @@ -0,0 +1,66 @@ +// angular +import { Component, OnInit, Inject } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +// ordercloud +import { OcAuthService, OcTokenService } from '@ordercloud/angular-sdk'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; +import { AppAuthService } from '@app-seller/auth/services/app-auth.service'; +import { AppStateService } from '@app-seller/shared'; + +@Component({ + selector: 'auth-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent implements OnInit { + form: FormGroup; + isAnon: boolean; + + constructor( + private ocAuthService: OcAuthService, + private appAuthService: AppAuthService, + private ocTokenService: OcTokenService, + private router: Router, + private formBuilder: FormBuilder, + private appStateService: AppStateService, + @Inject(applicationConfiguration) private appConfig: AppConfig + ) {} + + ngOnInit() { + this.form = this.formBuilder.group({ + username: '', + password: '', + rememberMe: false, + }); + } + + onSubmit() { + return this.ocAuthService + .Login( + this.form.get('username').value, + this.form.get('password').value, + this.appConfig.clientID, + this.appConfig.scope + ) + .subscribe((response) => { + const rememberMe = this.form.get('rememberMe').value; + if (rememberMe && response.refresh_token) { + /** + * set the token duration in the dashboard - https://developer.ordercloud.io/dashboard/settings + * refresh tokens are configured per clientID and initially set to 0 + * a refresh token of 0 means no refresh token is returned in OAuth response + */ + this.ocTokenService.SetRefresh(response.refresh_token); + this.appAuthService.setRememberStatus(true); + } + this.ocTokenService.SetAccess(response.access_token); + this.appStateService.isLoggedIn.next(true); + this.router.navigateByUrl('/home'); + }); + } +} diff --git a/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.html b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.html new file mode 100644 index 00000000..ede21b0e --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.html @@ -0,0 +1,40 @@ +
+

{{appConfig.appname}}

+
+
+

New Password

+
+
+ + +
+
+ + +
+ Passwords must match + +
+
\ No newline at end of file diff --git a/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.scss b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.spec.ts b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.spec.ts new file mode 100644 index 00000000..ce4539f8 --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.spec.ts @@ -0,0 +1,117 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { InjectionToken } from '@angular/core'; +import { of } from 'rxjs'; +import { HttpClientModule } from '@angular/common/http'; + +import { ResetPasswordComponent } from '@app-seller/auth/containers/reset-password/reset-password.component'; + +import { + OcPasswordResetService, + OcTokenService, +} from '@ordercloud/angular-sdk'; +import { CookieModule } from 'ngx-cookie'; +import { ToastrService } from 'ngx-toastr'; +import { AppFormErrorService } from '@app-seller/shared'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture; + + const router = { navigateByUrl: jasmine.createSpy('navigateByUrl') }; + const ocPasswordService = { + ResetPasswordByVerificationCode: jasmine + .createSpy('ResetPasswordByVerificationCode') + .and.returnValue(of(true)), + }; + const toastrService = { success: jasmine.createSpy('success') }; + const activatedRoute = { + snapshot: { queryParams: { user: 'username', code: 'pwverificationcode' } }, + }; + const formErrorService = { + hasPasswordMismatchError: jasmine.createSpy('hasPasswordMismatchError'), + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ResetPasswordComponent], + imports: [ReactiveFormsModule, CookieModule.forRoot(), HttpClientModule], + providers: [ + OcTokenService, + { provide: OcPasswordResetService, useValue: ocPasswordService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: ToastrService, useValue: toastrService }, + { provide: AppFormErrorService, useValue: formErrorService }, + { + provide: applicationConfiguration, + useValue: new InjectionToken('app.config'), + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + describe('ngOnInit', () => { + const formbuilder = new FormBuilder(); + beforeEach(() => { + component.ngOnInit(); + }); + it('should set the form values to empty strings, and the local vars to the matching query params', () => { + expect(component.resetPasswordForm.value).toEqual({ + password: '', + passwordConfirm: '', + }); + expect(component.username).toEqual( + activatedRoute.snapshot.queryParams.user + ); + expect(component.resetCode).toEqual( + activatedRoute.snapshot.queryParams.code + ); + }); + }); + describe('onSubmit', () => { + beforeEach(() => { + component['appConfig'].clientID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + + component.onSubmit(); + }); + it('should call the PasswordService ResetPasswordByVerificationCode method, Toastr success method, and route to login', () => { + expect( + ocPasswordService.ResetPasswordByVerificationCode + ).toHaveBeenCalledWith('pwverificationcode', { + ClientID: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + Password: component.resetPasswordForm.value.password, + Username: component.username, + }); + expect(toastrService.success).toHaveBeenCalledWith( + 'Password Reset Successfully' + ); + expect(router.navigateByUrl).toHaveBeenCalledWith('/login'); + }); + }); + + describe('passwordMismatchError', () => { + beforeEach(() => { + component['passwordMismatchError'](); + }); + it('should call formErrorService.hasRequiredError', () => { + expect(formErrorService.hasPasswordMismatchError).toHaveBeenCalledWith( + component.resetPasswordForm + ); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.ts b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.ts new file mode 100644 index 00000000..fd8d7f29 --- /dev/null +++ b/src/UI/Seller/src/app/auth/containers/reset-password/reset-password.component.ts @@ -0,0 +1,81 @@ +// angular +import { Component, OnInit, Inject } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; + +// angular libs +import { ToastrService } from 'ngx-toastr'; + +// ordercloud +import { + AppMatchFieldsValidator, + AppFormErrorService, +} from '@app-seller/shared'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; +import { OcPasswordResetService, PasswordReset } from '@ordercloud/angular-sdk'; + +@Component({ + selector: 'auth-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.scss'], +}) +export class ResetPasswordComponent implements OnInit { + resetPasswordForm: FormGroup; + username: string; + resetCode: string; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private toasterService: ToastrService, + private formBuilder: FormBuilder, + private ocPasswordResetService: OcPasswordResetService, + private formErrorService: AppFormErrorService, + @Inject(applicationConfiguration) private appConfig: AppConfig + ) {} + + ngOnInit() { + const urlParams = this.activatedRoute.snapshot.queryParams; + this.username = urlParams['user']; + this.resetCode = urlParams['code']; + + this.resetPasswordForm = this.formBuilder.group( + { + password: '', + passwordConfirm: '', + }, + { validator: AppMatchFieldsValidator('password', 'passwordConfirm') } + ); + } + + onSubmit() { + if (this.resetPasswordForm.status === 'INVALID') { + return; + } + + const config: PasswordReset = { + ClientID: this.appConfig.clientID, + Password: this.resetPasswordForm.get('password').value, + Username: this.username, + }; + + this.ocPasswordResetService + .ResetPasswordByVerificationCode(this.resetCode, config) + .subscribe( + () => { + this.toasterService.success('Password Reset Successfully'); + this.router.navigateByUrl('/login'); + }, + (error) => { + throw error; + } + ); + } + + // control visibility of password mismatch error + protected passwordMismatchError = (): boolean => + this.formErrorService.hasPasswordMismatchError(this.resetPasswordForm); +} diff --git a/src/UI/Seller/src/app/auth/index.ts b/src/UI/Seller/src/app/auth/index.ts new file mode 100644 index 00000000..0d3b1131 --- /dev/null +++ b/src/UI/Seller/src/app/auth/index.ts @@ -0,0 +1,15 @@ +// containers +export * from '@app-seller/auth/containers/forgot-password/forgot-password.component'; +export * from '@app-seller/auth/containers/login/login.component'; +export * from '@app-seller/auth/containers/reset-password/reset-password.component'; + +// interceptors +export * from '@app-seller/auth/interceptors/refresh-token/refresh-token.interceptor'; +export * from '@app-seller/auth/interceptors/auto-append-token/auto-append-token.interceptor'; + +// services +export * from '@app-seller/auth/services/app-auth.service'; + +// modules +export * from '@app-seller/auth/auth.module'; +export * from '@app-seller/auth/auth-routing.module'; diff --git a/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.spec.ts b/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.spec.ts new file mode 100644 index 00000000..6a355cfd --- /dev/null +++ b/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.spec.ts @@ -0,0 +1,69 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; + +import { AutoAppendTokenInterceptor } from '@app-seller/auth/interceptors/auto-append-token/auto-append-token.interceptor'; +import { applicationConfiguration } from '@app-seller/config/app.config'; +import { OcTokenService } from '@ordercloud/angular-sdk'; +import { CookieModule } from 'ngx-cookie'; + +describe('AutoAppendTokenInterceptor', () => { + const mockToken = 'ABC123'; + const mockMiddlewareUrl = 'my-integration-path/api'; + const tokenService = { + GetAccess: jasmine.createSpy('GetAccess').and.returnValue(mockToken), + }; + const appConfig = { middlewareUrl: mockMiddlewareUrl }; + let httpClient: HttpClient; + let httpMock: HttpTestingController; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CookieModule.forRoot(), HttpClientTestingModule], + providers: [ + AutoAppendTokenInterceptor, + { provide: OcTokenService, useValue: tokenService }, + { + provide: HTTP_INTERCEPTORS, + useClass: AutoAppendTokenInterceptor, + multi: true, + }, + { provide: applicationConfiguration, useValue: appConfig }, + ], + }); + httpClient = TestBed.get(HttpClient); + httpMock = TestBed.get(HttpTestingController); + }); + + it('should be created', inject( + [AutoAppendTokenInterceptor], + (service: AutoAppendTokenInterceptor) => { + expect(service).toBeTruthy(); + } + )); + + describe('making http calls', () => { + it('should add authorization headers to integration calls', () => { + httpClient.get(`${mockMiddlewareUrl}/data`).subscribe((response) => { + expect(response).toBeTruthy(); + }); + const req = httpMock.expectOne(`${mockMiddlewareUrl}/data`); + expect(req.request.headers.get('Authorization')).toEqual( + `Bearer ${mockToken}` + ); + req.flush({ hello: 'World' }); + httpMock.verify(); + }); + it('should not add authorization headers if call is not to integrations url', () => { + httpClient.get('/data').subscribe((response) => { + expect(response).toBeTruthy(); + }); + const req = httpMock.expectOne('/data'); + expect(req.request.headers.get('Authorization')).toBe(null); + req.flush({ hello: 'World' }); + httpMock.verify(); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.ts b/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.ts new file mode 100644 index 00000000..982c09c7 --- /dev/null +++ b/src/UI/Seller/src/app/auth/interceptors/auto-append-token/auto-append-token.interceptor.ts @@ -0,0 +1,38 @@ +import { Injectable, Inject } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { OcTokenService } from '@ordercloud/angular-sdk'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; + +/** + * automatically append token to the authorization header + * required to interact with middleware layer + */ +@Injectable() +export class AutoAppendTokenInterceptor implements HttpInterceptor { + constructor( + private ocTokenService: OcTokenService, + @Inject(applicationConfiguration) private appConfig: AppConfig + ) {} + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + if (request.url.includes(this.appConfig.middlewareUrl)) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${this.ocTokenService.GetAccess()}`, + }, + }); + } + return next.handle(request); + } +} diff --git a/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.spec.ts b/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.spec.ts new file mode 100644 index 00000000..4e8599cb --- /dev/null +++ b/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.spec.ts @@ -0,0 +1,140 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { InjectionToken } from '@angular/core'; +import { + HttpClient, + HttpHandler, + HTTP_INTERCEPTORS, + HttpErrorResponse, +} from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, + TestRequest, +} from '@angular/common/http/testing'; + +import { RefreshTokenInterceptor } from '@app-buyer/auth/interceptors/refresh-token/refresh-token.interceptor'; +import { + applicationConfiguration, + AppConfig, +} from '@app-buyer/config/app.config'; +import { OcTokenService, Configuration } from '@ordercloud/angular-sdk'; +import { CookieModule } from 'ngx-cookie'; +import { AppAuthService } from '@app-buyer/auth/services/app-auth.service'; +import { of, BehaviorSubject, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +describe('RefreshTokenInterceptor', () => { + const mockRefreshToken = 'RefreshABC123'; + const tokenService = { + GetAccess: jasmine + .createSpy('GetAccess') + .and.returnValue(of(mockRefreshToken)), + }; + const refreshToken = new BehaviorSubject(''); + const appAuthService = { + refreshToken: refreshToken, + fetchingRefreshToken: false, + failedRefreshAttempt: false, + refresh: jasmine.createSpy('refresh').and.returnValue(of(mockRefreshToken)), + }; + let httpClient: HttpClient; + let httpMock: HttpTestingController; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CookieModule.forRoot(), HttpClientTestingModule], + providers: [ + RefreshTokenInterceptor, + { provide: AppAuthService, useValue: appAuthService }, + { provide: OcTokenService, useValue: tokenService }, + { + provide: HTTP_INTERCEPTORS, + useClass: RefreshTokenInterceptor, + multi: true, + }, + { + provide: applicationConfiguration, + useValue: new InjectionToken('app.config'), + }, + ], + }); + httpClient = TestBed.get(HttpClient); + httpMock = TestBed.get(HttpTestingController); + }); + + it('should be created', inject( + [RefreshTokenInterceptor], + (service: RefreshTokenInterceptor) => { + expect(service).toBeTruthy(); + } + )); + + describe('making http calls', () => { + let thrownError = ''; + it('should rethrow error on non-ordercloud http calls', () => { + httpClient + .get('/data') + .pipe( + catchError((ex) => { + thrownError = ex.error; + return 'GoToSubscribe'; + }) + ) + .subscribe(() => { + expect(thrownError).toBe('mockError'); + }); + const req = httpMock.expectOne('/data'); + req.flush('mockError', { status: 401, statusText: 'Unauthorized' }); + httpMock.verify(); + }); + it('should rethrow error if status code is not 401', () => { + httpClient + .get('https://api.ordercloud.io/v1/me') + .pipe( + catchError((ex) => { + thrownError = ex.error; + return 'GoToSubscribe'; + }) + ) + .subscribe(() => { + expect(thrownError).toBe('mockError'); + }); + const req = httpMock.expectOne('https://api.ordercloud.io/v1/me'); + req.flush('mockError', { status: 500, statusText: 'Unauthorized' }); + httpMock.verify(); + }); + describe('refresh operation', () => { + let firstRequest: TestRequest; + let secondRequest: TestRequest; + const setupMockCalls = () => { + // call http client + httpClient.get('https://api.ordercloud.io/v1/me').subscribe(); + + // first request that "fails" but is caught + firstRequest = httpMock.expectOne('https://api.ordercloud.io/v1/me'); + firstRequest.flush('mockBody', { + status: 401, + statusText: 'Unauthorized', + }); + + // second request that goes out as a consequence of the refresh operation + secondRequest = httpMock.expectOne('https://api.ordercloud.io/v1/me'); + secondRequest.flush('mockBody'); + }; + it('should call appAuthService.refresh', () => { + setupMockCalls(); + expect(appAuthService.refresh).toHaveBeenCalled(); + }); + it('should set refresh token on second call', () => { + setupMockCalls(); + expect(firstRequest.request.headers.get('Authorization')).toBe(null); + expect(secondRequest.request.headers.get('Authorization')).toBe( + `Bearer ${mockRefreshToken}` + ); + appAuthService.failedRefreshAttempt = true; + }); + afterEach(() => { + httpMock.verify(); + }); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.ts b/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.ts new file mode 100644 index 00000000..3259506d --- /dev/null +++ b/src/UI/Seller/src/app/auth/interceptors/refresh-token/refresh-token.interceptor.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, + HttpErrorResponse, +} from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, filter, flatMap } from 'rxjs/operators'; +import { AppAuthService } from '@app-seller/auth/services/app-auth.service'; + +/** + * handle 401 unauthorized responses gracefully + * by attempting to refresh token + */ +@Injectable() +export class RefreshTokenInterceptor implements HttpInterceptor { + constructor(private appAuthService: AppAuthService) {} + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + return next.handle(request).pipe( + catchError((error) => { + // rethrow any non auth errors + if (!this.isAuthError(error)) { + return throwError(error); + } else { + // if a refresh attempt failed recently then ignore (3 seconds)` + if (this.appAuthService.failedRefreshAttempt) { + return; + } + + // ensure there is no outstanding request already fetching token + // if there is then wait for the token to resolve + const refreshToken = this.appAuthService.refreshToken.getValue(); + if (refreshToken || this.appAuthService.fetchingRefreshToken) { + return this.appAuthService.refreshToken.pipe( + filter((token) => token !== ''), + flatMap((token) => { + request = request.clone({ + setHeaders: { Authorization: `Bearer ${token}` }, + }); + return next.handle(request); + }) + ); + } else { + // attempt refresh for new token + return this.appAuthService.refresh().pipe( + flatMap((token) => { + request = request.clone({ + setHeaders: { Authorization: `Bearer ${token}` }, + }); + return next.handle(request); + }) + ); + } + } + }) + ); + } + + isAuthError(error: any): boolean { + return ( + error instanceof HttpErrorResponse && + error.url.indexOf('ordercloud.io') > -1 && + error.status === 401 + ); + } +} diff --git a/src/UI/Seller/src/app/auth/services/app-auth.service.spec.ts b/src/UI/Seller/src/app/auth/services/app-auth.service.spec.ts new file mode 100644 index 00000000..c3525420 --- /dev/null +++ b/src/UI/Seller/src/app/auth/services/app-auth.service.spec.ts @@ -0,0 +1,262 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { applicationConfiguration } from '@app-buyer/config/app.config'; + +import { + OcAuthService, + OcTokenService, + Configuration, +} from '@ordercloud/angular-sdk'; +import { CookieModule, CookieService } from 'ngx-cookie'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppErrorHandler } from '@app-buyer/config/error-handling.config'; +import { AppAuthService } from '@app-buyer/auth/services/app-auth.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of, throwError } from 'rxjs'; +import { Router } from '@angular/router'; +import { AppStateService } from '@app-buyer/shared'; + +describe('AppAuthService', () => { + const mockCookieResponse = { + 'mgr-test_cookieOne': 'cookieone', + 'mgr-test_cookieTwo': 'cookietwo', + otherCookie: 'othercookie', + }; + const mockRefreshToken = 'mock refresh token'; + const mockToken = 'mock token'; + const mockClientID = 'mockClientID'; + const mockAppName = 'mgr-test'; + const router = { navigate: jasmine.createSpy('navigate') }; + const cookieService = { + getObject: jasmine.createSpy('getObject').and.returnValue({ status: true }), + putObject: jasmine.createSpy('putObject'), + getAll: jasmine.createSpy('getAll').and.returnValue(mockCookieResponse), + remove: jasmine.createSpy('remove'), + }; + let appAuthService: AppAuthService; + let authService: OcAuthService; + let tokenService: OcTokenService; + let appConfig = { + appname: mockAppName, + clientID: mockClientID, + anonymousShoppingEnabled: true, + scope: ['FullAccess'], + }; + const appErrorHandler = { displayError: jasmine.createSpy('displayError') }; + const appStateService = { + isLoggedIn: { next: jasmine.createSpy('next').and.returnValue(null) }, + }; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + HttpClientTestingModule, + CookieModule.forRoot(), + ], + providers: [ + { provide: Router, useValue: router }, + { provide: CookieService, useValue: cookieService }, + OcAuthService, + AppAuthService, + { provide: AppErrorHandler, useValue: appErrorHandler }, + OcTokenService, + { provide: Configuration, useValue: new Configuration() }, + { provide: applicationConfiguration, useValue: appConfig }, + { provide: AppStateService, useValue: appStateService }, + ], + }); + appConfig = TestBed.get(applicationConfiguration); + tokenService = TestBed.get(OcTokenService); + appAuthService = TestBed.get(AppAuthService); + authService = TestBed.get(OcAuthService); + }); + + it('should be created', inject( + [AppAuthService], + (service: AppAuthService) => { + expect(service).toBeTruthy(); + } + )); + + describe('refresh', () => { + describe('on success', () => { + beforeEach(() => { + spyOn(appAuthService, 'fetchRefreshToken').and.returnValue( + of(mockToken) + ); + spyOn(appAuthService, 'logout').and.returnValue(of(null)); + spyOn(tokenService, 'SetAccess'); + appAuthService.refresh().subscribe(); + }); + it('should call fetchRefreshToken', () => { + expect(appAuthService.fetchRefreshToken).toHaveBeenCalled(); + }); + it('should save the retrieved refresh token', () => { + expect(tokenService.SetAccess).toHaveBeenCalledWith(mockToken); + }); + it('should set isLoggedIn to true', () => { + expect(appStateService.isLoggedIn.next).toHaveBeenCalledWith(true); + }); + }); + describe('on error', () => { + beforeEach(() => { + spyOn(tokenService, 'GetAccess').and.returnValue(mockToken); + spyOn(appAuthService, 'fetchRefreshToken').and.returnValue( + throwError('Token refresh attempt not possible') + ); + appAuthService.refresh().subscribe(); + }); + it('should check if the user had a token before failing call', () => { + expect(tokenService.GetAccess).toHaveBeenCalled(); + }); + it('should display error message if token existed before failing call', () => { + expect(appErrorHandler.displayError).toHaveBeenCalledWith({ + message: 'Token refresh attempt not possible', + }); + }); + it('should set failedRefreshAttempt to true', () => { + expect(appAuthService.failedRefreshAttempt).toBe(true); + }); + }); + }); + + describe('fetchToken', () => { + beforeEach(() => { + spyOn(appAuthService, 'fetchRefreshToken'); + }); + it('should return access token if it is available', () => { + spyOn(tokenService, 'GetAccess').and.returnValue('mockToken'); + appAuthService.fetchToken(); + expect(appAuthService.fetchRefreshToken).not.toHaveBeenCalled(); + }); + it('should call fetchRefresh token if no access token is available', () => { + spyOn(tokenService, 'GetAccess').and.returnValue(null); + appAuthService.fetchToken(); + expect(appAuthService.fetchRefreshToken).toHaveBeenCalled(); + }); + }); + + describe('fetchRefreshToken', () => { + describe('and has refresh token', () => { + beforeEach(() => { + spyOn(tokenService, 'GetRefresh').and.returnValue(mockRefreshToken); + }); + it('should call authService.RefreshToken', () => { + spyOn(authService, 'RefreshToken').and.returnValue( + of({ access_token: mockToken }) + ); + appAuthService.fetchRefreshToken(); + expect(authService.RefreshToken).toHaveBeenCalledWith( + mockRefreshToken, + mockClientID + ); + }); + it('should call auth anonymous if refresh failed and user is anonymous', () => { + spyOn(authService, 'RefreshToken').and.returnValue(throwError(null)); + spyOn(appAuthService, 'authAnonymous').and.returnValue(of(null)); + appConfig.anonymousShoppingEnabled = true; + appAuthService.fetchRefreshToken().subscribe(); + expect(appAuthService.authAnonymous).toHaveBeenCalled(); + }); + }); + describe('and does not have a refresh token', () => { + beforeEach(() => { + spyOn(tokenService, 'GetRefresh').and.returnValue(null); + }); + it('should attempt to auth anonymous if user is anonymous', () => { + appConfig.anonymousShoppingEnabled = true; + spyOn(appAuthService, 'authAnonymous').and.returnValue(of(null)); + }); + it('should throw error if user is not anonymous', () => { + spyOn(appAuthService, 'authAnonymous').and.returnValue(of(null)); + appConfig.anonymousShoppingEnabled = false; + expect(() => { + appAuthService.fetchRefreshToken().subscribe(); + }).toThrow(); + expect(appAuthService.authAnonymous).not.toHaveBeenCalled(); + }); + }); + + describe('logout', () => { + beforeEach(() => { + appAuthService.logout(); + }); + it('should clear out cookies that start with appname', () => { + expect(cookieService.getAll).toHaveBeenCalled(); + expect(cookieService.remove).toHaveBeenCalledWith('mgr-test_cookieOne'); + expect(cookieService.remove).toHaveBeenCalledWith('mgr-test_cookieTwo'); + expect(cookieService.remove).not.toHaveBeenCalledWith('otherCookie'); + }); + it('should navigate user to login page', () => { + expect(router.navigate).toHaveBeenCalledWith(['/login']); + }); + it('should set isLoggedIn to false', () => { + expect(appStateService.isLoggedIn.next).toHaveBeenCalledWith(false); + }); + }); + + describe('authAnonymous', () => { + beforeEach(() => { + spyOn(appAuthService, 'logout').and.returnValue(of(null)); + }); + it('should call authService.Anonymous', () => { + spyOn(authService, 'Anonymous').and.returnValue( + of({ access_token: mockToken }) + ); + appAuthService.authAnonymous().subscribe(); + expect(authService.Anonymous).toHaveBeenCalledWith(mockClientID, [ + 'FullAccess', + ]); + }); + it('should display error if authService.Anonymous fails', () => { + spyOn(authService, 'Anonymous').and.returnValue(throwError('error')); + appAuthService.authAnonymous().subscribe(); + expect(appErrorHandler.displayError).toHaveBeenCalledWith('error'); + }); + it('should log user out if austhService.Anonymous fails', () => { + spyOn(authService, 'Anonymous').and.returnValue(throwError('error')); + appAuthService.authAnonymous().subscribe(); + expect(appAuthService.logout).toHaveBeenCalled(); + }); + }); + describe('isUserAnon', () => { + // tslint:disable:max-line-length + const tokenWithOrderId = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJhbm9uX3VzZXIiLCJjaWQiOiI4MDIxODkzNi0zNTBiLTQxMDUtYTFmYy05NjJhZjAyM2Q2NjYiLCJvcmRlcmlkIjoiSVlBSnFOWVVpRVdyTy1Lei1TalpqUSIsInVzcnR5cGUiOiJidXllciIsInJvbGUiOlsiQnV5ZXJSZWFkZXIiLCJNZUFkbWluIiwiTWVDcmVkaXRDYXJkQWRtaW4iLCJNZUFkZHJlc3NBZG1pbiIsIk1lWHBBZG1pbiIsIlBhc3N3b3JkUmVzZXQiLCJTaGlwbWVudFJlYWRlciIsIlNob3BwZXIiLCJBZGRyZXNzUmVhZGVyIl0sImlzcyI6Imh0dHBzOi8vYXV0aC5vcmRlcmNsb3VkLmlvIiwiYXVkIjoiaHR0cHM6Ly9hcGkub3JkZXJjbG91ZC5pbyIsImV4cCI6MTUyNzA5Nzc0MywibmJmIjoxNTI2NDkyOTQzfQ.MBV7dqBq8RXSZZ8vEGidcfH8vSwOR55yHzvAq1w2bLc'; + const tokenWithoutOrderID = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJjaGlwb3RsZWNyaGlzdGlhbiIsImNpZCI6Ijc1NzMwMTc4LWU0MjQtNGM0OS1iM2Q3LTE3Mzg1Nzg0YjE5MSIsInVzcnR5cGUiOiJidXllciIsInJvbGUiOlsiQnV5ZXJSZWFkZXIiLCJDYXRhbG9nUmVhZGVyIiwiTWVBZG1pbiIsIk1lQ3JlZGl0Q2FyZEFkbWluIiwiTWVBZGRyZXNzQWRtaW4iLCJNZVhwQWRtaW4iLCJQYXNzd29yZFJlc2V0IiwiU2hpcG1lbnRSZWFkZXIiLCJTaG9wcGVyIiwiT3JkZXJSZWFkZXIiLCJBZGRyZXNzQWRtaW4iLCJVc2VyR3JvdXBBZG1pbiJdLCJpc3MiOiJodHRwczovL2F1dGgub3JkZXJjbG91ZC5pbyIsImF1ZCI6Imh0dHBzOi8vYXBpLm9yZGVyY2xvdWQuaW8iLCJleHAiOjE1MjY1Mjg3NzQsIm5iZiI6MTUyNjQ5Mjc3NH0.uqh3_yLXTCSpzLxk6B4gbPX0wmQF4JIZTEHRXvPD9r0'; + it('should call tokenService.GetAccess', () => { + spyOn(tokenService, 'GetAccess').and.returnValue(tokenWithOrderId); + appAuthService.isUserAnon(); + expect(tokenService.GetAccess).toHaveBeenCalled(); + }); + it('should return true if token has orderid property', () => { + spyOn(tokenService, 'GetAccess').and.returnValue(tokenWithOrderId); + const isUserAnon = appAuthService.isUserAnon(); + expect(isUserAnon).toBe(true); + }); + it('should return false if token does not have orderid property', () => { + spyOn(tokenService, 'GetAccess').and.returnValue(tokenWithoutOrderID); + const isUserAnon = appAuthService.isUserAnon(); + expect(isUserAnon).toBe(false); + }); + }); + describe('setRememberStatus', () => { + const statusTrue = true; + beforeEach(() => { + appAuthService.setRememberStatus(statusTrue); + }); + it('should store status in cookies', () => { + expect(cookieService.putObject).toHaveBeenCalledWith( + 'mgr-test_rememberMe', + { status: statusTrue } + ); + }); + }); + describe('getRememberStatus', () => { + it('should return status stored in cookies', () => { + const rememberMeStatus = appAuthService.getRememberStatus(); + expect(rememberMeStatus).toBe(true); + }); + }); + }); +}); diff --git a/src/UI/Seller/src/app/auth/services/app-auth.service.ts b/src/UI/Seller/src/app/auth/services/app-auth.service.ts new file mode 100644 index 00000000..fd2faa9b --- /dev/null +++ b/src/UI/Seller/src/app/auth/services/app-auth.service.ts @@ -0,0 +1,133 @@ +import { Injectable, Inject } from '@angular/core'; +import { Observable, throwError, of, BehaviorSubject } from 'rxjs'; +import { tap, catchError, finalize, map } from 'rxjs/operators'; +import { Router } from '@angular/router'; + +// 3rd party +import { OcTokenService, OcAuthService } from '@ordercloud/angular-sdk'; +import { + applicationConfiguration, + AppConfig, +} from '@app-seller/config/app.config'; +import { CookieService } from 'ngx-cookie'; +import { keys as _keys } from 'lodash'; +import { AppErrorHandler } from '@app-seller/config/error-handling.config'; +import * as jwtDecode from 'jwt-decode'; +import { isUndefined as _isUndefined } from 'lodash'; + +export const TokenRefreshAttemptNotPossible = + 'Token refresh attempt not possible'; +@Injectable() +export class AppAuthService { + private rememberMeCookieName = `${this.appConfig.appname + .replace(/ /g, '_') + .toLowerCase()}_rememberMe`; + fetchingRefreshToken = false; + failedRefreshAttempt = false; + refreshToken: BehaviorSubject; + + constructor( + private ocTokenService: OcTokenService, + private ocAuthService: OcAuthService, + private cookieService: CookieService, + private router: Router, + private appErrorHandler: AppErrorHandler, + @Inject(applicationConfiguration) private appConfig: AppConfig + ) { + this.refreshToken = new BehaviorSubject(''); + } + + refresh(): Observable { + this.fetchingRefreshToken = true; + return this.fetchRefreshToken().pipe( + tap((token) => { + this.ocTokenService.SetAccess(token); + this.refreshToken.next(token); + }), + catchError((error) => { + if ( + this.ocTokenService.GetAccess() && + error === TokenRefreshAttemptNotPossible + ) { + this.appErrorHandler.displayError({ message: error }); + } + // ignore new refresh attempts if a refresh + // attempt failed within the last 3 seconds + this.failedRefreshAttempt = true; + setTimeout(() => { + this.failedRefreshAttempt = false; + }, 3000); + return this.logout(); + }), + finalize(() => { + this.fetchingRefreshToken = false; + }) + ); + } + + fetchToken(): Observable { + const accessToken = this.ocTokenService.GetAccess(); + if (accessToken) { + return of(accessToken); + } + return this.fetchRefreshToken(); + } + + fetchRefreshToken(): Observable { + const refreshToken = this.ocTokenService.GetRefresh(); + if (refreshToken) { + return this.ocAuthService + .RefreshToken(refreshToken, this.appConfig.clientID) + .pipe( + map((authResponse) => authResponse.access_token), + tap((token) => this.ocTokenService.SetAccess(token)), + catchError((error) => { + return throwError(error); + }) + ); + } + throwError(TokenRefreshAttemptNotPossible); + } + + logout(): Observable { + const cookiePrefix = this.appConfig.appname + .replace(/ /g, '_') + .toLowerCase(); + const appCookieNames = _keys(this.cookieService.getAll()); + appCookieNames.forEach((cookieName) => { + if (cookieName.indexOf(cookiePrefix) > -1) { + this.cookieService.remove(cookieName); + } + }); + return of(this.router.navigate(['/login'])); + } + + authAnonymous(): Observable { + return this.ocAuthService + .Anonymous(this.appConfig.clientID, this.appConfig.scope) + .pipe( + map((authResponse) => authResponse.access_token), + tap((token) => this.ocTokenService.SetAccess(token)), + catchError((ex) => { + this.appErrorHandler.displayError(ex); + return this.logout(); + }) + ); + } + + isUserAnon(): boolean { + const anonOrderID = jwtDecode(this.ocTokenService.GetAccess()).orderid; + return !_isUndefined(anonOrderID); + } + + setRememberStatus(status: boolean): void { + this.cookieService.putObject(this.rememberMeCookieName, { status: status }); + } + + getRememberStatus(): boolean { + const rememberMe = <{ status: string }>( + this.cookieService.getObject(this.rememberMeCookieName) + ); + return !!(rememberMe && rememberMe.status); + } +} diff --git a/src/UI/Seller/src/app/config/error-handling.config.ts b/src/UI/Seller/src/app/config/error-handling.config.ts new file mode 100644 index 00000000..d7d720ec --- /dev/null +++ b/src/UI/Seller/src/app/config/error-handling.config.ts @@ -0,0 +1,51 @@ +import { ErrorHandler, Inject, Injector } from '@angular/core'; +import { ToastrService } from 'ngx-toastr'; + +/** + * this error handler class extends angular's ErrorHandler + * in order to automatically format ordercloud error messages + * and display them in toastr + */ +export class AppErrorHandler extends ErrorHandler { + constructor(@Inject(Injector) private readonly injector: Injector) { + super(); + } + + public handleError(ex: any): void { + this.displayError(ex); + super.handleError(ex); + } + /** + * use this to display error message + * but continue exection of code + */ + public displayError(ex: any): void { + let message = ''; + if (ex && ex.error && ex.error.Errors && ex.error.Errors.length) { + const e = ex.error.Errors[0]; + message = + e.ErrorCode === 'NotFound' + ? `${e.Data.ObjectType} ${e.Data.ObjectID} not found.` + : e.Message; + } else if (ex && ex.error && ex.error['error_description']) { + message = ex.error['error_description']; + } else if (ex.error) { + message = ex.error; + } else if (ex.message) { + message = ex.message; + } else { + message = 'An error occurred'; + } + if (typeof message === 'object') { + message = JSON.stringify(message); + } + this.toastrService.error(message, 'Error', { onActivateTick: true }); + } + + /** + * Need to get ToastrService from injector rather than constructor injection to avoid cyclic dependency error + */ + private get toastrService(): ToastrService { + return this.injector.get(ToastrService); + } +} diff --git a/src/UI/Seller/src/app/config/ordercloud-sdk.config.ts b/src/UI/Seller/src/app/config/ordercloud-sdk.config.ts new file mode 100644 index 00000000..9bd7fa73 --- /dev/null +++ b/src/UI/Seller/src/app/config/ordercloud-sdk.config.ts @@ -0,0 +1,14 @@ +import { ocAppConfig } from '@app-seller/config/app.config'; +import { Configuration } from '@ordercloud/angular-sdk'; + +export function OcSDKConfig() { + const apiurl = 'https://api.ordercloud.io'; + const apiVersion = 'v1'; + const authUrl = 'https://auth.ordercloud.io/oauth/token'; + + return new Configuration({ + basePath: `${apiurl}/${apiVersion}`, + authPath: authUrl, + cookiePrefix: ocAppConfig.appname.replace(/ /g, '_').toLowerCase(), + }); +} diff --git a/src/UI/Seller/src/app/layout/header/header.component.html b/src/UI/Seller/src/app/layout/header/header.component.html index 6d608860..eea4fed1 100644 --- a/src/UI/Seller/src/app/layout/header/header.component.html +++ b/src/UI/Seller/src/app/layout/header/header.component.html @@ -1,4 +1,4 @@ -