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

Add auth module (keycloak integration)

parent 3bc1986b
......@@ -7,14 +7,12 @@ import { StoreModule } from '@ngrx/store';
import { StoreRouterConnectingModule, RouterState } from '@ngrx/router-store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { EffectsModule } from '@ngrx/effects';
import { KeycloakAngularModule } from 'keycloak-angular';
import { initializeKeycloakAnis } from './app.keycloak';
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';
......@@ -26,9 +24,8 @@ import { environment } from '../environments/environment';
BrowserAnimationsModule,
HttpClientModule,
CoreModule,
KeycloakAngularModule,
AuthModule,
StaticModule,
LoginModule,
MetamodelModule,
StoreModule.forRoot(reducers, {
metaReducers,
......@@ -48,7 +45,6 @@ import { environment } from '../environments/environment';
!environment.production ? StoreDevtoolsModule.instrument() : [],
EffectsModule.forRoot([])
],
providers: [ initializeKeycloakAnis ],
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 } from 'keycloak-angular';
import { environment } from '../environments/environment';
import { KeycloakService, KeycloakEventType } from 'keycloak-angular';
import { Store } from '@ngrx/store';
function initializeKeycloak(keycloak: KeycloakService) {
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 => console.log(event))
from(keycloak.keycloakEvents$).subscribe(event => {
if (event.type === KeycloakEventType.OnAuthSuccess) {
store.dispatch(new keycloakActions.AuthSuccessAction());
}
})
return keycloak.init({
config: {
......@@ -28,5 +36,5 @@ export const initializeKeycloakAnis = {
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [ KeycloakService ],
deps: [ KeycloakService, Store ],
};
\ No newline at end of file
import { Action } from '@ngrx/store';
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 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: Keycloak.KeycloakProfile) { }
}
export type Actions
= LoginAction
| LogoutAction
| AuthSuccessAction
| LoadUserProfileSuccessAction;
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';
@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))
)
)
);
}
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
);
......@@ -45,7 +45,7 @@
<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">Authenticated</span>
<span class="dropdown-item font-italic">{{ userProfile.email }}</span>
</li>
<li class="divider dropdown-divider"></li>
<li role="menuitem">
......@@ -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">Authenticated</span>
<span class="dropdown-item font-italic">{{ userProfile.email }}</span>
</li>
<li *ngIf="isAuthenticated" class="divider dropdown-divider"></li>
<li role="menuitem">
......
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { LoginToken } from '../../login/store/model';
import { Instance } from '../../metamodel/model';
import { environment } from '../../../environments/environment'
......@@ -12,6 +11,7 @@ import { environment } from '../../../environments/environment'
})
export class NavComponent {
@Input() isAuthenticated: boolean;
@Input() userProfile: Keycloak.KeycloakProfile;
@Input() instance: Instance;
@Output() login: EventEmitter<any> = new EventEmitter();
@Output() logout: EventEmitter<any> = new EventEmitter();
......
<header>
<app-nav
[isAuthenticated]="isAuthenticated | async"
[userProfile]="userProfile | async"
[instance]="instance | async"
(login)="login()"
(logout)="logout()">
......
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable, from } from 'rxjs';
import { KeycloakService } from 'keycloak-angular';
import { Observable } from 'rxjs';
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 { Instance } from '../../metamodel/model';
import { VERSIONS } from '../../../settings/settings';
interface StoreState {
auth: fromAuth.State;
metamodel: fromMetamodel.State;
}
......@@ -24,10 +27,12 @@ export class AppComponent implements OnInit {
public anisClientVersion: string = VERSIONS.anisClient;
public year = (new Date()).getFullYear();
public isAuthenticated: Observable<boolean>;
public userProfile: Observable<Keycloak.KeycloakProfile>
public instance: Observable<Instance>;
constructor(private store: Store<StoreState>, private keycloak: KeycloakService) {
this.isAuthenticated = from(this.keycloak.isLoggedIn());
constructor(private store: Store<StoreState>) {
this.isAuthenticated = store.select(authSelector.isAuthenticated);
this.userProfile = store.select(authSelector.getUserProfile);
this.instance = store.select(metamodelSelector.getInstance);
}
......@@ -36,10 +41,10 @@ export class AppComponent implements OnInit {
}
login(): void {
this.keycloak.login();
this.store.dispatch(new authActions.LoginAction());
}
logout(): void {
this.keycloak.logout();
this.store.dispatch(new authActions.LogoutAction());
}
}
<h1 class="mb-3">ANIS</h1>
<p dir="auto">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras a laoreet risus, vel placerat sem. In vestibulum odio
ut enim venenatis commodo.
</p>
<ul dir="auto">
<li>Explore datasets on ANIS (no login needed)</li>
<li>More information about ANIS</li>
</ul>
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountBenefitsComponent } from './account-benefits.component';
describe('[Login] Component: AccountBenefitsComponent', () => {
let component: AccountBenefitsComponent;
let fixture: ComponentFixture<AccountBenefitsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AccountBenefitsComponent]
});
fixture = TestBed.createComponent(AccountBenefitsComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
});
import { Component } from '@angular/core';
@Component({
selector: 'app-account-benefits',
templateUrl: 'account-benefits.component.html'
})
export class AccountBenefitsComponent { }
<form name="form" (ngSubmit)="f.form.valid && emitSubmit(f.form.value)" #f="ngForm" novalidate>
<div class="form-group">
<label for="email">Email address</label>
<input type="email" id="email" class="form-control" name="email" [(ngModel)]="email" placeholder="Enter email"
required autofocus>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">
Reset password
</button>
</div>
</form>
\ No newline at end of file
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormForgotPasswordComponent } from './form-forgot-password.component';
import { FormsModule } from '@angular/forms';
describe('[Login] Component: FormForgotPasswordComponent', () => {
let component: FormForgotPasswordComponent;
let fixture: ComponentFixture<FormForgotPasswordComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FormForgotPasswordComponent],
imports: [FormsModule]
});
fixture = TestBed.createComponent(FormForgotPasswordComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('raises the submit form event when clicked', () => {
const email = 'test@email.com';
component.submitted.subscribe((event: string) => expect(event).toEqual(email));
component.emitSubmit(email);
});
});
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-form-forgot-password',
templateUrl: 'form-forgot-password.component.html'
})
export class FormForgotPasswordComponent {
email = '';
@Output() submitted: EventEmitter<string> = new EventEmitter();
emitSubmit(email: string) {
this.submitted.emit(email);
}
}
<form name="form" (ngSubmit)="f.form.valid && emitSubmit(f.form.value)" #f="ngForm" novalidate>
<div class="form-group">
<label for="email">Email address</label>
<input type="email" id="email" class="form-control" name="email" [(ngModel)]="model.email"
placeholder="Enter email" required autofocus>
<small class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<label for="password">Password</label>
<div class="input-group mb-3">
<input [type]="showPassword ? 'text' : 'password'" id="password" class="form-control"
placeholder="Password" name="password" [(ngModel)]="model.password" aria-label="Password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon2"
(click)="showPassword = !showPassword">
<div *ngIf="!showPassword"><span class="fas fa-fw fa-eye-slash"></span></div>
<div *ngIf="showPassword"><span class="fas fa-fw fa-eye"></span></div>
</button>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">Sign In</button>
</div>
</form>
\ No newline at end of file
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { FormLoginComponent } from './form-login.component';
import { Login } from '../store/model';
describe('[Login] Component: FormLoginComponent', () => {
let component: FormLoginComponent;
let fixture: ComponentFixture<FormLoginComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FormLoginComponent],
imports: [FormsModule]
});
fixture = TestBed.createComponent(FormLoginComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('raises the submit form event when clicked', () => {
const login: Login = { email: 'test@email.com', password: 'password'};
component.submitted.subscribe((event: Login) => expect(event).toEqual(login));
component.emitSubmit(login);
});
});
import { Component, Output, EventEmitter } from '@angular/core';
import { Login } from '../store/model';
@Component({
selector: 'app-form-login',
templateUrl: 'form-login.component.html'
})
export class FormLoginComponent {
model: Login = new Login();
showPassword = false;
@Output() submitted: EventEmitter<Login> = new EventEmitter();
emitSubmit(login: Login) {
this.submitted.emit(login);
}
}
Supports Markdown
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