Commit a111c7f0 authored by François Agneray's avatar François Agneray
Browse files

Merge branch 'develop' of gitlab.lam.fr:anis/anis-client into develop

parents c39a71a0 632ff67d
Pipeline #3705 passed with stages
in 8 minutes and 46 seconds
......@@ -8,7 +8,7 @@ module.exports = function (config) {
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-spec-reporter'),
// require('karma-spec-reporter'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
......@@ -21,7 +21,11 @@ module.exports = function (config) {
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml', 'spec'],
reporters: [
'progress',
'kjhtml',
// 'spec'
],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
......
import * as authActions from './auth.action';
describe('[Auth] Action', () => {
it('should create CheckAuthAction', () => {
const action = new authActions.CheckAuthAction();
expect(action.type).toEqual(authActions.CHECK_AUTH);
});
it('should create CheckAuthCompleteAction', () => {
const action = new authActions.CheckAuthCompleteAction(true);
expect(action.type).toEqual(authActions.CHECK_AUTH_COMPLETE);
expect(action.payload).toBeTruthy();
});
it('should create LoginAction', () => {
const action = new authActions.LoginAction();
expect(action.type).toEqual(authActions.LOGIN);
});
it('should create LoginCompleteAction', () => {
const action = new authActions.LoginCompleteAction('toto');
expect(action.type).toEqual(authActions.LOGIN_COMPLETE);
expect(action.payload).toEqual('toto');
});
it('should create LogoutAction', () => {
const action = new authActions.LogoutAction();
expect(action.type).toEqual(authActions.LOGOUT);
});
it('should create OpenEditProfileAction', () => {
const action = new authActions.OpenEditProfileAction();
expect(action.type).toEqual(authActions.OPEN_EDIT_PROFILE);
});
it('should create ParseJwtAction', () => {
const action = new authActions.ParseJwtAction('toto');
expect(action.type).toEqual(authActions.PARSE_JWT);
expect(action.payload).toEqual('toto');
});
it('should create LoadUserRolesAction', () => {
const action = new authActions.LoadUserRolesAction(['toto']);
expect(action.type).toEqual(authActions.LOAD_USER_ROLES);
expect(action.payload).toEqual(['toto']);
});
});
......@@ -9,22 +9,31 @@
import { Action } from '@ngrx/store';
export const CHECK_AUTH = '[Auth] check Auth';
export const CHECK_AUTH = '[Auth] Check Auth';
export const CHECK_AUTH_COMPLETE = '[Auth] Check Auth Complete';
export const LOGIN = '[Auth] Login';
export const LOGIN_COMPLETE = '[Auth] Login Complete';
export const LOGOUT = '[Auth] Logout';
export const OPEN_EDIT_PROFILE = '[Auth] Edit Profile';
export const GET_USER_ROLES = '[Auth] Get User Roles';
export const PARSE_JWT = '[Auth] Parse JWT';
export const LOAD_USER_ROLES = '[Auth] Load User Roles';
/**
* @class
* @classdesc CheckAuthAction action.
* @readonly
*/
export class CheckAuthAction implements Action {
readonly type = CHECK_AUTH;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc CheckAuthCompleteAction action.
* @readonly
*/
export class CheckAuthCompleteAction implements Action {
readonly type = CHECK_AUTH_COMPLETE;
......@@ -42,13 +51,17 @@ export class LoginAction implements Action {
constructor(public payload: string = null) { }
}
/**
* @class
* @classdesc LoginCompleteAction action.
* @readonly
*/
export class LoginCompleteAction implements Action {
readonly type = LOGIN_COMPLETE;
constructor(public payload: any) { }
}
/**
* @class
* @classdesc LogoutAction action.
......@@ -60,18 +73,33 @@ export class LogoutAction implements Action {
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc OpenEditProfileAction action.
* @readonly
*/
export class OpenEditProfileAction implements Action {
readonly type = OPEN_EDIT_PROFILE;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc ParseJwtAction action.
* @readonly
*/
export class ParseJwtAction implements Action {
readonly type = PARSE_JWT;
constructor(public payload: string) { }
}
/**
* @class
* @classdesc LoadUserRolesAction action.
* @readonly
*/
export class LoadUserRolesAction implements Action {
readonly type = LOAD_USER_ROLES;
......
......@@ -10,9 +10,9 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { Effect, Actions, ofType } from '@ngrx/effects';
import jwt_decode from 'jwt-decode';
import * as authActions from './auth.action';
......@@ -32,7 +32,7 @@ export class AuthEffects {
) { }
/**
* Execute Login (OIDC Login)
* Executes log in (OIDC Login).
*/
@Effect({ dispatch: false })
loginAction$ = this.actions$.pipe(
......@@ -46,6 +46,9 @@ export class AuthEffects {
})
);
/**
* Checks if user is authenticated.
*/
@Effect()
checkAuthAction$ = this.actions$.pipe(
ofType(authActions.CHECK_AUTH),
......@@ -58,12 +61,15 @@ export class AuthEffects {
)
);
/**
* Completes authentication if not complete.
*/
@Effect()
checkAuthCompleteAction$ = this.actions$.pipe(
ofType(authActions.CHECK_AUTH_COMPLETE),
switchMap((action: authActions.CheckAuthCompleteAction) => {
const isAuthenticated = action.payload;
if (isAuthenticated) {
return this.authService.userData.pipe(
switchMap((profile) => [
......@@ -77,6 +83,9 @@ export class AuthEffects {
})
);
/**
* Redirects user after login completes.
*/
@Effect({ dispatch: false })
loginCompleteAction$ = this.actions$.pipe(
ofType(authActions.LOGIN_COMPLETE),
......@@ -89,11 +98,14 @@ export class AuthEffects {
})
);
/**
* Parses token to get user roles.
*/
@Effect()
parseJWTAction$ = this.actions$.pipe(
ofType(authActions.PARSE_JWT),
switchMap((action: authActions.ParseJwtAction) => {
const jwt = jwt_decode(action.payload) as any;
const jwt: any = jwt_decode(action.payload);
if (environment.ssoName === 'auth0') {
return of({ type: '[No Action] ' + authActions.PARSE_JWT });
} else {
......@@ -102,6 +114,9 @@ export class AuthEffects {
})
);
/**
* Executes log out.
*/
@Effect({ dispatch: false })
logoutAction$ = this.actions$.pipe(
ofType(authActions.LOGOUT),
......@@ -112,7 +127,7 @@ export class AuthEffects {
* Opens edit profile page.
*/
@Effect({ dispatch: false })
OpenEditProfileAction$ = this.actions$.pipe(
openEditProfileAction$ = this.actions$.pipe(
ofType(authActions.OPEN_EDIT_PROFILE),
tap(_ => window.open(environment.ssoAuthUrl + '/account', '_blank'))
);
......
......@@ -10,6 +10,40 @@ describe('[Auth] Reducer', () => {
expect(state).toBe(initialState);
});
it('should set isAuthenticated to true and set userProfile', () => {
const { initialState } = fromAuth;
const action = new authActions.LoginCompleteAction('toto');
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeTruthy();
expect(state.userProfile).toEqual('toto');
expect(state.userRoles.length).toEqual(0);
expect(state).not.toEqual(initialState);
});
it('should set userRoles', () => {
const { initialState } = fromAuth;
const action = new authActions.LoadUserRolesAction(['toto']);
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeFalsy();
expect(state.userProfile).toBeNull();
expect(state.userRoles.length).toEqual(1);
expect(state.userRoles[0]).toEqual('toto');
expect(state).not.toEqual(initialState);
});
it('should set isAuthenticated to false and unset userRoles', () => {
const initialState = { ...fromAuth.initialState, isAuthenticated: true, userProfile: 'toto' };
const action = new authActions.LogoutAction();
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeFalsy();
expect(state.userProfile).toBeNull();
expect(state.userRoles.length).toEqual(0);
expect(state).not.toEqual(initialState);
});
it('should get isAuthenticated', () => {
const action = {} as authActions.Actions;
const state = fromAuth.reducer(undefined, action);
......@@ -23,4 +57,11 @@ describe('[Auth] Reducer', () => {
expect(fromAuth.getUserProfile(state)).toBeNull();
});
it('should get userRoles', () => {
const action = {} as authActions.Actions;
const state = fromAuth.reducer(undefined, action);
expect(fromAuth.getUserRoles(state).length).toEqual(0);
});
});
......@@ -37,20 +37,16 @@ export const initialState: State = {
export function reducer(state: State = initialState, action: actions.Actions): State {
switch (action.type) {
case actions.LOGIN_COMPLETE:
const userProfile = action.payload as any;
return {
...state,
isAuthenticated: true,
userProfile
userProfile: action.payload
};
case actions.LOAD_USER_ROLES:
const userRoles = action.payload;
return {
...state,
userRoles
userRoles: action.payload
};
case actions.LOGOUT:
......
......@@ -7,8 +7,18 @@ describe('[Auth] Selector', () => {
expect(authSelector.isAuthenticated(state)).toBeFalsy();
});
it('should get isAuthenticated', () => {
const state = { auth: { ...fromAuth.initialState }};
expect(authSelector.isAuthenticated(state)).toBeFalsy();
});
it('should get userProfile', () => {
const state = { auth: { ...fromAuth.initialState }};
expect(authSelector.getUserProfile(state)).toBeNull();
});
it('should get userRoles', () => {
const state = { auth: { ...fromAuth.initialState }};
expect(authSelector.getUserRoles(state).length).toEqual(0);
});
});
\ No newline at end of file
/**
* This file is part of Anis Client.
*
* @copyright Laboratoire d'Astrophysique de Marseille / CNRS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { environment } from 'src/environments/environment';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
/**
* @class
* @classdesc Authentication service.
*/
export class AuthService {
constructor(private oidcSecurityService: OidcSecurityService) { }
get isLoggedIn() {
return this.oidcSecurityService.isAuthenticated$;
}
/**
* Gets user data.
*
* @return Observable<any>
*/
get token() {
if (environment.ssoName === 'auth0') {
return this.oidcSecurityService.getIdToken();
......@@ -20,19 +35,35 @@ export class AuthService {
}
}
get userData() {
/**
* Gets user data.
*
* @return Observable<any>
*/
get userData(): Observable<any> {
return this.oidcSecurityService.userData$;
}
checkAuth() {
/**
* Checks authentication.
*
* @return Observable<boolean>
*/
checkAuth(): Observable<boolean> {
return this.oidcSecurityService.checkAuthIncludingServer();
}
/**
* Logs in user.
*/
doLogin() {
return of(this.oidcSecurityService.authorize());
this.oidcSecurityService.authorize();
}
signOut() {
/**
* Logs out user.
*/
signOut(): void {
this.oidcSecurityService.logoffAndRevokeTokens();
}
}
/**
* This file is part of Anis Client.
*
* @copyright Laboratoire d'Astrophysique de Marseille / CNRS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { OidcConfigService, LogLevel } from 'angular-auth-oidc-client';
import { environment } from '../../environments/environment';
export function configureAuth(oidcConfigService: OidcConfigService) {
return () =>
oidcConfigService.withConfig({
stsServer: environment.ssoAuthUrl,
redirectUrl: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: environment.ssoClientId,
scope: 'openid profile email offline_access',
responseType: 'code',
silentRenew: true,
silentRenewUrl: `${window.location.origin}/silent-renew.html`,
renewTimeBeforeTokenExpiresInSeconds: 10,
logLevel: LogLevel.None,
});
/**
* Adds user token to the request.
*
* @param {OidcConfigService} oidcConfigService - The single sign on service.
*
* @return any
*/
export function configureAuth(oidcConfigService: OidcConfigService): any {
return () => oidcConfigService.withConfig({
stsServer: environment.ssoAuthUrl,
redirectUrl: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: environment.ssoClientId,
scope: 'openid profile email offline_access',
responseType: 'code',
silentRenew: true,
silentRenewUrl: `${window.location.origin}/silent-renew.html`,
renewTimeBeforeTokenExpiresInSeconds: 10,
logLevel: LogLevel.None,
});
}
/**
* This file is part of Anis Client.
*
* @copyright Laboratoire d'Astrophysique de Marseille / CNRS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
import { environment } from '../../environments/environment';
@Injectable()
/**
* @class
* @classdesc Token interceptor.
*
* @implements HttpInterceptor
*/
export class TokenInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
/**
* Adds user token to the request.
*
* @param {HttpRequest<any>} request - The outgoing request.
* @param {HttpHandler} next - The next interceptor.
*
* @return Observable<HttpEvent<any>>
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let requestToForward = request;
const token = this.authService.token;
const isApiUrl = request.url.startsWith(environment.apiUrl);
if (token !== '' && isApiUrl) {
......@@ -21,7 +44,6 @@ export class TokenInterceptor implements HttpInterceptor {
}
});
}
return next.handle(requestToForward);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment