Skip to content
Snippets Groups Projects
Commit ca0e34c1 authored by Lukasz Lopatowski's avatar Lukasz Lopatowski
Browse files

Merge branch '265-add-modal-to-linkind-account' into 'develop'

Resolve "Add modal when account is about to be linked"

See merge request !105
parents 98ffc891 c14b4a99
No related branches found
No related tags found
4 merge requests!184Develop,!119Develop,!108Develop,!105Resolve "Add modal when account is about to be linked"
......@@ -6,6 +6,7 @@ import { WelcomeRoutes } from './welcome/welcome.routes';
import {ServiceUnavailableRoutes} from './service-unavailable/service-unavailable.routes';
import {PageNotFoundComponent} from './shared/page-not-found/page-not-found.component';
import {LoginSuccessComponent} from './auth/login-success/login-success.component';
import {LinkAccountComponent} from './welcome/link-account/link-account.component';
const appRoutes: Routes = [
...WelcomeRoutes,
......@@ -13,6 +14,7 @@ const appRoutes: Routes = [
...ServiceUnavailableRoutes,
{ path: 'notfound', component: PageNotFoundComponent },
{ path: 'login-success', component: LoginSuccessComponent },
{ path: 'login-linking', component: LinkAccountComponent},
{ path: '**', redirectTo: '/welcome' },
];
......
......@@ -3,19 +3,18 @@ import {TestBed, waitForAsync} from '@angular/core/testing';
import {AuthService} from './auth.service';
import {AppConfigService, ConfigurationService} from '../service';
import {JwtHelperService} from '@auth0/angular-jwt';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {Role, UserRole} from '../model/userrole';
import {ProfileService} from '../service/profile.service';
import {Observable, of} from 'rxjs';
import {Configuration} from '../model/configuration';
import { HttpHandler } from '@angular/common/http';
describe('Service: Auth', () => {
let authService: AuthService;
let appConfigServiceSpy: jasmine.SpyObj<AppConfigService>;
let jwtHelperServiceSpy: jasmine.SpyObj<JwtHelperService>;
let maintenanceServiceSpy: jasmine.SpyObj<ConfigurationService>;
let httpMock: HttpTestingController;
let store: any = {};
beforeEach(waitForAsync(() => {
......@@ -23,7 +22,8 @@ describe('Service: Auth', () => {
config: {
apiUrl: 'http://api.url',
tokenName: 'token',
}
},
getTestInstanceModalKey: () => 'testModalKey'
};
const jwtSpy = jasmine.createSpyObj('JwtHelperService', ['decodeToken', 'isTokenExpired']);
jwtSpy.decodeToken.and.returnValue({
......@@ -84,6 +84,7 @@ describe('Service: Auth', () => {
],
});
httpMock = TestBed.inject(HttpTestingController)
authService = TestBed.get(AuthService);
authService.profile = [userRole, userRole2]
appConfigServiceSpy = TestBed.get(AppConfigService);
......@@ -104,6 +105,10 @@ describe('Service: Auth', () => {
});
}));
afterEach(() => {
httpMock.verify();
store = {};
});
it('should create service', () => {
expect(authService).toBeTruthy();
......@@ -180,8 +185,11 @@ describe('Service: Auth', () => {
});
it('should remove token on logout', () => {
store['oidc-token'] = 'some-oidc-token';
authService.logout();
expect(store['token']).not.toBeDefined();
const req = httpMock.expectOne('http://api.url/oidc/logout/some-oidc-token');
req.flush({});
});
it('should be logged in when token is present and valid', () => {
......@@ -197,4 +205,58 @@ describe('Service: Auth', () => {
expect(r).toEqual(false);
});
it('should store token and oidc token in localStorage', () => {
authService.storeToken('abc123');
expect(store['token']).toEqual('abc123');
authService.storeOidcToken('oidc456');
expect(store['oidc-token']).toEqual('oidc456');
});
it('should remove roles from localStorage', () => {
store['rolesToken'] = 'some_roles';
authService.removeRoles();
expect(store['rolesToken']).toBeUndefined();
});
it('should load and parse roles from localStorage', () => {
const roles = [{domainId: 1, role: Role.ROLE_USER, domainName: 'x'}];
store['rolesToken'] = JSON.stringify(roles);
const result = authService.loadRoles();
expect(result.length).toEqual(1);
expect(result[0].role).toEqual(Role.ROLE_USER);
});
it('should assign loaded roles to profile', () => {
const roles = [{domainId: 2, role: Role.ROLE_DOMAIN_ADMIN, domainName: 'x'}];
store['rolesToken'] = JSON.stringify(roles);
authService.loadAndSaveRoles();
expect(authService.profile[0].role).toEqual(Role.ROLE_DOMAIN_ADMIN);
});
it('should stringify and store roles', () => {
const roles = [new UserRole()];
roles[0].domainId = 1;
roles[0].role = Role.ROLE_USER;
roles[0].domainName = 'dom1';
authService.storeRoles(roles);
expect(store['rolesToken']).toContain('ROLE_USER');
});
it('should get global role from token', () => {
const result = authService.getGlobalRole();
expect(result).toContain('ROLE_SYSTEM_ADMIN');
});
it('should handle login error with catchError', waitForAsync(() => {
authService.login('user', 'pass').subscribe({
next: () => fail('Expected error'),
error: (err) => {
expect(err.status).toEqual(401);
}
});
const req = httpMock.expectOne('http://api.url/auth/basic/login');
req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' });
}));
});
import {BehaviorSubject, Observable, Subject, throwError as observableThrowError, of} from 'rxjs';
import {BehaviorSubject, Observable, of, Subject, throwError as observableThrowError} from 'rxjs';
import {catchError, debounceTime, map} from 'rxjs/operators';
import {Injectable} from '@angular/core';
import {AppConfigService, ConfigurationService} from '../service';
import {JwtHelperService} from '@auth0/angular-jwt';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {User} from '../model';
import {ProfileService} from '../service/profile.service';
import {Role, UserRole} from '../model/userrole';
import {interval, Subscription} from 'rxjs';
export class DomainRoles {
......@@ -17,10 +15,6 @@ export class DomainRoles {
) {
}
public getDomainId(): number {
return this.domainId;
}
public getRoles(): string[] {
return this.roles;
}
......@@ -183,15 +177,6 @@ export class AuthService {
return this.jwtHelper.decodeToken(token).global_role;
}
public getDomainsRoles() {
const token = this.getToken();
if (token == null) {
return null;
}
return this.jwtHelper.decodeToken(token).roles;
}
public getDomainRoles(): Map<number, DomainRoles> {
const domainRolesMap: Map<number, DomainRoles> = new Map<number, DomainRoles>();
......@@ -253,6 +238,49 @@ export class AuthService {
return domainsWithRole;
}
public oidcLinkingLogin(oidcToken: string,
email: string,
password: string,
uuid: string,
firstName: string,
lastName: string) {
const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'});
return this.http.post(this.appConfig.config.apiUrl + '/oidc/link',
JSON.stringify(
{
'oidcToken': oidcToken,
'email': email,
'password': password,
'uuid': uuid,
'firstName': firstName,
'lastName': lastName,
}
),
{headers: headers}).pipe(
debounceTime(1000),
map((res: Response) => {
const token = res && res['token'];
const oidcToken = res && res['oidcToken'];
if (token && oidcToken) {
this.storeToken(token);
this.storeOidcToken(oidcToken);
this.loginUsingSsoService = false;
this.isLoggedInSubject.next(true);
this.profileService.getRoles().subscribe(profile => {
this.profile = profile
this.storeRoles(profile);
return true;
})
} else {
this.isLoggedInSubject.next(false);
return false;
}
}
),
)
}
public login(username: string, password: string): Observable<boolean> {
// hack so test instance modal is shown onl after login
localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True');
......@@ -306,49 +334,6 @@ export class AuthService {
}));
}
public propagateSSOLogin(userid: string): Observable<boolean> {
console.debug('propagateSSOLogin');
console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl);
console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl + '/auth/sso/login');
console.debug('propagateSSOLogin ' + userid);
// hack so test instance modal is shown onl after login
localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True');
if (this.maintenance) {
this.isLoggedInSubject.next(false);
return of(false);
}
const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'});
return this.http.post(this.appConfig.config.apiUrl + '/auth/sso/login',
JSON.stringify({'userid': userid}), {headers: headers}).pipe(
debounceTime(10000),
map((response: Response) => {
console.debug('SSO login response: ' + response);
// login successful if there's a jwt token in the response
const token = response && response['token'];
if (token) {
this.storeToken(token);
console.debug('SSO AUTH | User: ' + this.getUsername());
console.debug('SSO AUTH | Domains: ' + this.getDomains());
console.debug('SSO AUTH | Roles: ' + this.getRoles());
console.debug('SSO AUTH | DomainRoles: ' + this.getDomainRoles());
this.loginUsingSsoService = true;
this.isLoggedInSubject.next(true);
return true;
} else {
// return false to indicate failed login
this.isLoggedInSubject.next(false);
return false;
}
}),
catchError((error) => {
console.error('SSO login error: ' + error.error['message']);
return observableThrowError(error);
}));
}
public logout(): void {
const oidcToken = this.getOidcToken();
this.refresh = null;
......@@ -366,6 +351,11 @@ export class AuthService {
}
}
public oidcLogout(oidcToken: string): void {
this.http.get(this.appConfig.config.apiUrl + '/oidc/logout/' + oidcToken).subscribe(() => {
})
}
public isLogged(): boolean {
const token = this.getToken();
if (token == null) {
......@@ -374,11 +364,6 @@ export class AuthService {
return (token ? !this.jwtHelper.isTokenExpired(token) : false);
}
get isLoggedIn$(): Observable<boolean> {
return this.isLoggedInSubject.pipe(
debounceTime(100), // use debounceTime to aggregate multiple emissions https://rxjs.dev/api/operators/debounceTime
);
}
public getDomainIds(): number[] {
return Array.from(new Set(this.profile.map(ur => ur.domainId)));
......
<div style="display: flex; justify-content: center;">
<div style="
margin-top: 50px;
width: 60%
" class="panel panel-default">
<div class="panel-heading">{{ 'ACCOUNT_LINKING.HEADER' | translate }}</div>
<div class="panel-body">
<form *ngIf="user"
class="form-horizontal" #userDetailsForm="ngForm">
<div>
<p>
{{ 'ACCOUNT_LINKING.INFO' | translate }}
</p>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'USER_DETAILS.FIRST_NAME' | translate }}</label>
<div class="col-sm-10">
<p class="form-control-static">{{ user.firstname }}</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'USER_DETAILS.LAST_NAME' | translate }}</label>
<div class="col-sm-10">
<p class="form-control-static">{{ user.lastname }}</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'USER_DETAILS.EMAIL' | translate }}</label>
<div class="col-sm-10">
<p class="form-control-static">{{ user.email }}</p>
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">{{ 'PASSWORD.PASSWORD' | translate }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password"
name="password" [(ngModel)]="password">
</div>
</div>
<button type="submit" class="btn btn-primary"
(click)="submit()">{{ 'ACCOUNT_LINKING.CONFIRM' | translate }}
</button>
</form>
<br>
</div>
</div>
</div>
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LinkAccountComponent} from './link-account.component';
import {ActivatedRoute} from '@angular/router';
import {of} from 'rxjs';
import {AuthService} from '../../auth/auth.service';
import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core';
describe('LinkAccountComponent', () => {
let component: LinkAccountComponent;
let fixture: ComponentFixture<LinkAccountComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LinkAccountComponent],
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader
}
})],
providers: [
{
provide: ActivatedRoute,
useValue: {
queryParams: of({
oidc_token: 'mocked.jwt.token'
}),
snapshot: {
paramMap: {
get: () => null
}
}
}
},
{
provide: AuthService,
useValue: {
isLogged: () => true,
oidcLogout: jasmine.createSpy('oidcLogout'),
oidcLinkingLogin: jasmine.createSpy('oidcLinkingLogin').and.returnValue(of({}))
}
},
]
})
.compileComponents();
fixture = TestBed.createComponent(LinkAccountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {Component, OnDestroy, OnInit} from '@angular/core';
import {User} from '../../model';
import {ActivatedRoute, Router} from '@angular/router';
import jwtDecode from 'jwt-decode';
import {AuthService} from '../../auth/auth.service';
@Component({
selector: 'app-link-account',
templateUrl: './link-account.component.html',
styleUrl: './link-account.component.css'
})
export class LinkAccountComponent implements OnInit, OnDestroy {
public user: User;
private token: string;
public password: string;
constructor(
private readonly route: ActivatedRoute,
private readonly authService: AuthService,
private readonly router: Router
) {
}
ngOnDestroy() {
if (!this.authService.isLogged()) {
this.authService.oidcLogout(this.token)
}
}
ngOnInit() {
this.route.queryParams.subscribe(param => {
this.token = param['oidc_token'];
const decoded: TokenPayload = jwtDecode<TokenPayload>(this.token);
this.user = new User();
this.user.username = decoded.sub;
this.user.firstname = decoded.given_name;
this.user.lastname = decoded.family_name;
this.user.email = decoded.email;
})
}
public submit(): void {
this.authService.oidcLinkingLogin(
this.token,
this.user.email,
this.password,
this.user.username,
this.user.firstname,
this.user.lastname,
).subscribe(
() => {
this.router.navigate(['/']);
}
)
}
}
interface TokenPayload {
sub: string;
email: string;
given_name: string;
family_name: string;
}
......@@ -64,9 +64,12 @@ export class LoginComponent implements OnInit {
);
}
// only for use in linking accounts
public triggerOIDC() {
if (this.configuration.maintenance) {
window.location.href = this.appConfig.getOidcUrl();
}
}
public sendResetNotification() {
if (this.resetPasswordForm.valid) {
......
......@@ -20,6 +20,7 @@ import {TranslateModule} from '@ngx-translate/core';
import {PasswordResetComponent} from './passwordreset/password-reset.component';
import {PasswordStrengthMeterComponent} from 'angular-password-strength-meter';
import {PolicySubpageComponent} from './policy-subpage/policy-subpage.component';
import {LinkAccountComponent} from './link-account/link-account.component';
@NgModule({
declarations: [
......@@ -31,7 +32,8 @@ import {PolicySubpageComponent} from './policy-subpage/policy-subpage.component'
CompleteComponent,
TermsAcceptanceComponent,
PasswordResetComponent,
PolicySubpageComponent
PolicySubpageComponent,
LinkAccountComponent
],
imports: [
FormsModule,
......@@ -54,4 +56,5 @@ import {PolicySubpageComponent} from './policy-subpage/policy-subpage.component'
ContentDisplayService
]
})
export class WelcomeModule {}
export class WelcomeModule {
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment