mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'remotes/origin/master' into #601-resource-policies
# Conflicts: # src/app/shared/shared.module.ts
This commit is contained in:
28
.github/pull_request_template.md
vendored
Normal file
28
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
## References
|
||||
_Add references/links to any related tickets or PRs. These may include:_
|
||||
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
||||
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
||||
|
||||
## Description
|
||||
Short summary of changes (1-2 sentences).
|
||||
|
||||
## Instructions for Reviewers
|
||||
Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers.
|
||||
|
||||
List of changes in this PR:
|
||||
* First, ...
|
||||
* Second, ...
|
||||
|
||||
**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes.
|
||||
|
||||
## Checklist
|
||||
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
|
||||
|
||||
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
||||
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
|
||||
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
39
LICENSE
Normal file
39
LICENSE
Normal file
@@ -0,0 +1,39 @@
|
||||
DSpace source code BSD License:
|
||||
|
||||
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name DuraSpace nor the name of the DSpace Foundation
|
||||
nor the names of its contributors may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
|
||||
DSpace uses third-party libraries which may be distributed under
|
||||
different licenses to the above. Information about these licenses
|
||||
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
|
||||
tree. You must agree to the terms of these licenses, in addition to
|
||||
the above DSpace source code license, in order to use this software.
|
15
LICENSES_THIRD_PARTY
Normal file
15
LICENSES_THIRD_PARTY
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
DSpace uses third-party libraries which may be distributed under different licenses.
|
||||
A summary of all third-party, production dependencies used by this user interface may be found by running:
|
||||
|
||||
npx license-checker --production --summary
|
||||
|
||||
(Additional license-checker options may be found in its documentation: https://github.com/davglass/license-checker)
|
||||
|
||||
You must agree to the terms of these licenses, in addition to the DSpace source code license, in order to use this
|
||||
software.
|
||||
|
||||
PLEASE NOTE: Some third-party dependencies may be listed under multiple licenses if they are dual-licensed.
|
||||
This is especially true of anything listed as GPL (or similar), as DSpace does NOT allow for the inclusion of
|
||||
any dependencies that are solely released under GPL (or similar) terms. For more info see:
|
||||
https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions
|
@@ -38,7 +38,7 @@
|
||||
|
||||
"admin.registries.bitstream-formats.edit.extensions.label": "File extensions",
|
||||
|
||||
"admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot",
|
||||
"admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extension without the dot",
|
||||
|
||||
"admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.",
|
||||
|
||||
@@ -1115,6 +1115,10 @@
|
||||
|
||||
|
||||
|
||||
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
|
||||
|
||||
|
||||
|
||||
"journal.listelement.badge": "Journal",
|
||||
|
||||
"journal.page.description": "Description",
|
||||
@@ -1205,8 +1209,12 @@
|
||||
|
||||
"login.form.new-user": "New user? Click here to register.",
|
||||
|
||||
"login.form.or-divider": "or",
|
||||
|
||||
"login.form.password": "Password",
|
||||
|
||||
"login.form.shibboleth": "Log in with Shibboleth",
|
||||
|
||||
"login.form.submit": "Log in",
|
||||
|
||||
"login.title": "Login",
|
||||
|
@@ -918,7 +918,7 @@
|
||||
"item.edit.move.processing": "Movendo...",
|
||||
|
||||
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
||||
"item.edit.move.search.placeholder": "nsira uma consulta para procurar coleções",
|
||||
"item.edit.move.search.placeholder": "Insira uma consulta para procurar coleções",
|
||||
|
||||
// "item.edit.move.success": "The item has been moved successfully",
|
||||
"item.edit.move.success": "O item foi movido com sucesso",
|
||||
|
@@ -34,6 +34,11 @@ const COLLECTION_EDIT_PATH = 'edit';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: COLLECTION_CREATE_PATH,
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
resolve: {
|
||||
@@ -66,11 +71,6 @@ const COLLECTION_EDIT_PATH = 'edit';
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: COLLECTION_CREATE_PATH,
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -33,6 +33,11 @@ const COMMUNITY_EDIT_PATH = 'edit';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: COMMUNITY_CREATE_PATH,
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
resolve: {
|
||||
@@ -59,11 +64,6 @@ const COMMUNITY_EDIT_PATH = 'edit';
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: COMMUNITY_CREATE_PATH,
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||
<div class="simple-view-link my-3">
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||
|
@@ -63,16 +63,29 @@ export function getDSOPath(dso: DSpaceObject): string {
|
||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
||||
{
|
||||
path: 'mydspace',
|
||||
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
||||
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
|
||||
{ path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] },
|
||||
{
|
||||
path: 'workspaceitems',
|
||||
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
||||
},
|
||||
{
|
||||
path: 'workflowitems',
|
||||
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
||||
},
|
||||
{
|
||||
path: PROFILE_MODULE_PATH,
|
||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
],
|
||||
{
|
||||
|
@@ -10,6 +10,7 @@ import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest }
|
||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRequestService {
|
||||
@@ -18,7 +19,8 @@ export class AuthRequestService {
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected halService: HALEndpointService,
|
||||
protected requestService: RequestService) {
|
||||
protected requestService: RequestService,
|
||||
private http: HttpClient) {
|
||||
}
|
||||
|
||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||
@@ -38,7 +40,7 @@ export class AuthRequestService {
|
||||
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
||||
}
|
||||
|
||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<any> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||
@@ -67,4 +69,5 @@ export class AuthRequestService {
|
||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import { type } from '../../shared/ngrx/type';
|
||||
// import models
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
export const AuthActionTypes = {
|
||||
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
||||
@@ -14,12 +16,16 @@ export const AuthActionTypes = {
|
||||
AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
|
||||
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
|
||||
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
|
||||
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
||||
CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'),
|
||||
RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'),
|
||||
RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'),
|
||||
RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'),
|
||||
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
||||
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
||||
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
||||
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
||||
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
||||
RETRIEVE_TOKEN: type('dspace/auth/RETRIEVE_TOKEN'),
|
||||
ADD_MESSAGE: type('dspace/auth/ADD_MESSAGE'),
|
||||
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
||||
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||
@@ -95,7 +101,7 @@ export class AuthenticatedErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ export class AuthenticationErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +144,11 @@ export class CheckAuthenticationTokenAction implements Action {
|
||||
|
||||
/**
|
||||
* Check Authentication Token Error.
|
||||
* @class CheckAuthenticationTokenErrorAction
|
||||
* @class CheckAuthenticationTokenCookieAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class CheckAuthenticationTokenErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR;
|
||||
export class CheckAuthenticationTokenCookieAction implements Action {
|
||||
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +158,9 @@ export class CheckAuthenticationTokenErrorAction implements Action {
|
||||
*/
|
||||
export class LogOutAction implements Action {
|
||||
public type: string = AuthActionTypes.LOG_OUT;
|
||||
constructor(public payload?: any) {}
|
||||
|
||||
constructor(public payload?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +173,7 @@ export class LogOutErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +184,9 @@ export class LogOutErrorAction implements Action {
|
||||
*/
|
||||
export class LogOutSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.LOG_OUT_SUCCESS;
|
||||
constructor(public payload?: any) {}
|
||||
|
||||
constructor(public payload?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +199,7 @@ export class RedirectWhenAuthenticationIsRequiredAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.payload = message ;
|
||||
this.payload = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +213,7 @@ export class RedirectWhenTokenExpiredAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.payload = message ;
|
||||
this.payload = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +254,15 @@ export class RefreshTokenErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.REFRESH_TOKEN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authentication token.
|
||||
* @class RetrieveTokenAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveTokenAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_TOKEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign up.
|
||||
* @class RegistrationAction
|
||||
@@ -268,7 +287,7 @@ export class RegistrationErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +328,45 @@ export class ResetAuthenticationMessagesAction implements Action {
|
||||
public type: string = AuthActionTypes.RESET_MESSAGES;
|
||||
}
|
||||
|
||||
// // Next three Actions are used by dynamic login methods
|
||||
/**
|
||||
* Action that triggers an effect fetching the authentication methods enabled ant the backend
|
||||
* @class RetrieveAuthMethodsAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
||||
|
||||
payload: AuthStatus;
|
||||
|
||||
constructor(authStatus: AuthStatus) {
|
||||
this.payload = authStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Authentication methods enabled at the backend
|
||||
* @class RetrieveAuthMethodsSuccessAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
||||
payload: AuthMethod[];
|
||||
|
||||
constructor(authMethods: AuthMethod[] ) {
|
||||
this.payload = authMethods;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password as default authentication method on error
|
||||
* @class RetrieveAuthMethodsErrorAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the redirect url.
|
||||
* @class SetRedirectUrlAction
|
||||
@@ -319,7 +377,7 @@ export class SetRedirectUrlAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.payload = url ;
|
||||
this.payload = url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,13 +436,21 @@ export type AuthActions
|
||||
| AuthenticationErrorAction
|
||||
| AuthenticationSuccessAction
|
||||
| CheckAuthenticationTokenAction
|
||||
| CheckAuthenticationTokenErrorAction
|
||||
| CheckAuthenticationTokenCookieAction
|
||||
| RedirectWhenAuthenticationIsRequiredAction
|
||||
| RedirectWhenTokenExpiredAction
|
||||
| RegistrationAction
|
||||
| RegistrationErrorAction
|
||||
| RegistrationSuccessAction
|
||||
| AddAuthenticationMessageAction
|
||||
| RefreshTokenAction
|
||||
| RefreshTokenErrorAction
|
||||
| RefreshTokenSuccessAction
|
||||
| ResetAuthenticationMessagesAction
|
||||
| RetrieveAuthMethodsAction
|
||||
| RetrieveAuthMethodsSuccessAction
|
||||
| RetrieveAuthMethodsErrorAction
|
||||
| RetrieveTokenAction
|
||||
| ResetAuthenticationMessagesAction
|
||||
| RetrieveAuthenticatedEpersonAction
|
||||
| RetrieveAuthenticatedEpersonErrorAction
|
||||
|
@@ -14,19 +14,24 @@ import {
|
||||
AuthenticatedSuccessAction,
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
RefreshTokenErrorAction,
|
||||
RefreshTokenSuccessAction,
|
||||
RetrieveAuthenticatedEpersonAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
RetrieveTokenAction
|
||||
} from './auth.actions';
|
||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||
import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthState } from './auth.reducer';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
describe('AuthEffects', () => {
|
||||
let authEffects: AuthEffects;
|
||||
@@ -168,13 +173,56 @@ describe('AuthEffects', () => {
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token } });
|
||||
|
||||
const expected = cold('--b-', { b: new CheckAuthenticationTokenErrorAction() });
|
||||
const expected = cold('--b-', { b: new CheckAuthenticationTokenCookieAction() });
|
||||
|
||||
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('checkTokenCookie$', () => {
|
||||
|
||||
describe('when check token succeeded', () => {
|
||||
it('should return a RETRIEVE_TOKEN action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is true', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||
observableOf(
|
||||
{
|
||||
authenticated: true
|
||||
})
|
||||
);
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveTokenAction() });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||
observableOf(
|
||||
{ authenticated: false })
|
||||
);
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus) });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check token failed', () => {
|
||||
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE, payload: token } });
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticatedErrorAction(new Error('Message Error test')) });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveAuthenticatedEperson$', () => {
|
||||
|
||||
describe('when request is successful', () => {
|
||||
@@ -231,6 +279,38 @@ describe('AuthEffects', () => {
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveToken$', () => {
|
||||
describe('when user is authenticated', () => {
|
||||
it('should return a AUTHENTICATE_SUCCESS action in response to a RETRIEVE_TOKEN action', () => {
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: AuthActionTypes.RETRIEVE_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) });
|
||||
|
||||
expect(authEffects.retrieveToken$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not authenticated', () => {
|
||||
it('should return a AUTHENTICATE_ERROR action in response to a RETRIEVE_TOKEN action', () => {
|
||||
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: AuthActionTypes.RETRIEVE_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) });
|
||||
|
||||
expect(authEffects.retrieveToken$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logOut$', () => {
|
||||
|
||||
describe('when refresh token succeeded', () => {
|
||||
@@ -256,4 +336,29 @@ describe('AuthEffects', () => {
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveMethods$', () => {
|
||||
|
||||
describe('when retrieve authentication methods succeeded', () => {
|
||||
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock) });
|
||||
|
||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when retrieve authentication methods failed', () => {
|
||||
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction() });
|
||||
|
||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { of as observableOf, Observable } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators';
|
||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// import @ngrx
|
||||
@@ -9,6 +9,14 @@ import { Action, select, Store } from '@ngrx/store';
|
||||
|
||||
// import services
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
// import actions
|
||||
import {
|
||||
AuthActionTypes,
|
||||
@@ -18,7 +26,7 @@ import {
|
||||
AuthenticatedSuccessAction,
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
RefreshTokenAction,
|
||||
@@ -29,14 +37,12 @@ import {
|
||||
RegistrationSuccessAction,
|
||||
RetrieveAuthenticatedEpersonAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
RetrieveTokenAction
|
||||
} from './auth.actions';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
|
||||
@Injectable()
|
||||
export class AuthEffects {
|
||||
@@ -47,45 +53,45 @@ export class AuthEffects {
|
||||
*/
|
||||
@Effect()
|
||||
public authenticate$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATE),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
||||
take(1),
|
||||
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATE),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
||||
take(1),
|
||||
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticated$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED),
|
||||
switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser(action.payload).pipe(
|
||||
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED),
|
||||
switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser(action.payload).pipe(
|
||||
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
);
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@Effect({ dispatch: false })
|
||||
public authenticatedError$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_ERROR),
|
||||
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED_ERROR),
|
||||
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveAuthenticatedEperson$: Observable<Action> = this.actions$.pipe(
|
||||
@@ -99,42 +105,71 @@ export class AuthEffects {
|
||||
|
||||
@Effect()
|
||||
public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
|
||||
switchMap(() => {
|
||||
return this.authService.hasValidAuthenticationToken().pipe(
|
||||
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
||||
catchError((error) => observableOf(new CheckAuthenticationTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
switchMap(() => {
|
||||
return this.authService.hasValidAuthenticationToken().pipe(
|
||||
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
||||
catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public checkTokenCookie$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE),
|
||||
switchMap(() => {
|
||||
return this.authService.checkAuthenticationCookie().pipe(
|
||||
map((response: AuthStatus) => {
|
||||
if (response.authenticated) {
|
||||
return new RetrieveTokenAction();
|
||||
} else {
|
||||
return new RetrieveAuthMethodsAction(response);
|
||||
}
|
||||
}),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public createUser$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.REGISTRATION),
|
||||
debounceTime(500), // to remove when functionality is implemented
|
||||
switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload).pipe(
|
||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.REGISTRATION),
|
||||
debounceTime(500), // to remove when functionality is implemented
|
||||
switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload).pipe(
|
||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveToken$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.RETRIEVE_TOKEN),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.refreshAuthenticationToken(null).pipe(
|
||||
take(1),
|
||||
map((token: AuthTokenInfo) => new AuthenticationSuccessAction(token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
|
||||
switchMap((action: RefreshTokenAction) => {
|
||||
return this.authService.refreshAuthenticationToken(action.payload).pipe(
|
||||
map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
|
||||
catchError((error) => observableOf(new RefreshTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
switchMap((action: RefreshTokenAction) => {
|
||||
return this.authService.refreshAuthenticationToken(action.payload).pipe(
|
||||
map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
|
||||
catchError((error) => observableOf(new RefreshTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@Effect({ dispatch: false })
|
||||
public refreshTokenSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
|
||||
tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
|
||||
);
|
||||
ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
|
||||
tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
|
||||
);
|
||||
|
||||
/**
|
||||
* When the store is rehydrated in the browser,
|
||||
@@ -188,6 +223,19 @@ export class AuthEffects {
|
||||
tap(() => this.authService.redirectToLoginWhenTokenExpired())
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveMethods$: Observable<Action> = this.actions$
|
||||
.pipe(
|
||||
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
||||
switchMap((action: RetrieveAuthMethodsAction) => {
|
||||
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload)
|
||||
.pipe(
|
||||
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)),
|
||||
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction()))
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Actions} actions$
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandler,
|
||||
HttpHeaders,
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
@@ -17,10 +18,12 @@ import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty, isUndefined, isNotNull } from '../../shared/empty.util';
|
||||
import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
@@ -30,17 +33,33 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
// we're creating a refresh token request list
|
||||
protected refreshTokenRequestUrls = [];
|
||||
|
||||
constructor(private inj: Injector, private router: Router, private store: Store<AppState>) { }
|
||||
constructor(private inj: Injector, private router: Router, private store: Store<AppState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response status code is 401
|
||||
*
|
||||
* @param response
|
||||
*/
|
||||
private isUnauthorized(response: HttpResponseBase): boolean {
|
||||
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||
return response.status === 401;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response status code is 200 or 204
|
||||
*
|
||||
* @param response
|
||||
*/
|
||||
private isSuccess(response: HttpResponseBase): boolean {
|
||||
return (response.status === 200 || response.status === 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if http request is to authn endpoint
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http && http.url
|
||||
&& (http.url.endsWith('/authn/login')
|
||||
@@ -48,18 +67,131 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
|| http.url.endsWith('/authn/status'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response is from a login request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/login');
|
||||
return http.url && http.url.endsWith('/authn/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response is from a logout request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isLogoutResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/logout');
|
||||
}
|
||||
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus {
|
||||
/**
|
||||
* Check if response is from a status request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isStatusResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract location url from the WWW-Authenticate header
|
||||
*
|
||||
* @param header
|
||||
*/
|
||||
private parseLocation(header: string): string {
|
||||
let location = header.trim();
|
||||
location = location.replace('location="', '');
|
||||
location = location.replace('"', '');
|
||||
let re = /%3A%2F%2F/g;
|
||||
location = location.replace(re, '://');
|
||||
re = /%3A/g;
|
||||
location = location.replace(re, ':');
|
||||
return location.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort authentication methods list
|
||||
*
|
||||
* @param authMethodModels
|
||||
*/
|
||||
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
|
||||
const sortedAuthMethodModels: AuthMethod[] = [];
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType === AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType !== AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
return sortedAuthMethodModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract authentication methods list from the WWW-Authenticate headers
|
||||
*
|
||||
* @param headers
|
||||
*/
|
||||
private parseAuthMethodsFromHeaders(headers: HttpHeaders): AuthMethod[] {
|
||||
let authMethodModels: AuthMethod[] = [];
|
||||
if (isNotEmpty(headers.get('www-authenticate'))) {
|
||||
// get the realms from the header - a realm is a single auth method
|
||||
const completeWWWauthenticateHeader = headers.get('www-authenticate');
|
||||
const regex = /(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g;
|
||||
const realms = completeWWWauthenticateHeader.match(regex);
|
||||
|
||||
// tslint:disable-next-line:forin
|
||||
for (const j in realms) {
|
||||
|
||||
const splittedRealm = realms[j].split(', ');
|
||||
const methodName = splittedRealm[0].split(' ')[0].trim();
|
||||
|
||||
let authMethodModel: AuthMethod;
|
||||
if (splittedRealm.length === 1) {
|
||||
authMethodModel = new AuthMethod(methodName);
|
||||
authMethodModels.push(authMethodModel);
|
||||
} else if (splittedRealm.length > 1) {
|
||||
let location = splittedRealm[1];
|
||||
location = this.parseLocation(location);
|
||||
authMethodModel = new AuthMethod(methodName, location);
|
||||
authMethodModels.push(authMethodModel);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the email + password login component gets rendered first
|
||||
authMethodModels = this.sortAuthMethods(authMethodModels);
|
||||
} else {
|
||||
authMethodModels.push(new AuthMethod(AuthMethodType.Password));
|
||||
}
|
||||
|
||||
return authMethodModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an AuthStatus object
|
||||
*
|
||||
* @param authenticated
|
||||
* @param accessToken
|
||||
* @param error
|
||||
* @param httpHeaders
|
||||
*/
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string, httpHeaders?: HttpHeaders): AuthStatus {
|
||||
const authStatus = new AuthStatus();
|
||||
// let authMethods: AuthMethodModel[];
|
||||
if (httpHeaders) {
|
||||
authStatus.authMethods = this.parseAuthMethodsFromHeaders(httpHeaders);
|
||||
}
|
||||
|
||||
authStatus.id = null;
|
||||
|
||||
authStatus.okay = true;
|
||||
// authStatus.authMethods = authMethods;
|
||||
|
||||
if (authenticated) {
|
||||
authStatus.authenticated = true;
|
||||
authStatus.token = new AuthTokenInfo(accessToken);
|
||||
@@ -70,12 +202,18 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
return authStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept method
|
||||
* @param req
|
||||
* @param next
|
||||
*/
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
const authService = this.inj.get(AuthService);
|
||||
|
||||
const token = authService.getToken();
|
||||
let newReq;
|
||||
const token: AuthTokenInfo = authService.getToken();
|
||||
let newReq: HttpRequest<any>;
|
||||
let authorization: string;
|
||||
|
||||
if (authService.isTokenExpired()) {
|
||||
authService.setRedirectUrl(this.router.url);
|
||||
@@ -96,30 +234,41 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
}
|
||||
});
|
||||
// Get the auth header from the service.
|
||||
const Authorization = authService.buildAuthHeader(token);
|
||||
authorization = authService.buildAuthHeader(token);
|
||||
// Clone the request to add the new header.
|
||||
newReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
||||
newReq = req.clone({ headers: req.headers.set('authorization', authorization) });
|
||||
} else {
|
||||
newReq = req;
|
||||
newReq = req.clone();
|
||||
}
|
||||
|
||||
// Pass on the new request instead of the original request.
|
||||
return next.handle(newReq).pipe(
|
||||
// tap((response) => console.log('next.handle: ', response)),
|
||||
map((response) => {
|
||||
// Intercept a Login/Logout response
|
||||
if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) {
|
||||
if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) {
|
||||
// It's a success Login/Logout response
|
||||
let authRes: HttpResponse<any>;
|
||||
if (this.isLoginResponse(response)) {
|
||||
// login successfully
|
||||
const newToken = response.headers.get('authorization');
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(true, newToken)});
|
||||
authRes = response.clone({
|
||||
body: this.makeAuthStatusObject(true, newToken)
|
||||
});
|
||||
|
||||
// clean eventually refresh Requests list
|
||||
this.refreshTokenRequestUrls = [];
|
||||
} else if (this.isStatusResponse(response)) {
|
||||
authRes = response.clone({
|
||||
body: Object.assign(response.body, {
|
||||
authMethods: this.parseAuthMethodsFromHeaders(response.headers)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// logout successfully
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
||||
authRes = response.clone({
|
||||
body: this.makeAuthStatusObject(false)
|
||||
});
|
||||
}
|
||||
return authRes;
|
||||
} else {
|
||||
@@ -129,13 +278,15 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
catchError((error, caught) => {
|
||||
// Intercept an error response
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
|
||||
// Checks if is a response from a request to an authentication endpoint
|
||||
if (this.isAuthRequest(error)) {
|
||||
// clean eventually refresh Requests list
|
||||
this.refreshTokenRequestUrls = [];
|
||||
|
||||
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
||||
const authResponse = new HttpResponse({
|
||||
body: this.makeAuthStatusObject(false, null, error.error),
|
||||
body: this.makeAuthStatusObject(false, null, error.error, error.headers),
|
||||
headers: error.headers,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
@@ -17,11 +17,19 @@ import {
|
||||
RefreshTokenAction,
|
||||
RefreshTokenErrorAction,
|
||||
RefreshTokenSuccessAction,
|
||||
ResetAuthenticationMessagesAction, RetrieveAuthenticatedEpersonErrorAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||
ResetAuthenticationMessagesAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
|
||||
describe('authReducer', () => {
|
||||
|
||||
@@ -157,18 +165,18 @@ describe('authReducer', () => {
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_ERROR action', () => {
|
||||
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
};
|
||||
const action = new CheckAuthenticationTokenErrorAction();
|
||||
const action = new CheckAuthenticationTokenCookieAction();
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
loading: true,
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -451,4 +459,63 @@ describe('authReducer', () => {
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
};
|
||||
const action = new RetrieveAuthMethodsAction(new AuthStatus());
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_SUCCESS action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
const authMethods = [
|
||||
new AuthMethod(AuthMethodType.Password),
|
||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||
];
|
||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: authMethods
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
|
||||
const action = new RetrieveAuthMethodsErrorAction();
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
@@ -8,12 +8,16 @@ import {
|
||||
LogOutErrorAction,
|
||||
RedirectWhenAuthenticationIsRequiredAction,
|
||||
RedirectWhenTokenExpiredAction,
|
||||
RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RefreshTokenSuccessAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
// import models
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
|
||||
/**
|
||||
* The auth state.
|
||||
@@ -47,6 +51,10 @@ export interface AuthState {
|
||||
|
||||
// the authenticated user
|
||||
user?: EPerson;
|
||||
|
||||
// all authentication Methods enabled at the backend
|
||||
authMethods?: AuthMethod[];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +64,7 @@ const initialState: AuthState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -75,6 +84,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATED:
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
@@ -113,21 +124,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
loading: false
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATED:
|
||||
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
||||
case AuthActionTypes.LOG_OUT:
|
||||
return state;
|
||||
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loading: false
|
||||
});
|
||||
|
||||
case AuthActionTypes.LOG_OUT_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
authenticated: true,
|
||||
@@ -192,6 +192,24 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
info: undefined,
|
||||
});
|
||||
|
||||
// next three cases are used by dynamic rendering of login methods
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
||||
});
|
||||
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||
});
|
||||
|
||||
case AuthActionTypes.SET_REDIRECT_URL:
|
||||
return Object.assign({}, state, {
|
||||
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||
|
@@ -27,6 +27,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { authMethodsMock } from '../../shared/testing/auth-service-stub';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
|
||||
describe('AuthService test', () => {
|
||||
|
||||
@@ -144,6 +146,26 @@ describe('AuthService test', () => {
|
||||
expect(authService.logout.bind(null)).toThrow();
|
||||
});
|
||||
|
||||
it('should return the authentication status object to check an Authentication Cookie', () => {
|
||||
authService.checkAuthenticationCookie().subscribe((status: AuthStatus) => {
|
||||
expect(status).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the authentication methods available', () => {
|
||||
const authStatus = new AuthStatus();
|
||||
|
||||
authService.retrieveAuthMethodsFromAuthStatus(authStatus).subscribe((authMethods: AuthMethod[]) => {
|
||||
expect(authMethods).toBeDefined();
|
||||
expect(authMethods.length).toBe(0);
|
||||
});
|
||||
|
||||
authStatus.authMethods = authMethodsMock;
|
||||
authService.retrieveAuthMethodsFromAuthStatus(authStatus).subscribe((authMethods: AuthMethod[]) => {
|
||||
expect(authMethods).toBeDefined();
|
||||
expect(authMethods.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
|
@@ -18,16 +18,20 @@ import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/emp
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||
import {
|
||||
CheckAuthenticationTokenAction,
|
||||
ResetAuthenticationMessagesAction,
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
export const LOGOUT_ROUTE = '/logout';
|
||||
|
||||
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
||||
|
||||
/**
|
||||
@@ -114,6 +118,21 @@ export class AuthService {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into the request cookie
|
||||
*/
|
||||
public checkAuthenticationCookie(): Observable<AuthStatus> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the user is authenticated
|
||||
* @returns {Observable<boolean>}
|
||||
@@ -154,10 +173,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||
* Checks if token is present into browser storage and is valid.
|
||||
*/
|
||||
public checkAuthenticationToken() {
|
||||
return
|
||||
this.store.dispatch(new CheckAuthenticationTokenAction());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,8 +206,11 @@ export class AuthService {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
if (token && token.accessToken) {
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
}
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.postToEndpoint('login', {}, options).pipe(
|
||||
map((status: AuthStatus) => {
|
||||
if (status.authenticated) {
|
||||
@@ -206,6 +228,18 @@ export class AuthService {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authentication methods available
|
||||
* @returns {User}
|
||||
*/
|
||||
public retrieveAuthMethodsFromAuthStatus(status: AuthStatus): Observable<AuthMethod[]> {
|
||||
let authMethods: AuthMethod[] = [];
|
||||
if (isNotEmpty(status.authMethods)) {
|
||||
authMethods = status.authMethods;
|
||||
}
|
||||
return observableOf(authMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* @returns {User}
|
||||
|
@@ -1,17 +1,14 @@
|
||||
|
||||
import {take} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import {Observable, of} from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
// reducers
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes
|
||||
|
@@ -12,6 +12,7 @@ import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
import { AuthError } from './auth-error.model';
|
||||
import { AUTH_STATUS } from './auth-status.resource-type';
|
||||
import { AuthTokenInfo } from './auth-token-info.model';
|
||||
import { AuthMethod } from './auth.method';
|
||||
|
||||
/**
|
||||
* Object that represents the authenticated status of a user
|
||||
@@ -79,5 +80,13 @@ export class AuthStatus implements CacheableObject {
|
||||
* Authentication error if there was one for this status
|
||||
*/
|
||||
// TODO should be refactored to use the RemoteData error
|
||||
@autoserialize
|
||||
error?: AuthError;
|
||||
|
||||
/**
|
||||
* All authentication methods enabled at the backend
|
||||
*/
|
||||
@autoserialize
|
||||
authMethods: AuthMethod[];
|
||||
|
||||
}
|
||||
|
7
src/app/core/auth/models/auth.method-type.ts
Normal file
7
src/app/core/auth/models/auth.method-type.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum AuthMethodType {
|
||||
Password = 'password',
|
||||
Shibboleth = 'shibboleth',
|
||||
Ldap = 'ldap',
|
||||
Ip = 'ip',
|
||||
X509 = 'x509'
|
||||
}
|
38
src/app/core/auth/models/auth.method.ts
Normal file
38
src/app/core/auth/models/auth.method.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { AuthMethodType } from './auth.method-type';
|
||||
|
||||
export class AuthMethod {
|
||||
authMethodType: AuthMethodType;
|
||||
location?: string;
|
||||
|
||||
// isStandalonePage? = true;
|
||||
|
||||
constructor(authMethodName: string, location?: string) {
|
||||
switch (authMethodName) {
|
||||
case 'ip': {
|
||||
this.authMethodType = AuthMethodType.Ip;
|
||||
break;
|
||||
}
|
||||
case 'ldap': {
|
||||
this.authMethodType = AuthMethodType.Ldap;
|
||||
break;
|
||||
}
|
||||
case 'shibboleth': {
|
||||
this.authMethodType = AuthMethodType.Shibboleth;
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
case 'x509': {
|
||||
this.authMethodType = AuthMethodType.X509;
|
||||
break;
|
||||
}
|
||||
case 'password': {
|
||||
this.authMethodType = AuthMethodType.Password;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -107,6 +107,17 @@ const _getRegistrationError = (state: AuthState) => state.error;
|
||||
*/
|
||||
const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
||||
|
||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||
|
||||
/**
|
||||
* Returns the authentication methods enabled at the backend
|
||||
* @function getAuthenticationMethods
|
||||
* @param {AuthState} state
|
||||
* @param {any} props
|
||||
* @return {any}
|
||||
*/
|
||||
export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods);
|
||||
|
||||
/**
|
||||
* Returns the authenticated user
|
||||
* @function getAuthenticatedUser
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { CheckAuthenticationTokenAction } from './auth.actions';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
@@ -43,10 +43,23 @@ export class ServerAuthService extends AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||
* Checks if token is present into the request cookie
|
||||
*/
|
||||
public checkAuthenticationToken() {
|
||||
this.store.dispatch(new CheckAuthenticationTokenAction())
|
||||
public checkAuthenticationCookie(): Observable<AuthStatus> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
if (isNotEmpty(this.req.protocol) && isNotEmpty(this.req.header('host'))) {
|
||||
const referer = this.req.protocol + '://' + this.req.header('host') + this.req.path;
|
||||
// use to allow the rest server to identify the real origin on SSR
|
||||
headers = headers.append('X-Requested-With', referer);
|
||||
}
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,24 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import {
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
|
||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
|
||||
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
|
||||
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config';
|
||||
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
|
||||
import { FormService } from '../shared/form/form.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||
|
||||
import {
|
||||
MOCK_RESPONSE_MAP,
|
||||
MockResponseMap,
|
||||
@@ -48,7 +43,6 @@ import { SubmissionUploadsModel } from './config/models/config-submission-upload
|
||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||
|
||||
import { coreEffects } from './core.effects';
|
||||
import { coreReducers } from './core.reducers';
|
||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||
@@ -103,7 +97,6 @@ import { RegistryService } from './registry/registry.service';
|
||||
import { RoleService } from './roles/role.service';
|
||||
|
||||
import { ApiService } from './services/api.service';
|
||||
import { RouteService } from './services/route.service';
|
||||
import { ServerResponseService } from './services/server-response.service';
|
||||
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
||||
import { BitstreamFormat } from './shared/bitstream-format.model';
|
||||
@@ -179,7 +172,11 @@ const PROVIDERS = [
|
||||
SiteDataService,
|
||||
DSOResponseParsingService,
|
||||
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
|
||||
{ provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [GLOBAL_CONFIG, MOCK_RESPONSE_MAP, HttpClient]},
|
||||
{
|
||||
provide: DSpaceRESTv2Service,
|
||||
useFactory: restServiceFactory,
|
||||
deps: [GLOBAL_CONFIG, MOCK_RESPONSE_MAP, HttpClient]
|
||||
},
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService,
|
||||
@@ -215,7 +212,6 @@ const PROVIDERS = [
|
||||
BrowseItemsResponseParsingService,
|
||||
BrowseService,
|
||||
ConfigResponseParsingService,
|
||||
RouteService,
|
||||
SubmissionDefinitionsConfigService,
|
||||
SubmissionFormsConfigService,
|
||||
SubmissionRestService,
|
||||
|
@@ -230,6 +230,8 @@ export class AuthPostRequest extends PostRequest {
|
||||
}
|
||||
|
||||
export class AuthGetRequest extends GetRequest {
|
||||
forceBypassCache = true;
|
||||
|
||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||
super(uuid, href, null, options);
|
||||
}
|
||||
|
@@ -90,6 +90,14 @@ export class DSpaceRESTv2Service {
|
||||
requestOptions.headers = options.headers;
|
||||
}
|
||||
|
||||
if (options && options.params) {
|
||||
requestOptions.params = options.params;
|
||||
}
|
||||
|
||||
if (options && options.withCredentials) {
|
||||
requestOptions.withCredentials = options.withCredentials;
|
||||
}
|
||||
|
||||
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);
|
||||
|
@@ -59,7 +59,9 @@ export function parameterSelector(key: string, paramsSelector: (state: CoreState
|
||||
/**
|
||||
* Service to keep track of the current query parameters
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RouteService {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private store: Store<CoreState>) {
|
||||
this.saveRouting();
|
||||
|
@@ -1,26 +1,33 @@
|
||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" (click)="$event.stopPropagation();">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
||||
(click)="$event.stopPropagation();">
|
||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle class="px-1">{{ 'nav.login' | translate }}</a>
|
||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
|
||||
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle
|
||||
class="px-1">{{ 'nav.login' | translate }}</a>
|
||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||
aria-labelledby="dropdownLogin">
|
||||
<ds-log-in
|
||||
[isStandalonePage]="false"></ds-log-in>
|
||||
[isStandalonePage]="false"></ds-log-in>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1" >{{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
|
||||
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1">{{ 'nav.login' | translate }}<span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
|
||||
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i
|
||||
class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
|
||||
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||
<ds-user-menu></ds-user-menu>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="logoutLink" routerLink="/logout" routerLinkActive="active" class="px-1"><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i><span class="sr-only">(current)</span></a>
|
||||
<a id="logoutLink" routerLink="/logout" routerLinkActive="active" class="px-1"><i
|
||||
class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i><span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@@ -137,7 +137,7 @@ describe('ComColFormComponent', () => {
|
||||
type: Community.type
|
||||
},
|
||||
),
|
||||
uploader: {} as any,
|
||||
uploader: undefined,
|
||||
deleteLogo: false
|
||||
}
|
||||
);
|
||||
|
@@ -39,7 +39,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
|
||||
/**
|
||||
* The logo uploader component
|
||||
*/
|
||||
@ViewChild(UploaderComponent, {static: true}) uploaderComponent: UploaderComponent;
|
||||
@ViewChild(UploaderComponent, {static: false}) uploaderComponent: UploaderComponent;
|
||||
|
||||
/**
|
||||
* DSpaceObject that the form represents
|
||||
|
@@ -0,0 +1,5 @@
|
||||
<ds-alert *ngIf="isLatestVersion$ && !(isLatestVersion$ | async)"
|
||||
[content]="('item.version.notice' | translate:{ destination: getItemPage(((latestVersion$ | async)?.item | async)?.payload) })"
|
||||
[dismissible]="false"
|
||||
[type]="AlertTypeEnum.Warning">
|
||||
</ds-alert>
|
@@ -0,0 +1,93 @@
|
||||
import { ItemVersionsNoticeComponent } from './item-versions-notice.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
||||
import { Version } from '../../../../core/shared/version.model';
|
||||
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../testing/utils';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ItemVersionsNoticeComponent', () => {
|
||||
let component: ItemVersionsNoticeComponent;
|
||||
let fixture: ComponentFixture<ItemVersionsNoticeComponent>;
|
||||
|
||||
const versionHistory = Object.assign(new VersionHistory(), {
|
||||
id: '1'
|
||||
});
|
||||
const firstVersion = Object.assign(new Version(), {
|
||||
id: '1',
|
||||
version: 1,
|
||||
created: new Date(2020, 1, 1),
|
||||
summary: 'first version',
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const latestVersion = Object.assign(new Version(), {
|
||||
id: '2',
|
||||
version: 2,
|
||||
summary: 'latest version',
|
||||
created: new Date(2020, 1, 2),
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const versions = [latestVersion, firstVersion];
|
||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||
const firstItem = Object.assign(new Item(), {
|
||||
id: 'first_item_id',
|
||||
uuid: 'first_item_id',
|
||||
handle: '123456789/1',
|
||||
version: createSuccessfulRemoteDataObject$(firstVersion)
|
||||
});
|
||||
const latestItem = Object.assign(new Item(), {
|
||||
id: 'latest_item_id',
|
||||
uuid: 'latest_item_id',
|
||||
handle: '123456789/2',
|
||||
version: createSuccessfulRemoteDataObject$(latestVersion)
|
||||
});
|
||||
firstVersion.item = createSuccessfulRemoteDataObject$(firstItem);
|
||||
latestVersion.item = createSuccessfulRemoteDataObject$(latestItem);
|
||||
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
|
||||
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemVersionsNoticeComponent],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: VersionHistoryDataService, useValue: versionHistoryService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('when the item is the latest version', () => {
|
||||
beforeEach(() => {
|
||||
initComponentWithItem(latestItem);
|
||||
});
|
||||
|
||||
it('should not display a notice', () => {
|
||||
const alert = fixture.debugElement.query(By.css('ds-alert'));
|
||||
expect(alert).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not the latest version', () => {
|
||||
beforeEach(() => {
|
||||
initComponentWithItem(firstItem);
|
||||
});
|
||||
|
||||
it('should display a notice', () => {
|
||||
const alert = fixture.debugElement.query(By.css('ds-alert'));
|
||||
expect(alert).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function initComponentWithItem(item: Item) {
|
||||
fixture = TestBed.createComponent(ItemVersionsNoticeComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
}
|
||||
});
|
@@ -0,0 +1,114 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../search/paginated-search-options.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
||||
import { Version } from '../../../../core/shared/version.model';
|
||||
import { hasValue } from '../../../empty.util';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { filter, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { AlertType } from '../../../alert/aletr-type';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { getItemPageRoute } from '../../../../+item-page/item-page-routing.module';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-versions-notice',
|
||||
templateUrl: './item-versions-notice.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for displaying a warning notice when the item is not the latest version within its version history
|
||||
* The notice contains a link to the latest version's item page
|
||||
*/
|
||||
export class ItemVersionsNoticeComponent implements OnInit {
|
||||
/**
|
||||
* The item to display a version notice for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The item's version
|
||||
*/
|
||||
versionRD$: Observable<RemoteData<Version>>;
|
||||
|
||||
/**
|
||||
* The item's full version history
|
||||
*/
|
||||
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
|
||||
|
||||
/**
|
||||
* The latest version of the item's version history
|
||||
*/
|
||||
latestVersion$: Observable<Version>;
|
||||
|
||||
/**
|
||||
* Is the item's version equal to the latest version from the version history?
|
||||
* This will determine whether or not to display a notice linking to the latest version
|
||||
*/
|
||||
isLatestVersion$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Pagination options to fetch a single version on the first page (this is the latest version in the history)
|
||||
*/
|
||||
latestVersionOptions = Object.assign(new PaginationComponentOptions(),{
|
||||
id: 'item-newest-version-options',
|
||||
currentPage: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(private versionHistoryService: VersionHistoryDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component's observables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const latestVersionSearch = new PaginatedSearchOptions({pagination: this.latestVersionOptions});
|
||||
if (hasValue(this.item.version)) {
|
||||
this.versionRD$ = this.item.version;
|
||||
this.versionHistoryRD$ = this.versionRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((version: Version) => version.versionhistory)
|
||||
);
|
||||
const versionHistory$ = this.versionHistoryRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
this.latestVersion$ = versionHistory$.pipe(
|
||||
switchMap((versionHistory: VersionHistory) =>
|
||||
this.versionHistoryService.getVersions(versionHistory.id, latestVersionSearch, followLink('item'))),
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((versions) => versions.page.length > 0),
|
||||
map((versions) => versions.page[0])
|
||||
);
|
||||
|
||||
this.isLatestVersion$ = observableCombineLatest(
|
||||
this.versionRD$.pipe(getAllSucceededRemoteData(), getRemoteDataPayload()), this.latestVersion$
|
||||
).pipe(
|
||||
map(([itemVersion, latestVersion]: [Version, Version]) => itemVersion.id === latestVersion.id),
|
||||
startWith(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item page url
|
||||
* @param item The item for which the url is requested
|
||||
*/
|
||||
getItemPage(item: Item): string {
|
||||
if (hasValue(item)) {
|
||||
return getItemPageRoute(item.id);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<ng-container
|
||||
*ngComponentOutlet="getAuthMethodContent();
|
||||
injector: objectInjector;">
|
||||
</ng-container>
|
||||
|
@@ -0,0 +1,21 @@
|
||||
:host ::ng-deep .card {
|
||||
margin-bottom: $submission-sections-margin-bottom;
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
.section-focus {
|
||||
border-radius: $border-radius;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
||||
|
||||
// TODO to remove the following when upgrading @ng-bootstrap
|
||||
:host ::ng-deep .card:first-of-type {
|
||||
border-bottom: $card-border-width solid $card-border-color !important;
|
||||
border-bottom-left-radius: $card-border-radius !important;
|
||||
border-bottom-right-radius: $card-border-radius !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .card-header button {
|
||||
box-shadow: none !important;
|
||||
width: 100%;
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInContainerComponent } from './log-in-container.component';
|
||||
import { authReducer } from '../../../core/auth/auth.reducer';
|
||||
import { SharedModule } from '../../shared.module';
|
||||
import { createTestComponent } from '../../testing/utils';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
import { AuthServiceStub } from '../../testing/auth-service-stub';
|
||||
|
||||
describe('LogInContainerComponent', () => {
|
||||
|
||||
let component: LogInContainerComponent;
|
||||
let fixture: ComponentFixture<LogInContainerComponent>;
|
||||
|
||||
const authMethod = new AuthMethod('password');
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: AuthService, useClass: AuthServiceStub},
|
||||
LogInContainerComponent
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `<ds-log-in-container [authMethod]="authMethod"> </ds-log-in-container>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create LogInContainerComponent', inject([LogInContainerComponent], (app: LogInContainerComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogInContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
spyOn(component, 'getAuthMethodContent').and.callThrough();
|
||||
component.authMethod = authMethod;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
component = null;
|
||||
});
|
||||
|
||||
it('should inject component properly', () => {
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getAuthMethodContent).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
isStandalonePage = true;
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { rendersAuthMethodType } from '../methods/log-in.methods-decorator';
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
|
||||
/**
|
||||
* This component represents a component container for log-in methods available.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-log-in-container',
|
||||
templateUrl: './log-in-container.component.html',
|
||||
styleUrls: ['./log-in-container.component.scss']
|
||||
})
|
||||
export class LogInContainerComponent implements OnInit {
|
||||
|
||||
@Input() authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* Injector to inject a section component with the @Input parameters
|
||||
* @type {Injector}
|
||||
*/
|
||||
public objectInjector: Injector;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {Injector} injector
|
||||
*/
|
||||
constructor(private injector: Injector) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.objectInjector = Injector.create({
|
||||
providers: [
|
||||
{ provide: 'authMethodProvider', useFactory: () => (this.authMethod), deps: [] },
|
||||
],
|
||||
parent: this.injector
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the correct component based on the AuthMethod's type
|
||||
*/
|
||||
getAuthMethodContent(): string {
|
||||
return rendersAuthMethodType(this.authMethod.authMethodType)
|
||||
}
|
||||
|
||||
}
|
@@ -1,28 +1,13 @@
|
||||
<ds-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-loading>
|
||||
<form *ngIf="!(loading | async) && !(isAuthenticated | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
|
||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||
<input id="inputEmail"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
class="form-control form-control-lg position-relative"
|
||||
formControlName="email"
|
||||
placeholder="{{'login.form.email' | translate}}"
|
||||
required
|
||||
type="email">
|
||||
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
|
||||
<input id="inputPassword"
|
||||
autocomplete="off"
|
||||
class="form-control form-control-lg position-relative mb-3"
|
||||
placeholder="{{'login.form.password' | translate}}"
|
||||
formControlName="password"
|
||||
required
|
||||
type="password">
|
||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ (error | async) | translate }}</div>
|
||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" @fadeOut>{{ (message | async) | translate }}</div>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
|
||||
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 login-container">
|
||||
<ng-container *ngFor="let authMethod of (authMethods | async); let i = index">
|
||||
<div *ngIf="i === 1" class="text-center mt-2">
|
||||
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
|
||||
</div>
|
||||
<ds-log-in-container [authMethod]="authMethod"></ds-log-in-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
|
||||
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
@@ -1,13 +1,3 @@
|
||||
.form-login .form-control:focus {
|
||||
z-index: 2;
|
||||
.login-container {
|
||||
max-width: 350px;
|
||||
}
|
||||
.form-login input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-login input[type="password"] {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
|
@@ -1,50 +1,58 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInComponent } from './log-in.component';
|
||||
import { authReducer } from '../../core/auth/auth.reducer';
|
||||
import { EPersonMock } from '../testing/eperson-mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../testing/auth-service-stub';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { authMethodsMock, AuthServiceStub } from '../testing/auth-service-stub';
|
||||
import { createTestComponent } from '../testing/utils';
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { appReducers } from '../../app.reducer';
|
||||
import { NativeWindowService } from '../../core/services/window.service';
|
||||
import { NativeWindowMockFactory } from '../mocks/mock-native-window-ref';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../testing/router-stub';
|
||||
import { ActivatedRouteStub } from '../testing/active-router-stub';
|
||||
|
||||
describe('LogInComponent', () => {
|
||||
|
||||
let component: LogInComponent;
|
||||
let fixture: ComponentFixture<LogInComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
const initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: authMethodsMock
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
StoreModule.forRoot(appReducers),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInComponent
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: AuthService, useClass: AuthServiceStub}
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
provideMockStore({ initialState }),
|
||||
LogInComponent
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
@@ -54,75 +62,58 @@ describe('LogInComponent', () => {
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInComponent);
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `<ds-log-in [isStandalonePage]="isStandalonePage"> </ds-log-in>`;
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
|
||||
// verify the fixture is stable (no pending tasks)
|
||||
fixture.whenStable().then(() => {
|
||||
page.addPageElements();
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
}));
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create a FormGroup comprised of FormControls', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.form instanceof FormGroup).toBe(true);
|
||||
it('should create LogInComponent', inject([LogInComponent], (app: LogInComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should authenticate', () => {
|
||||
fixture.detectChanges();
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogInComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// set FormControl values
|
||||
component.form.controls.email.setValue('user');
|
||||
component.form.controls.password.setValue('password');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
// submit form
|
||||
component.submit();
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
component = null;
|
||||
});
|
||||
|
||||
// verify Store.dispatch() is invoked
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||
it('should render a log-in container component for each auth method available', () => {
|
||||
const loginContainers = fixture.debugElement.queryAll(By.css('ds-log-in-container'));
|
||||
expect(loginContainers.length).toBe(2);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
isStandalonePage = true;
|
||||
|
||||
constructor(private component: LogInComponent, private fixture: ComponentFixture<LogInComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
public addPageElements() {
|
||||
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
||||
this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
|
||||
|
||||
const passwordInputSelector = 'input[formcontrolname=\'password\']';
|
||||
this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,13 @@
|
||||
import { filter, map, takeWhile } from 'rxjs/operators';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AuthenticateAction,
|
||||
ResetAuthenticationMessagesAction
|
||||
} from '../../core/auth/auth.actions';
|
||||
import { filter, takeWhile, } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import {
|
||||
getAuthenticationError,
|
||||
getAuthenticationInfo,
|
||||
isAuthenticated,
|
||||
isAuthenticationLoading,
|
||||
} from '../../core/auth/selectors';
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||
import { CoreState } from '../../core/core.reducers';
|
||||
|
||||
import { isNotEmpty } from '../empty.util';
|
||||
import { fadeOut } from '../animations/fade';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
@@ -29,34 +16,21 @@ import { Router } from '@angular/router';
|
||||
@Component({
|
||||
selector: 'ds-log-in',
|
||||
templateUrl: './log-in.component.html',
|
||||
styleUrls: ['./log-in.component.scss'],
|
||||
animations: [fadeOut]
|
||||
styleUrls: ['./log-in.component.scss']
|
||||
})
|
||||
export class LogInComponent implements OnDestroy, OnInit {
|
||||
export class LogInComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The error if authentication fails.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public error: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication error.
|
||||
* A boolean representing if LogInComponent is in a standalone page
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasError = false;
|
||||
@Input() isStandalonePage: boolean;
|
||||
|
||||
/**
|
||||
* The authentication info message.
|
||||
* @type {Observable<string>}
|
||||
* The list of authentication methods available
|
||||
* @type {AuthMethod[]}
|
||||
*/
|
||||
public message: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication message.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasMessage = false;
|
||||
public authMethods: Observable<AuthMethod[]>;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
@@ -70,69 +44,28 @@ export class LogInComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The authentication form.
|
||||
* @type {FormGroup}
|
||||
*/
|
||||
public form: FormGroup;
|
||||
|
||||
/**
|
||||
* Component state.
|
||||
* @type {boolean}
|
||||
*/
|
||||
private alive = true;
|
||||
|
||||
@Input() isStandalonePage: boolean;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthService} authService
|
||||
* @param {FormBuilder} formBuilder
|
||||
* @param {Router} router
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
constructor(private store: Store<CoreState>,
|
||||
private authService: AuthService,) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||
* @method ngOnInit
|
||||
*/
|
||||
public ngOnInit() {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
ngOnInit(): void {
|
||||
|
||||
// set formGroup
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
|
||||
// set error
|
||||
this.error = this.store.pipe(select(
|
||||
getAuthenticationError),
|
||||
map((error) => {
|
||||
this.hasError = (isNotEmpty(error));
|
||||
return error;
|
||||
})
|
||||
);
|
||||
|
||||
// set error
|
||||
this.message = this.store.pipe(
|
||||
select(getAuthenticationInfo),
|
||||
map((message) => {
|
||||
this.hasMessage = (isNotEmpty(message));
|
||||
return message;
|
||||
})
|
||||
this.authMethods = this.store.pipe(
|
||||
select(getAuthenticationMethods),
|
||||
);
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// subscribe to success
|
||||
this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
@@ -142,55 +75,11 @@ export class LogInComponent implements OnDestroy, OnInit {
|
||||
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
|
||||
* @method ngOnDestroy
|
||||
*/
|
||||
public ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
this.alive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error or message.
|
||||
*/
|
||||
public resetErrorOrMessage() {
|
||||
if (this.hasError || this.hasMessage) {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
this.hasError = false;
|
||||
this.hasMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To the registration page.
|
||||
* @method register
|
||||
*/
|
||||
public register() {
|
||||
// TODO enable after registration process is done
|
||||
// this.router.navigate(['/register']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the authentication form.
|
||||
* @method submit
|
||||
*/
|
||||
public submit() {
|
||||
this.resetErrorOrMessage();
|
||||
// get email and password values
|
||||
const email: string = this.form.get('email').value;
|
||||
const password: string = this.form.get('password').value;
|
||||
|
||||
// trim values
|
||||
email.trim();
|
||||
password.trim();
|
||||
|
||||
// dispatch AuthenticationAction
|
||||
this.store.dispatch(new AuthenticateAction(email, password));
|
||||
|
||||
// clear form
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
16
src/app/shared/log-in/methods/log-in.methods-decorator.ts
Normal file
16
src/app/shared/log-in/methods/log-in.methods-decorator.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
|
||||
|
||||
const authMethodsMap = new Map();
|
||||
|
||||
export function renderAuthMethodFor(authMethodType: AuthMethodType) {
|
||||
return function decorator(objectElement: any) {
|
||||
if (!objectElement) {
|
||||
return;
|
||||
}
|
||||
authMethodsMap.set(authMethodType, objectElement);
|
||||
};
|
||||
}
|
||||
|
||||
export function rendersAuthMethodType(authMethodType: AuthMethodType) {
|
||||
return authMethodsMap.get(authMethodType);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<form (ngSubmit)="submit()"
|
||||
[formGroup]="form" novalidate>
|
||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||
<input id="inputEmail"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
class="form-control form-control-lg position-relative"
|
||||
formControlName="email"
|
||||
placeholder="{{'login.form.email' | translate}}"
|
||||
required
|
||||
type="email">
|
||||
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
|
||||
<input id="inputPassword"
|
||||
autocomplete="off"
|
||||
class="form-control form-control-lg position-relative mb-3"
|
||||
placeholder="{{'login.form.password' | translate}}"
|
||||
formControlName="password"
|
||||
required
|
||||
type="password">
|
||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
|
||||
@fadeOut>{{ (error | async) | translate }}</div>
|
||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
|
||||
@fadeOut>{{ (message | async) | translate }}</div>
|
||||
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit"
|
||||
[disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
|
||||
</form>
|
@@ -0,0 +1,13 @@
|
||||
.form-login .form-control:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
.form-login input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-login input[type="password"] {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
@@ -0,0 +1,131 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInPasswordComponent } from './log-in-password.component';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson-mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service-stub';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
|
||||
describe('LogInPasswordComponent', () => {
|
||||
|
||||
let component: LogInPasswordComponent;
|
||||
let fixture: ComponentFixture<LogInPasswordComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInPasswordComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) }
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInPasswordComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
|
||||
// verify the fixture is stable (no pending tasks)
|
||||
fixture.whenStable().then(() => {
|
||||
page.addPageElements();
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
it('should create a FormGroup comprised of FormControls', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.form instanceof FormGroup).toBe(true);
|
||||
});
|
||||
|
||||
it('should authenticate', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// set FormControl values
|
||||
component.form.controls.email.setValue('user');
|
||||
component.form.controls.password.setValue('password');
|
||||
|
||||
// submit form
|
||||
component.submit();
|
||||
|
||||
// verify Store.dispatch() is invoked
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInPasswordComponent, private fixture: ComponentFixture<LogInPasswordComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
public addPageElements() {
|
||||
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
||||
this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
|
||||
|
||||
const passwordInputSelector = 'input[formcontrolname=\'password\']';
|
||||
this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
|
||||
}
|
||||
}
|
@@ -0,0 +1,144 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../../../core/auth/auth.actions';
|
||||
|
||||
import { getAuthenticationError, getAuthenticationInfo, } from '../../../../core/auth/selectors';
|
||||
import { CoreState } from '../../../../core/core.reducers';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { fadeOut } from '../../../animations/fade';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
* @class LogInPasswordComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-log-in-password',
|
||||
templateUrl: './log-in-password.component.html',
|
||||
styleUrls: ['./log-in-password.component.scss'],
|
||||
animations: [fadeOut]
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Password)
|
||||
export class LogInPasswordComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* The error if authentication fails.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public error: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication error.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasError = false;
|
||||
|
||||
/**
|
||||
* The authentication info message.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public message: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication message.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasMessage = false;
|
||||
|
||||
/**
|
||||
* The authentication form.
|
||||
* @type {FormGroup}
|
||||
*/
|
||||
public form: FormGroup;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {FormBuilder} formBuilder
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||
* @method ngOnInit
|
||||
*/
|
||||
public ngOnInit() {
|
||||
|
||||
// set formGroup
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
|
||||
// set error
|
||||
this.error = this.store.pipe(select(
|
||||
getAuthenticationError),
|
||||
map((error) => {
|
||||
this.hasError = (isNotEmpty(error));
|
||||
return error;
|
||||
})
|
||||
);
|
||||
|
||||
// set error
|
||||
this.message = this.store.pipe(
|
||||
select(getAuthenticationInfo),
|
||||
map((message) => {
|
||||
this.hasMessage = (isNotEmpty(message));
|
||||
return message;
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error or message.
|
||||
*/
|
||||
public resetErrorOrMessage() {
|
||||
if (this.hasError || this.hasMessage) {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
this.hasError = false;
|
||||
this.hasMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the authentication form.
|
||||
* @method submit
|
||||
*/
|
||||
public submit() {
|
||||
this.resetErrorOrMessage();
|
||||
// get email and password values
|
||||
const email: string = this.form.get('email').value;
|
||||
const password: string = this.form.get('password').value;
|
||||
|
||||
// trim values
|
||||
email.trim();
|
||||
password.trim();
|
||||
|
||||
// dispatch AuthenticationAction
|
||||
this.store.dispatch(new AuthenticateAction(email, password));
|
||||
|
||||
// clear form
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToShibboleth()" role="button">
|
||||
{{"login.form.shibboleth" | translate}}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,139 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson-mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service-stub';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInShibbolethComponent } from './log-in-shibboleth.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router-stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router-stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
|
||||
describe('LogInShibbolethComponent', () => {
|
||||
|
||||
let component: LogInShibbolethComponent;
|
||||
let fixture: ComponentFixture<LogInShibbolethComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
const shibbolethBaseUrl = 'dspace-rest.test/shibboleth?redirectUrl=';
|
||||
const location = shibbolethBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(authReducer),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInShibbolethComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Shibboleth, location) },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInShibbolethComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
}));
|
||||
|
||||
it('should set the properly a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(shibbolethBaseUrl + currentUrl)
|
||||
|
||||
});
|
||||
|
||||
it('should not set a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/home';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(shibbolethBaseUrl + currentUrl)
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInShibbolethComponent, private fixture: ComponentFixture<LogInShibbolethComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import { Component, Inject, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
import { CoreState } from '../../../../core/core.reducers';
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isNotNull } from '../../../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
templateUrl: './log-in-shibboleth.component.html',
|
||||
styleUrls: ['./log-in-shibboleth.component.scss'],
|
||||
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
export class LogInShibbolethComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The shibboleth authentication location url.
|
||||
* @type {string}
|
||||
*/
|
||||
public location: string;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {RouteService} route
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private route: RouteService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set location
|
||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||
|
||||
}
|
||||
|
||||
redirectToShibboleth() {
|
||||
let newLocationUrl = this.location;
|
||||
const currentUrl = this._window.nativeWindow.location.href;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
const match = myRegexp.exec(this.location);
|
||||
const redirectUrl = (match && match[1]) ? match[1] : null;
|
||||
|
||||
// Check whether the current page is different from the redirect url received from rest
|
||||
if (isNotNull(redirectUrl) && redirectUrl !== currentUrl) {
|
||||
// change the redirect url with the current page url
|
||||
const newRedirectUrl = `?redirectUrl=${currentUrl}`;
|
||||
newLocationUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||
}
|
||||
|
||||
// redirect to shibboleth authentication url
|
||||
this._window.nativeWindow.location.href = newLocationUrl;
|
||||
}
|
||||
|
||||
}
|
@@ -1 +1 @@
|
||||
@import '../log-in/log-in.component.scss';
|
||||
@import '../log-in/methods/password/log-in-password.component';
|
||||
|
21
src/app/shared/mocks/mock-native-window-ref.ts
Normal file
21
src/app/shared/mocks/mock-native-window-ref.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const MockWindow = {
|
||||
location: {
|
||||
_href: '',
|
||||
set href(url: string) {
|
||||
this._href = url;
|
||||
},
|
||||
get href() {
|
||||
return this._href;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class NativeWindowRefMock {
|
||||
get nativeWindow(): any {
|
||||
return MockWindow;
|
||||
}
|
||||
}
|
||||
|
||||
export function NativeWindowMockFactory() {
|
||||
return new NativeWindowRefMock();
|
||||
}
|
@@ -42,13 +42,15 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
|
||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||
import { VarDirective } from './utils/var.directive';
|
||||
import { LogInComponent } from './log-in/log-in.component';
|
||||
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
||||
import { LogOutComponent } from './log-out/log-out.component';
|
||||
import { FormComponent } from './form/form.component';
|
||||
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
|
||||
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||
import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||
import {
|
||||
DsDynamicFormControlContainerComponent,
|
||||
dsDynamicFormControlMapFn
|
||||
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
||||
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
@@ -178,7 +180,12 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { ItemVersionsComponent } from './item/item-versions/item-versions.component';
|
||||
import { SortablejsModule } from 'ngx-sortablejs';
|
||||
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
|
||||
import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component';
|
||||
import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component';
|
||||
import { LogInComponent } from './log-in/log-in.component';
|
||||
import { MissingTranslationHelper } from './translate/missing-translation.helper';
|
||||
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
|
||||
import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component';
|
||||
import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive';
|
||||
import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form';
|
||||
@@ -351,8 +358,12 @@ const COMPONENTS = [
|
||||
ExternalSourceEntryImportModalComponent,
|
||||
ImportableListItemControlComponent,
|
||||
ExistingMetadataListElementComponent,
|
||||
LogInShibbolethComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInContainerComponent,
|
||||
ItemVersionsComponent,
|
||||
PublicationSearchResultListElementComponent,
|
||||
ItemVersionsNoticeComponent,
|
||||
ResourcePoliciesComponent,
|
||||
ResourcePolicyFormComponent,
|
||||
ResourcePolicyCreateComponent,
|
||||
@@ -421,7 +432,10 @@ const ENTRY_COMPONENTS = [
|
||||
DsDynamicLookupRelationSelectionTabComponent,
|
||||
DsDynamicLookupRelationExternalSourceTabComponent,
|
||||
ExternalSourceEntryImportModalComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInShibbolethComponent,
|
||||
ItemVersionsComponent,
|
||||
ItemVersionsNoticeComponent,
|
||||
ResourcePolicyCreateComponent,
|
||||
ResourcePolicyEditComponent
|
||||
];
|
||||
|
@@ -21,7 +21,7 @@ export class AuthRequestServiceStub {
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
} else {
|
||||
} else if (isNotEmpty(options)) {
|
||||
const token = (options.headers as any).lazyUpdate[1].value;
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
@@ -37,6 +37,8 @@ export class AuthRequestServiceStub {
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
return observableOf(authStatusStub);
|
||||
}
|
||||
@@ -48,7 +50,7 @@ export class AuthRequestServiceStub {
|
||||
authStatusStub.authenticated = false;
|
||||
break;
|
||||
case 'status':
|
||||
const token = (options.headers as any).lazyUpdate[1].value;
|
||||
const token = ((options.headers as any).lazyUpdate[1]) ? (options.headers as any).lazyUpdate[1].value : null;
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
authStatusStub.token = this.mockTokenInfo;
|
||||
|
@@ -4,6 +4,12 @@ import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { EPersonMock } from './eperson-mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from './utils';
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
|
||||
export const authMethodsMock = [
|
||||
new AuthMethod('password'),
|
||||
new AuthMethod('shibboleth', 'dspace.test/shibboleth')
|
||||
];
|
||||
|
||||
export class AuthServiceStub {
|
||||
|
||||
@@ -106,4 +112,12 @@ export class AuthServiceStub {
|
||||
isAuthenticated() {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
checkAuthenticationCookie() {
|
||||
return;
|
||||
}
|
||||
|
||||
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
|
||||
return observableOf(authMethodsMock);
|
||||
}
|
||||
}
|
||||
|
10
src/config/auth-config.interfaces.ts
Normal file
10
src/config/auth-config.interfaces.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Config } from './config.interface';
|
||||
|
||||
export interface AuthTarget {
|
||||
host: string;
|
||||
page: string;
|
||||
}
|
||||
|
||||
export interface AuthConfig extends Config {
|
||||
target: AuthTarget;
|
||||
}
|
@@ -10,12 +10,14 @@ import { BrowseByConfig } from './browse-by-config.interface';
|
||||
import { ItemPageConfig } from './item-page-config.interface';
|
||||
import { CollectionPageConfig } from './collection-page-config.interface';
|
||||
import { Theme } from './theme.inferface';
|
||||
import {AuthConfig} from './auth-config.interfaces';
|
||||
|
||||
export interface GlobalConfig extends Config {
|
||||
ui: ServerConfig;
|
||||
rest: ServerConfig;
|
||||
production: boolean;
|
||||
cache: CacheConfig;
|
||||
auth: AuthConfig;
|
||||
form: FormConfig;
|
||||
notifications: INotificationBoardOptions;
|
||||
submission: SubmissionConfig;
|
||||
|
Reference in New Issue
Block a user