diff --git a/package.json b/package.json index 46eeb7be2f..1f75da6c8b 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "jwt-decode": "^2.2.0", "methods": "1.1.2", "moment": "^2.22.1", - "morgan": "1.9.0", + "morgan": "^1.9.1", "ng-mocks": "^6.2.1", "ng2-file-upload": "1.2.1", "ng2-nouislider": "^1.7.11", diff --git a/src/app/core/auth/auth-object-factory.ts b/src/app/core/auth/auth-object-factory.ts index e37475d94c..02458f4e3e 100644 --- a/src/app/core/auth/auth-object-factory.ts +++ b/src/app/core/auth/auth-object-factory.ts @@ -4,6 +4,7 @@ import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { CacheableObject } from '../cache/object-cache.reducer'; +import { NormalizedGroup } from '../eperson/models/normalized-group.model'; export class AuthObjectFactory { public static getConstructor(type): GenericConstructor> { @@ -12,6 +13,10 @@ export class AuthObjectFactory { return NormalizedEPerson } + case AuthType.Group: { + return NormalizedGroup + } + case AuthType.Status: { return NormalizedAuthStatus } diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 6d782cbbe2..14784d1b55 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -6,11 +6,18 @@ import { RequestService } from '../data/request.service'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; -import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models'; +import { + AuthGetRequest, + AuthPostRequest, + GetRequest, + PostRequest, + RestRequest +} from '../data/request.models'; import { AuthStatusResponse, ErrorResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { RequestEntry } from '../data/request.reducer'; import { getResponseFromEntry } from '../shared/operators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @Injectable() export class AuthRequestService { @@ -56,8 +63,8 @@ export class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)), - tap((request: PostRequest) => this.requestService.configure(request, true)), - mergeMap((request: PostRequest) => this.fetchRequest(request)), + tap((request: GetRequest) => this.requestService.configure(request, true)), + mergeMap((request: GetRequest) => this.fetchRequest(request)), distinctUntilChanged()); } } diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 3cb00789f6..1993f6d162 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -13,6 +13,8 @@ import { RestRequest } from '../data/request.models'; import { AuthType } from './auth-type'; import { AuthStatus } from './models/auth-status.model'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -27,11 +29,10 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { - const response = this.process(data.payload, request.uuid); + const response = this.process, AuthType>(data.payload, request.uuid); return new AuthStatusResponse(response, data.statusCode, data.statusText); } else { - return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText); + return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText); } } - } diff --git a/src/app/core/auth/auth-type.ts b/src/app/core/auth/auth-type.ts index 9a248da91f..f0460449ea 100644 --- a/src/app/core/auth/auth-type.ts +++ b/src/app/core/auth/auth-type.ts @@ -1,4 +1,5 @@ export enum AuthType { EPerson = 'eperson', - Status = 'status' + Status = 'status', + Group = 'group' } diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index c461148eea..ebb4026265 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -26,21 +26,24 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-da describe('AuthService test', () => { - const mockStore: Store = jasmine.createSpyObj('store', { - dispatch: {}, - pipe: observableOf(true) - }); + let mockStore: Store; let authService: AuthService; let authRequest; - const window = new NativeWindowRef(); - const routerStub = new RouterStub(); + let window; + let routerStub; let routeStub; let storage: CookieService; let token: AuthTokenInfo; let authenticatedState; - const rdbService = getMockRemoteDataBuildService(); + let rdbService; function init() { + mockStore = jasmine.createSpyObj('store', { + dispatch: {}, + pipe: observableOf(true) + }); + window = new NativeWindowRef(); + routerStub = new RouterStub() token = new AuthTokenInfo('test_token'); token.expires = Date.now() + (1000 * 60 * 60); authenticatedState = { @@ -52,15 +55,14 @@ describe('AuthService test', () => { }; authRequest = new AuthRequestServiceStub(); routeStub = new ActivatedRouteStub(); - } + rdbService = getMockRemoteDataBuildService(); + spyOn(rdbService, 'build').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})}); - beforeEach(() => { - init(); - }); + } describe('', () => { beforeEach(() => { - + init(); TestBed.configureTestingModule({ imports: [ CommonModule, @@ -137,7 +139,8 @@ describe('AuthService test', () => { { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, { provide: RemoteDataBuildService, useValue: rdbService }, - CookieService + CookieService, + AuthService ] }).compileComponents(); })); @@ -176,8 +179,8 @@ describe('AuthService test', () => { }); describe('', () => { - beforeEach(async(() => { + init(); TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({ authReducer }) @@ -186,8 +189,10 @@ describe('AuthService test', () => { { provide: AuthRequestService, useValue: authRequest }, { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, + { provide: RemoteDataBuildService, useValue: rdbService }, ClientCookieService, - CookieService + CookieService, + AuthService ] }).compileComponents(); })); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index aac8953098..a01768e687 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -130,14 +130,10 @@ export class AuthService { headers = headers.append('Authorization', `Bearer ${token.accessToken}`); options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( + map((status) => this.rdbService.build(status)), switchMap((status: AuthStatus) => { - if (status.authenticated) { - // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... - // Review when https://jira.duraspace.org/browse/DS-4006 is fixed - // See https://github.com/DSpace/dspace-angular/issues/292 - const person$ = this.rdbService.buildSingle(status.eperson.toString()); - return person$.pipe(map((eperson) => eperson.payload)); + return status.eperson.pipe(map((eperson) => eperson.payload)); } else { throw(new Error('Not authenticated')); } @@ -226,7 +222,6 @@ export class AuthService { throw(new Error('auth.errors.invalid-user')); } })) - } /** diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 37f8d76672..6e722a80c9 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -3,8 +3,9 @@ import { AuthTokenInfo } from './auth-token-info.model'; import { EPerson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data'; import { Observable } from 'rxjs'; +import { CacheableObject } from '../../cache/object-cache.reducer'; -export class AuthStatus { +export class AuthStatus implements CacheableObject { id: string; diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index b61b11a4f2..c344683e38 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -34,15 +34,10 @@ export class ServerAuthService extends AuthService { options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( + map((status) => this.rdbService.build(status)), switchMap((status: AuthStatus) => { - if (status.authenticated) { - - // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... - const person$ = this.rdbService.buildSingle(status.eperson.toString()); - return person$.pipe( - map((eperson) => eperson.payload) - ); + return status.eperson.pipe(map((eperson) => eperson.payload)); } else { throw(new Error('Not authenticated')); } diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 7052432487..c195ca6231 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -20,6 +20,7 @@ import { CacheableObject } from '../object-cache.reducer'; import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model'; import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model'; import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model'; +import { NormalizedAuthStatus } from '../../auth/models/normalized-auth-status.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor> { diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 316e02e4be..1d63a29bc7 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -14,6 +14,7 @@ import { MetadataField } from '../metadata/metadatafield.model'; import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -202,7 +203,7 @@ export class AuthStatusResponse extends RestResponse { public toCache = false; constructor( - public response: AuthStatus, + public response: NormalizedAuthStatus, public statusCode: number, public statusText: string, ) { diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 9466b6e812..c7abcdf865 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -6,9 +6,8 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; -import { ResourceType } from '../shared/resource-type'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; + /* tslint:disable:max-classes-per-file */ export abstract class BaseResponseParsingService { @@ -26,7 +25,6 @@ export abstract class BaseResponseParsingService { } else if (Array.isArray(data)) { return this.processArray(data, requestUUID); } else if (isRestDataObject(data)) { - data = this.fixBadEPersonRestResponse(data); const object = this.deserialize(data); if (isNotEmpty(data._embedded)) { Object @@ -142,25 +140,6 @@ export abstract class BaseResponseParsingService { return this.toCache ? obj.self : obj; } - // TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed - // See https://github.com/DSpace/dspace-angular/issues/292 - private fixBadEPersonRestResponse(obj: any): any { - if (obj.type === ResourceType.EPerson) { - const groups = obj.groups; - const normGroups = []; - if (isNotEmpty(groups)) { - groups.forEach((group) => { - const parts = ['eperson', 'groups', group.uuid]; - const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString(); - normGroups.push(href); - } - ) - } - return Object.assign({}, obj, { groups: normGroups }); - } - return obj; - } - protected isSuccessStatus(statusCode: number) { return statusCode >= 200 && statusCode < 300; } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 44df7918f4..83071382ed 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { Observable, race as observableRace } from 'rxjs'; -import { filter, find, mergeMap, take } from 'rxjs/operators'; -import { remove } from 'lodash'; +import { filter, find, map, mergeMap, take } from 'rxjs/operators'; +import { cloneDeep, remove } from 'lodash'; import { AppState } from '../../app.reducer'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; @@ -118,6 +119,16 @@ export class RequestService { return this.store.pipe(select(entryFromUUIDSelector(originalUUID))) }, )) + ).pipe( + map((entry: RequestEntry) => { + // Headers break after being retrieved from the store (because of lazy initialization) + // Combining them with a new object fixes this issue + if (hasValue(entry) && hasValue(entry.request) && hasValue(entry.request.options) && hasValue(entry.request.options.headers)) { + entry = cloneDeep(entry); + entry.request.options.headers = Object.assign(new HttpHeaders(), entry.request.options.headers) + } + return entry; + }) ); } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts index 18b9090844..a7aba56a3b 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts @@ -1,8 +1,10 @@ import { TestBed, inject } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { DSpaceRESTv2Service } from './dspace-rest-v2.service'; +import { DEFAULT_CONTENT_TYPE, DSpaceRESTv2Service } from './dspace-rest-v2.service'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { HttpHeaders } from '@angular/common/http'; describe('DSpaceRESTv2Service', () => { let dSpaceRESTv2Service: DSpaceRESTv2Service; @@ -47,29 +49,72 @@ describe('DSpaceRESTv2Service', () => { const req = httpMock.expectOne(url); expect(req.request.method).toBe('GET'); - req.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText}); + req.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText }); + }); + it('should throw an error', () => { + dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => { + expect(err).toEqual(mockError); + }); + const req = httpMock.expectOne(url); + expect(req.request.method).toBe('GET'); + req.error(mockError); + }); + + it('should log an error', () => { + spyOn(console, 'log'); + + dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => { + expect(console.log).toHaveBeenCalled(); + }); + + const req = httpMock.expectOne(url); + expect(req.request.method).toBe('GET'); + req.error(mockError); + }); + + it('when no content-type header is provided, it should use application/json', () => { + dSpaceRESTv2Service.request(RestRequestMethod.POST, url, {}).subscribe(); + + const req = httpMock.expectOne(url); + expect(req.request.headers.get('Content-Type')).toContain(DEFAULT_CONTENT_TYPE); }); }); - it('should throw an error', () => { - dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => { - expect(err).toEqual(mockError); - }); - const req = httpMock.expectOne(url); - expect(req.request.method).toBe('GET'); - req.error(mockError); - }); + describe('#request', () => { + it('should return an Observable', () => { + const mockPayload = { + page: 1 + }; + const mockStatusCode = 200; + const mockStatusText = 'GREAT'; - it('should log an error', () => { - spyOn(console, 'log'); + dSpaceRESTv2Service.request(RestRequestMethod.POST, url, {}).subscribe((response) => { + expect(response).toBeTruthy(); + expect(response.statusCode).toEqual(mockStatusCode); + expect(response.statusText).toEqual(mockStatusText); + expect(response.payload.page).toEqual(mockPayload.page); + }); - dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => { - expect(console.log).toHaveBeenCalled(); + const req = httpMock.expectOne(url); + expect(req.request.method).toBe('POST'); + req.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText }); }); - const req = httpMock.expectOne(url); - expect(req.request.method).toBe('GET'); - req.error(mockError); + it('when a content-type header is provided, it should not use application/json', () => { + let headers = new HttpHeaders(); + headers = headers.set('Content-Type', 'text/html'); + dSpaceRESTv2Service.request(RestRequestMethod.POST, url, {}, { headers }).subscribe(); + + const req = httpMock.expectOne(url); + expect(req.request.headers.get('Content-Type')).not.toContain(DEFAULT_CONTENT_TYPE); + }); + + it('when no content-type header is provided, it should use application/json', () => { + dSpaceRESTv2Service.request(RestRequestMethod.POST, url, {}).subscribe(); + + const req = httpMock.expectOne(url); + expect(req.request.headers.get('Content-Type')).toContain(DEFAULT_CONTENT_TYPE); + }); }); describe('buildFormData', () => { diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index a2a9f2530c..204c782e79 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -1,14 +1,15 @@ -import {throwError as observableThrowError, Observable } from 'rxjs'; -import {catchError, map} from 'rxjs/operators'; +import { Observable, throwError as observableThrowError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http' import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { HttpObserve } from '@angular/common/http/src/client'; import { RestRequestMethod } from '../data/rest-request-method'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../shared/dspace-object.model'; +export const DEFAULT_CONTENT_TYPE = 'application/json; charset=utf-8'; export interface HttpOptions { body?: any; headers?: HttpHeaders; @@ -38,11 +39,23 @@ export class DSpaceRESTv2Service { * An Observable containing the response from the server */ get(absoluteURL: string): Observable { - return this.http.get(absoluteURL, { observe: 'response' }).pipe( - map((res: HttpResponse) => ({ payload: res.body, statusCode: res.status, statusText: res.statusText })), + const requestOptions = { + observe: 'response' as any, + headers: new HttpHeaders({'Content-Type': DEFAULT_CONTENT_TYPE}) + }; + return this.http.get(absoluteURL, requestOptions).pipe( + map((res: HttpResponse) => ({ + payload: res.body, + statusCode: res.status, + statusText: res.statusText + })), catchError((err) => { console.log('Error: ', err); - return observableThrowError({statusCode: err.status, statusText: err.statusText, message: err.message}); + return observableThrowError({ + statusCode: err.status, + statusText: err.statusText, + message: err.message + }); })); } @@ -65,17 +78,35 @@ export class DSpaceRESTv2Service { requestOptions.body = this.buildFormData(body); } requestOptions.observe = 'response'; - if (options && options.headers) { - requestOptions.headers = Object.assign(new HttpHeaders(), options.headers); - } + if (options && options.responseType) { requestOptions.responseType = options.responseType; } + + if (hasNoValue(options) || hasNoValue(options.headers)) { + requestOptions.headers = new HttpHeaders(); + } else { + requestOptions.headers = options.headers; + } + + if (!requestOptions.headers.has('Content-Type')) { + // Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers + requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE); + } return this.http.request(method, url, requestOptions).pipe( - map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.status, statusText: res.statusText })), + map((res) => ({ + payload: res.body, + headers: res.headers, + statusCode: res.status, + statusText: res.statusText + })), catchError((err) => { console.log('Error: ', err); - return observableThrowError({statusCode: err.status, statusText: err.statusText, message: err.message}); + return observableThrowError({ + statusCode: err.status, + statusText: err.statusText, + message: err.message + }); })); } diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts index 675e539d90..3cab439581 100644 --- a/src/app/shared/mocks/mock-remote-data-build.service.ts +++ b/src/app/shared/mocks/mock-remote-data-build.service.ts @@ -4,6 +4,7 @@ import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-bu import { RemoteData } from '../../core/data/remote-data'; import { RequestEntry } from '../../core/data/request.reducer'; import { hasValue } from '../empty.util'; +import { NormalizedObject } from '../../core/cache/models/normalized-object.model'; export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable>): RemoteDataBuildService { return { @@ -17,7 +18,8 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab } as RemoteData))) } }, - buildSingle: (href$: string | Observable) => observableOf(new RemoteData(false, false, true, undefined, {})) + buildSingle: (href$: string | Observable) => observableOf(new RemoteData(false, false, true, undefined, {})), + build: (normalized: NormalizedObject) => Object.create({}) } as RemoteDataBuildService; } diff --git a/yarn.lock b/yarn.lock index 786c9ade3e..50cf67c8d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6530,14 +6530,14 @@ moment@^2.22.1: resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= -morgan@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051" - integrity sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE= +morgan@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" + integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== dependencies: basic-auth "~2.0.0" debug "2.6.9" - depd "~1.1.1" + depd "~1.1.2" on-finished "~2.3.0" on-headers "~1.0.1"