Commit 621c2ceb authored by Tifenn Guillas's avatar Tifenn Guillas

Merge branch 'develop' into 148-add-comments

parents c89f7a64 64ff8597
3.4.0
\ No newline at end of file
3.5.0
\ No newline at end of file
FROM node:13-slim
FROM node:14-slim
ENV DEBIAN_FRONTEND=noninteractive
......
......@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
......
......@@ -11,52 +11,54 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.1",
"@angular/common": "~9.1.1",
"@angular/compiler": "~9.1.1",
"@angular/core": "~9.1.1",
"@angular/forms": "~9.1.1",
"@angular/platform-browser": "~9.1.1",
"@angular/platform-browser-dynamic": "~9.1.1",
"@angular/router": "~9.1.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@ng-select/ng-select": "^4.0.0",
"@ngrx/effects": "^9.1.0",
"@ngrx/entity": "^9.1.0",
"@ngrx/router-store": "^9.1.0",
"@ngrx/store": "^9.1.0",
"@ngrx/store-devtools": "^9.1.0",
"bootstrap": "^4.4.1",
"@angular/animations": "~10.2.2",
"@angular/common": "~10.2.2",
"@angular/compiler": "~10.2.2",
"@angular/core": "~10.2.2",
"@angular/forms": "~10.2.2",
"@angular/platform-browser": "~10.2.2",
"@angular/platform-browser-dynamic": "~10.2.2",
"@angular/router": "~10.2.2",
"@fortawesome/fontawesome-free": "^5.15.1",
"@ng-select/ng-select": "^5.0.8",
"@ngrx/effects": "^10.0.1",
"@ngrx/entity": "^10.0.1",
"@ngrx/router-store": "^10.0.1",
"@ngrx/store": "^10.0.1",
"@ngrx/store-devtools": "^10.0.1",
"bootstrap": "^4.5.3",
"d3": "^5.15.1",
"ng-inline-svg": "^10.1.0",
"ngx-bootstrap": "^5.6.1",
"keycloak-angular": "^8.0.1",
"keycloak-js": "^11.0.3",
"ng-inline-svg": "^11.0.1",
"ngx-bootstrap": "^6.1.0",
"ngx-json-viewer": "^2.4.0",
"ngx-toastr": "^12.0.1",
"rxjs": "~6.5.4",
"tslib": "^1.9.0",
"ngx-toastr": "^13.1.0",
"rxjs": "~6.6.3",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.1",
"@angular/language-service": "~9.1.1",
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.2",
"@angular/language-service": "~10.2.2",
"@types/d3": "^5.7.2",
"@types/jasmine": "~3.3.8",
"@types/jasmine": "~3.6.1",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^5.0.0",
"codelyzer": "^6.0.1",
"intl": "^1.2.5",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.7.5"
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~9.0.0",
"tslint": "~6.1.0",
"typescript": "~4.0.5"
}
}
......@@ -11,8 +11,8 @@ import { EffectsModule } from '@ngrx/effects';
import { CustomRouterStateSerializer } from './shared/utils';
import { reducers, metaReducers } from './app.reducer';
import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';
import { StaticModule } from './static/static.module';
import { LoginModule } from './login/login.module';
import { MetamodelModule } from './metamodel/metamodel.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './core/containers/app.component';
......@@ -24,8 +24,8 @@ import { environment } from '../environments/environment';
BrowserAnimationsModule,
HttpClientModule,
CoreModule,
AuthModule,
StaticModule,
LoginModule,
MetamodelModule,
StoreModule.forRoot(reducers, {
metaReducers,
......@@ -45,6 +45,6 @@ import { environment } from '../environments/environment';
!environment.production ? StoreDevtoolsModule.instrument() : [],
EffectsModule.forRoot([])
],
bootstrap: [AppComponent]
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { KeycloakAngularModule } from 'keycloak-angular';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { initializeKeycloakAnis } from './init.keycloak';
import { reducer } from './store/auth.reducer';
import { AuthEffects } from './store/auth.effects';
@NgModule({
imports: [
KeycloakAngularModule,
StoreModule.forFeature('auth', reducer),
EffectsModule.forFeature([ AuthEffects ])
],
providers: [
initializeKeycloakAnis
]
})
export class AuthModule { }
import { APP_INITIALIZER } from '@angular/core';
import { from } from 'rxjs';
import { KeycloakService, KeycloakEventType } from 'keycloak-angular';
import { Store } from '@ngrx/store';
import * as keycloakActions from './store/auth.action';
import * as fromKeycloak from './store/auth.reducer';
import { environment } from '../../environments/environment';
function initializeKeycloak(keycloak: KeycloakService, store: Store<{ keycloak: fromKeycloak.State }>) {
return async () => {
from(keycloak.keycloakEvents$).subscribe(event => {
if (event.type === KeycloakEventType.OnAuthSuccess) {
store.dispatch(new keycloakActions.AuthSuccessAction());
}
})
return keycloak.init({
config: {
url: environment.ssoAuthUrl,
realm: environment.ssoRealm,
clientId: environment.ssoClientId,
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html'
},
loadUserProfileAtStartUp: true
});
}
}
export const initializeKeycloakAnis = {
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [ KeycloakService, Store ],
};
\ No newline at end of file
import { Action } from '@ngrx/store';
import { UserProfile } from './user-profile.model';
export const LOGIN = '[Auth] Login';
export const LOGOUT = '[Auth] Logout';
export const AUTH_SUCCESS = '[Auth] Auth Success';
export const LOAD_USER_PROFILE_SUCCESS = '[Auth] Load User Profile Success';
export const OPEN_EDIT_PROFILE = '[Auth] Edit Profile';
export class LoginAction implements Action {
readonly type = LOGIN;
constructor(public payload: {} = null) { }
}
export class LogoutAction implements Action {
readonly type = LOGOUT;
constructor(public payload: {} = null) { }
}
export class AuthSuccessAction implements Action {
readonly type = AUTH_SUCCESS;
constructor(public payload: {} = null) { }
}
export class LoadUserProfileSuccessAction implements Action {
readonly type = LOAD_USER_PROFILE_SUCCESS;
constructor(public payload: UserProfile) { }
}
export class OpenEditProfileAction implements Action {
readonly type = OPEN_EDIT_PROFILE;
constructor(public payload: {} = null) { }
}
export type Actions
= LoginAction
| LogoutAction
| AuthSuccessAction
| LoadUserProfileSuccessAction
| OpenEditProfileAction;
import { Injectable } from '@angular/core';
import { from } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { KeycloakService } from 'keycloak-angular';
import * as authActions from './auth.action';
import { environment } from '../../../environments/environment';
@Injectable()
export class AuthEffects {
constructor(
private actions$: Actions,
private keycloak: KeycloakService
) { }
@Effect({ dispatch: false })
loginAction$ = this.actions$.pipe(
ofType(authActions.LOGIN),
tap(_ => this.keycloak.login())
);
@Effect({ dispatch: false })
logoutAction$ = this.actions$.pipe(
ofType(authActions.LOGOUT),
tap(_ => this.keycloak.logout())
);
@Effect()
authSuccessAction$ = this.actions$.pipe(
ofType(authActions.AUTH_SUCCESS),
switchMap(_ =>
from(this.keycloak.loadUserProfile()).pipe(
map(userProfile => new authActions.LoadUserProfileSuccessAction(userProfile))
)
)
);
@Effect({ dispatch: false })
OpenEditProfileAction$ = this.actions$.pipe(
ofType(authActions.OPEN_EDIT_PROFILE),
tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank'))
);
}
import * as actions from './login.action';
import { LoginToken } from './model';
import * as actions from './auth.action';
export interface State {
isAuthenticated: boolean;
loginToken: LoginToken;
userProfile: any;
}
export const initialState: State = {
isAuthenticated: false,
loginToken: null
userProfile: null
};
export function reducer(state: State = initialState, action: actions.Actions): State {
switch (action.type) {
case actions.LOGIN_LOCAL_STORAGE_SUCCESS:
case actions.LOGIN_SUCCESS:
const loginToken: LoginToken = action.payload;
case actions.AUTH_SUCCESS:
return {
...state,
isAuthenticated: true,
loginToken
isAuthenticated: true
};
case actions.LOGOUT:
case actions.LOAD_USER_PROFILE_SUCCESS:
const userProfile = action.payload;
return {
isAuthenticated: false,
loginToken: null
};
...state,
userProfile
}
default:
return state;
......@@ -36,4 +32,4 @@ export function reducer(state: State = initialState, action: actions.Actions): S
}
export const isAuthenticated = (state: State) => state.isAuthenticated;
export const getLoginToken = (state: State) => state.loginToken;
export const getUserProfile = (state: State) => state.userProfile;
import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as login from './login.reducer';
import * as auth from './auth.reducer';
export const getLoginState = createFeatureSelector<login.State>('login');
export const getAuthtate = createFeatureSelector<auth.State>('auth');
export const isAuthenticated = createSelector(
getLoginState,
login.isAuthenticated
getAuthtate,
auth.isAuthenticated
);
export const getLoginToken = createSelector(
getLoginState,
login.getLoginToken
export const getUserProfile = createSelector(
getAuthtate,
auth.getUserProfile
);
export interface UserProfile {
id?: string;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
enabled?: boolean;
emailVerified?: boolean;
totp?: boolean;
createdTimestamp?: number;
}
\ No newline at end of file
......@@ -30,7 +30,7 @@
</ul>
<button *ngIf="!isAuthenticated" class="btn btn-outline-success my-2 my-sm-0"
id="button-sign-in"
routerLink="/login">
(click)="emitLogin()">
Sign In / Register
</button>
<span *ngIf="isAuthenticated" id="dropdown-menu" dropdown>
......@@ -45,12 +45,12 @@
<ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu"
aria-labelledby="basic-link">
<li id="li-email" role="menuitem">
<span class="dropdown-item font-italic">{{ loginToken.email }}</span>
<span class="dropdown-item font-italic">{{ userProfile.email }}</span>
</li>
<li class="divider dropdown-divider"></li>
<li role="menuitem">
<a class="dropdown-item" routerLink="/change-password">
<span class="fas fa-key fa-fw"></span> Change password
<a class="dropdown-item pointer" (click)="emitOpenEditProfile()">
<span class="fas fa-id-card"></span> Edit profile
</a>
</li>
<li class="divider dropdown-divider"></li>
......@@ -71,7 +71,7 @@
<ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu"
aria-labelledby="basic-link">
<li *ngIf="isAuthenticated" role="menuitem">
<span class="dropdown-item font-italic">{{ loginToken.email }}</span>
<span class="dropdown-item font-italic">{{ userProfile.email }}</span>
</li>
<li *ngIf="isAuthenticated" class="divider dropdown-divider"></li>
<li role="menuitem">
......@@ -96,8 +96,8 @@
</li>
<li *ngIf="isAuthenticated" class="divider dropdown-divider"></li>
<li *ngIf="isAuthenticated" role="menuitem">
<a class="dropdown-item" routerLink="/change-password">
<span class="fas fa-key fa-fw"></span> Change password
<a class="dropdown-item pointer" (click)="emitOpenEditProfile()">
<span class="fas fa-id-card"></span> Edit profile
</a>
</li>
<li class="divider dropdown-divider"></li>
......
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { LoginToken } from '../../login/store/model';
import { Instance } from '../../metamodel/model';
import { UserProfile } from '../../auth/store/user-profile.model';
import { environment } from '../../../environments/environment'
@Component({
......@@ -12,9 +12,11 @@ import { environment } from '../../../environments/environment'
})
export class NavComponent {
@Input() isAuthenticated: boolean;
@Input() loginToken: LoginToken;
@Input() userProfile: UserProfile;
@Input() instance: Instance;
@Output() login: EventEmitter<any> = new EventEmitter();
@Output() logout: EventEmitter<any> = new EventEmitter();
@Output() openEditProfile: EventEmitter<any> = new EventEmitter();
baseHref: string = environment.baseHref;
isSearchAllowed(): boolean {
......@@ -38,7 +40,15 @@ export class NavComponent {
return false;
}
emitLogin() {
this.login.emit();
}
emitLogout() {
this.logout.emit();
}
emitOpenEditProfile() {
this.openEditProfile.emit();
}
}
<header>
<app-nav
[isAuthenticated]="isAuthenticated | async"
[loginToken]="loginToken | async"
[userProfile]="userProfile | async"
[instance]="instance | async"
(logout)="logout()">
(login)="login()"
(logout)="logout()"
(openEditProfile)="openEditProfile()">
</app-nav>
</header>
<main role="main" class="container-fluid pb-4">
......
......@@ -3,11 +3,10 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import * as fromAuth from '../../auth/store/auth.reducer';
import * as authActions from '../../auth/store/auth.action';
import { UserProfile } from '../../auth/store/user-profile.model';
import { AppComponent } from './app.component';
import * as fromLogin from '../../login/store/login.reducer';
import * as loginActions from '../../login/store/login.action';
import { LoginToken } from '../../login/store/model';
import * as fromMetamodel from '../../metamodel/reducers';
import * as instanceActions from '../../metamodel/action/instance.action';
......@@ -15,14 +14,14 @@ describe('[Core] Container: AppComponent', () => {
@Component({ selector: 'app-nav', template: '' })
class NavStubComponent {
@Input() isAuthenticated: boolean;
@Input() loginToken: LoginToken;
@Input() userProfile: UserProfile;
}
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let store: MockStore;
const initialState = {
login: { ...fromLogin.initialState },
auth: { ...fromAuth.initialState },
metamodel: { ...fromMetamodel }
};
......@@ -47,17 +46,15 @@ describe('[Core] Container: AppComponent', () => {
});
it('should execute ngOnInit lifecycle', () => {
const loginLocalStorageAction = new loginActions.LoginLocalStorageAction();
const loadInstanceMetaAction = new instanceActions.LoadInstanceMetaAction();
const spy = spyOn(store, 'dispatch');
component.ngOnInit();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith(loginLocalStorageAction);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(loadInstanceMetaAction);
});
it('#logout() should dispatch LogoutAction', () => {
const logoutAction = new loginActions.LogoutAction();
const logoutAction = new authActions.LogoutAction();
const spy = spyOn(store, 'dispatch');
component.logout();
expect(spy).toHaveBeenCalledTimes(1);
......
......@@ -3,18 +3,18 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as fromLogin from '../../login/store/login.reducer';
import * as loginActions from '../../login/store/login.action';
import * as loginSelector from '../../login/store/login.selector';
import { LoginToken } from '../../login/store/model';
import * as fromMetamodel from '../../metamodel/reducers';
import * as instanceActions from '../../metamodel/action/instance.action';
import * as metamodelSelector from '../../metamodel/selectors';
import * as fromAuth from '../../auth/store/auth.reducer';
import * as authActions from '../../auth/store/auth.action';
import * as authSelector from '../../auth/store/auth.selector';
import { UserProfile } from '../../auth/store/user-profile.model';
import { Instance } from '../../metamodel/model';
import { VERSIONS } from '../../../settings/settings';
interface StoreState {
login: fromLogin.State;
auth: fromAuth.State;
metamodel: fromMetamodel.State;
}
......@@ -28,21 +28,28 @@ export class AppComponent implements OnInit {
public anisClientVersion: string = VERSIONS.anisClient;
public year = (new Date()).getFullYear();
public isAuthenticated: Observable<boolean>;
public loginToken: Observable<LoginToken>;
public userProfile: Observable<UserProfile>
public instance: Observable<Instance>;