feat(login): Now adding orgnr to all API requests. (TV-399)

Squashed commit of the following:

commit b0cc0cf07a4eeaf85c8fdfc111fee1898fa14185
Merge: be9d909 59ce393
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Aug 24 13:42:09 2021 +0200

    Merged develop and fixed conflicts

commit be9d909232326eb06221336a966fc40c7c88289d
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Aug 24 08:02:12 2021 +0200

    Updated auth guard to remove localstorage data when user is not logged in

commit a4a182f565689a44e612b9353ae46514c1b439c7
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Aug 23 15:08:00 2021 +0200

    Updated organization functionality to check if organization matches users organizations

commit c170245c2799118bbf7961e95d507885a0571de6
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Aug 20 14:34:33 2021 +0200

    Now saving organization instead of organization number

commit 7c19600f712f48c9c56ba797e4e281a82adcf72f
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Aug 20 13:43:27 2021 +0200

    Removed all headers from API requests from services

commit 7c243bafc63f0544e11f1fa8729a139615cb14c0
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Aug 20 13:18:19 2021 +0200

    Dynamically adding orgnr to interceptor
This commit is contained in:
Erik Tiekstra
2021-08-24 13:43:03 +02:00
parent 59ce393259
commit 50a83f784d
19 changed files with 242 additions and 274 deletions

View File

@@ -56,6 +56,7 @@ const routes: Routes = [
path: 'logga-ut', path: 'logga-ut',
data: { title: 'Logga ut' }, data: { title: 'Logga ut' },
loadChildren: () => import('./pages/logout/logout.module').then(m => m.LogoutModule), loadChildren: () => import('./pages/logout/logout.module').then(m => m.LogoutModule),
canActivate: [AuthGuard],
}, },
{ {
path: 'organization-picker', path: 'organization-picker',
@@ -66,6 +67,7 @@ const routes: Routes = [
path: 'mitt-konto', path: 'mitt-konto',
data: { title: 'Mitt konto' }, data: { title: 'Mitt konto' },
loadChildren: () => import('./pages/my-account/my-account.module').then(m => m.MyAccountModule), loadChildren: () => import('./pages/my-account/my-account.module').then(m => m.MyAccountModule),
canActivate: [AuthGuard],
}, },
]; ];

View File

@@ -1,9 +1,8 @@
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorHandler, NgModule } from '@angular/core'; import { ErrorHandler, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { AuthGuard } from '@msfa-guards/auth.guard'; import { AuthInterceptor } from '@msfa-interceptors/auth.interceptor';
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler.module'; import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler.module';
import { AuthInterceptor } from '@msfa-services/api/auth.interceptor';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { ToastListModule } from './components/toast-list/toast-list.module'; import { ToastListModule } from './components/toast-list/toast-list.module';
@@ -18,7 +17,6 @@ import { AvropModule } from './pages/avrop/avrop.module';
useClass: CustomErrorHandler, useClass: CustomErrorHandler,
}, },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@@ -6,11 +6,11 @@
att logga ut applikationen och logga in på nytt. att logga ut applikationen och logga in på nytt.
</p> </p>
</digi-typography> </digi-typography>
<ng-container *ngIf="user$ | async as user"> <ng-container *ngIf="organizations$ | async as organizations">
<msfa-organization-picker-form <msfa-organization-picker-form
class="organization-picker__form" class="organization-picker__form"
*ngIf="user.organizations?.length !== 1" *ngIf="organizations.length !== 1"
[organizations]="user.organizations" [organizations]="organizations"
(selectedOrganizationChanged)="loginWithOrganization($event)" (selectedOrganizationChanged)="loginWithOrganization($event)"
></msfa-organization-picker-form> ></msfa-organization-picker-form>
</ng-container> </ng-container>

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive'; import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { Organization } from '@msfa-models/organization.model'; import { Organization } from '@msfa-models/organization.model';
import { UserService } from '@msfa-services/api/user.service'; import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
export const redirectUriQueryParam = 'redirect_uri'; import { filter, map } from 'rxjs/operators';
@Component({ @Component({
selector: 'msfa-organization-picker', selector: 'msfa-organization-picker',
@@ -12,38 +12,25 @@ export const redirectUriQueryParam = 'redirect_uri';
styleUrls: ['./organization-picker.component.scss'], styleUrls: ['./organization-picker.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class OrganizationPickerComponent extends UnsubscribeDirective implements OnInit { export class OrganizationPickerComponent extends UnsubscribeDirective {
user$ = this.userService.user$; organizations$: Observable<Organization[]> = this.userService.user$.pipe(
filter(user => !!(user && user.organizations?.length)),
map(({ organizations }) => organizations)
);
private redirectUri: string | null = null; constructor(private userService: UserService, private router: Router) {
constructor(private userService: UserService, private router: Router, private route: ActivatedRoute) {
super(); super();
super.unsubscribeOnDestroy( super.unsubscribeOnDestroy(
this.user$.subscribe(user => { this.organizations$.subscribe(organizations => {
if (user?.organizations?.length === 1) { if (organizations.length === 1) {
this.loginWithOrganization(user.organizations[0]); this.loginWithOrganization(organizations[0]);
} }
}) })
); );
} }
ngOnInit(): void {
super.unsubscribeOnDestroy(
this.route.queryParams.subscribe(params => {
this.redirectUri = params[redirectUriQueryParam] ? decodeURI(params[redirectUriQueryParam]) : null;
})
);
}
loginWithOrganization(organization: Organization): void { loginWithOrganization(organization: Organization): void {
this.userService.setSelectedUserOrganization(organization); this.userService.setSelectedOrganization(organization);
void this.router.navigateByUrl('/');
if (this.redirectUri) {
location.href = this.redirectUri;
return;
}
this.router.navigateByUrl('/');
} }
} }

View File

@@ -21,7 +21,7 @@ export class LayoutComponent extends UnsubscribeDirective {
routerLink: '/', routerLink: '/',
}; };
private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this.startBreadcrumb]); private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this.startBreadcrumb]);
isLoggedIn$: Observable<boolean> = this.authService.isLoggedIn$; isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
user$: Observable<User> = this.isLoggedIn$.pipe( user$: Observable<User> = this.isLoggedIn$.pipe(
filter(loggedIn => !!loggedIn), filter(loggedIn => !!loggedIn),
switchMap(() => this.userService.user$) switchMap(() => this.userService.user$)
@@ -34,7 +34,7 @@ export class LayoutComponent extends UnsubscribeDirective {
constructor( constructor(
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private authService: AuthenticationService, private authenticationService: AuthenticationService,
private userService: UserService private userService: UserService
) { ) {
super(); super();

View File

@@ -1 +1,9 @@
export const selectedUserOrganizationNumberKey = 'selectedOrganizationId'; export const AUTH_TOKEN_KEY = 'id_token';
export const AUTH_TOKEN_EXPIRE_KEY = 'expires_at';
export const SELECTED_ORGANIZATION_NUMBER_KEY = 'selected_orgnr';
export const ALL_LOCAL_STORAGE_KEYS = {
AUTH_TOKEN_KEY,
AUTH_TOKEN_EXPIRE_KEY,
SELECTED_ORGANIZATION_NUMBER_KEY,
};

View File

@@ -1,33 +1,39 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { UserService } from '@msfa-services/api/user.service';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { AuthenticationService } from '@msfa-services/api/authentication.service'; import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { UserService } from '@msfa-services/api/user.service';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { redirectUriQueryParam } from '../../pages/organization-picker/organization-picker.component';
@Injectable() @Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor( constructor(
private authenticationService: AuthenticationService, private authenticationService: AuthenticationService,
private userService: UserService, private router: Router,
private router: Router private userService: UserService
) {} ) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authenticationService.isLoggedIn$.pipe( return this.authenticationService.isLoggedIn$.pipe(
switchMap(loggedIn => { switchMap(loggedIn => {
if (loggedIn) { if (loggedIn) {
if (!this.authenticationService.hasSelectedUserOrganization()) { return this.userService.selectedOrganization$.pipe(
this.router.navigateByUrl(`/organization-picker?${redirectUriQueryParam}=${encodeURI(location.href)}`); map(organization => {
} if (!organization) {
void this.router.navigateByUrl(`/organization-picker`);
return of(true); }
return true;
})
);
} else if (route.queryParams.code) { } else if (route.queryParams.code) {
return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result)); return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result));
} }
void this.authenticationService.removeLocalStorageData();
if (environment.environment === 'local') { if (environment.environment === 'local') {
void this.router.navigateByUrl(environment.loginUrl); void this.router.navigateByUrl(environment.loginUrl);
} else { } else {

View File

@@ -0,0 +1,31 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SELECTED_ORGANIZATION_NUMBER_KEY } from '@msfa-constants/local-storage-keys';
import { environment } from '@msfa-environment';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
private get authorizationToken(): { Authorization: string } | null {
const bearerToken = this.authenticationService.currentAuthorizationToken;
return bearerToken ? { Authorization: `Bearer ${bearerToken}` } : null;
}
private get selectedOrganizationNumber(): { orgnr: string } | null {
const orgnr = localStorage.getItem(SELECTED_ORGANIZATION_NUMBER_KEY);
return orgnr ? { orgnr } : null;
}
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const clonedRequest: HttpRequest<unknown> = req.clone({
setHeaders: { ...environment.api.headers, ...this.authorizationToken, ...this.selectedOrganizationNumber },
});
return next.handle(clonedRequest);
}
}

View File

@@ -1,23 +0,0 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthenticationService } from './authentication.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthenticationService) {}
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const idToken = this.auth.currentAuthorizationToken;
if (idToken) {
const cloned = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + idToken),
});
return next.handle(cloned);
} else {
return next.handle(req);
}
}
}

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { selectedUserOrganizationNumberKey } from '@msfa-constants/local-storage-keys'; import { ALL_LOCAL_STORAGE_KEYS, AUTH_TOKEN_EXPIRE_KEY, AUTH_TOKEN_KEY } from '@msfa-constants/local-storage-keys';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model'; import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model';
import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model'; import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model';
@@ -9,8 +9,6 @@ import { add, isBefore } from 'date-fns';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -36,17 +34,16 @@ export class AuthenticationService {
const expiresAt = add(new Date(), { seconds: authenticationResult.expiresIn }); const expiresAt = add(new Date(), { seconds: authenticationResult.expiresIn });
this._token$.next(authenticationResult.idToken); this._token$.next(authenticationResult.idToken);
localStorage.setItem('id_token', authenticationResult.idToken); localStorage.setItem(AUTH_TOKEN_KEY, authenticationResult.idToken);
this._expiration$.next(expiresAt.valueOf()); this._expiration$.next(expiresAt.valueOf());
localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); localStorage.setItem(AUTH_TOKEN_EXPIRE_KEY, JSON.stringify(expiresAt.valueOf()));
} }
login$(authorizationCodeFromCiam: string): Observable<Authentication> { login$(authorizationCodeFromCiam: string): Observable<Authentication> {
this.removeLocalStorageData();
return this.httpClient return this.httpClient
.get<{ data: AuthenticationResponse }>( .get<{ data: AuthenticationResponse }>(AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam))
AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam),
API_HEADERS
)
.pipe( .pipe(
map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)), map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)),
tap(authenticationResult => { tap(authenticationResult => {
@@ -56,8 +53,8 @@ export class AuthenticationService {
} }
private _getLocalStorageData(): { id_token: string; expires_at: number } { private _getLocalStorageData(): { id_token: string; expires_at: number } {
const id_token = localStorage.getItem('id_token'); const id_token = localStorage.getItem(AUTH_TOKEN_KEY);
const expiration = localStorage.getItem('expires_at'); const expiration = localStorage.getItem(AUTH_TOKEN_EXPIRE_KEY);
return id_token && expiration return id_token && expiration
? { ? {
@@ -67,6 +64,12 @@ export class AuthenticationService {
: null; : null;
} }
public removeLocalStorageData(): void {
Object.values(ALL_LOCAL_STORAGE_KEYS).forEach(value => {
localStorage.removeItem(value);
});
}
get currentAuthorizationToken(): string { get currentAuthorizationToken(): string {
return this._token$.getValue(); return this._token$.getValue();
} }
@@ -81,9 +84,7 @@ export class AuthenticationService {
} }
logout(): void { logout(): void {
localStorage.removeItem('id_token'); this.removeLocalStorageData();
localStorage.removeItem('expires_at');
localStorage.removeItem(selectedUserOrganizationNumberKey);
if (environment.environment === 'local') { if (environment.environment === 'local') {
void this.router.navigateByUrl(environment.logoutUrl); void this.router.navigateByUrl(environment.logoutUrl);
@@ -91,8 +92,4 @@ export class AuthenticationService {
document.location.href = environment.logoutUrl; document.location.href = environment.logoutUrl;
} }
} }
hasSelectedUserOrganization(): boolean {
return !!localStorage.getItem(selectedUserOrganizationNumberKey);
}
} }

View File

@@ -9,15 +9,13 @@ import {
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthorizationService { export class AuthorizationService {
private _authorizationsApiUrl = `${environment.api.url}/authorizations`; private _apiBaseUrl = `${environment.api.url}/authorizations`;
public authorizations$: Observable<Authorization[]> = this.httpClient public authorizations$: Observable<Authorization[]> = this.httpClient
.get<AuthorizationApiResponse>(this._authorizationsApiUrl, API_HEADERS) .get<AuthorizationApiResponse>(this._apiBaseUrl)
.pipe(map(({ data }) => data.map(authorization => mapAuthorizationApiResponseToAuthorization(authorization)))); .pipe(map(({ data }) => data.map(authorization => mapAuthorizationApiResponseToAuthorization(authorization))));
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}

View File

@@ -14,8 +14,6 @@ import { Observable, of } from 'rxjs';
import { delay, filter, map } from 'rxjs/operators'; import { delay, filter, map } from 'rxjs/operators';
import { HandledareAvrop } from '../../../pages/avrop/models/handledare-avrop'; import { HandledareAvrop } from '../../../pages/avrop/models/handledare-avrop';
const API_HEADERS = { headers: environment.api.headers };
const tempHandledareMock: HandledareAvrop[] = [ const tempHandledareMock: HandledareAvrop[] = [
{ id: '1', fullName: 'Göran Persson' }, { id: '1', fullName: 'Göran Persson' },
{ id: '2', fullName: 'Stefan Löfven' }, { id: '2', fullName: 'Stefan Löfven' },
@@ -38,12 +36,10 @@ export class AvropApiService {
offset = 0, offset = 0,
limit = 20 limit = 20
): Observable<Avrop[]> { ): Observable<Avrop[]> {
return this.httpClient return this.httpClient.get<{ data: AvropResponse[] }>(`${this._apiBaseUrl}`).pipe(
.get<{ data: AvropResponse[] }>(`${this._apiBaseUrl}`, { ...API_HEADERS }) filter(response => !!response?.data),
.pipe( map(({ data }) => data.map(avrop => mapAvropResponseToAvrop(avrop)))
filter(response => !!response?.data), );
map(({ data }) => data.map(avrop => mapAvropResponseToAvrop(avrop)))
);
} }
getSelectableHandledare$(deltagare: Avrop[]): Observable<HandledareAvrop[]> { getSelectableHandledare$(deltagare: Avrop[]): Observable<HandledareAvrop[]> {
@@ -67,7 +63,7 @@ export class AvropApiService {
selectedKommuner: MultiselectFilterOption[] selectedKommuner: MultiselectFilterOption[]
): Observable<MultiselectFilterOption[]> { ): Observable<MultiselectFilterOption[]> {
return this.httpClient return this.httpClient
.get<{ data: UtforandeVerksamhetResponse[] }>(`${this._apiBaseUrl}/utforandeverksamheter`, { ...API_HEADERS }) .get<{ data: UtforandeVerksamhetResponse[] }>(`${this._apiBaseUrl}/utforandeverksamheter`)
.pipe( .pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => map(({ data }) =>
@@ -82,12 +78,10 @@ export class AvropApiService {
selectedTjanster: MultiselectFilterOption[], selectedTjanster: MultiselectFilterOption[],
selectedUtforandeVerksamheter: MultiselectFilterOption[] selectedUtforandeVerksamheter: MultiselectFilterOption[]
): Observable<MultiselectFilterOption[]> { ): Observable<MultiselectFilterOption[]> {
return this.httpClient return this.httpClient.get<{ data: KommunResponse[] }>(`${this._apiBaseUrl}/kommuner`).pipe(
.get<{ data: KommunResponse[] }>(`${this._apiBaseUrl}/kommuner`, { ...API_HEADERS }) filter(response => !!response?.data),
.pipe( map(({ data }) => data.map(kommun => ({ label: mapKommunResponseToKommun(kommun).name })))
filter(response => !!response?.data), );
map(({ data }) => data.map(kommun => ({ label: mapKommunResponseToKommun(kommun).name })))
);
} }
async tilldelaHandledare(deltagare: Avrop[], handledare: HandledareAvrop): Promise<void> { async tilldelaHandledare(deltagare: Avrop[], handledare: HandledareAvrop): Promise<void> {

View File

@@ -33,8 +33,6 @@ import { sortFromToDates } from '@msfa-utils/sort.util';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators'; import { catchError, filter, map, switchMap } from 'rxjs/operators';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -96,7 +94,6 @@ export class DeltagareService extends UnsubscribeDirective {
console.log(params); console.log(params);
return this.httpClient return this.httpClient
.get<DeltagareCompactApiResponse>(this._apiBaseUrl, { .get<DeltagareCompactApiResponse>(this._apiBaseUrl, {
...API_HEADERS,
params, params,
}) })
.pipe( .pipe(
@@ -112,32 +109,28 @@ export class DeltagareService extends UnsubscribeDirective {
} }
private _fetchContactInformation$(id: string): Observable<ContactInformation | Partial<ContactInformation>> { private _fetchContactInformation$(id: string): Observable<ContactInformation | Partial<ContactInformation>> {
return this.httpClient return this.httpClient.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${id}/contact`).pipe(
.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${id}/contact`, { ...API_HEADERS }) map(({ data }) => mapResponseToContactInformation(data)),
.pipe( catchError(error => {
map(({ data }) => mapResponseToContactInformation(data)), this.errorService.add(errorToCustomError(error));
catchError(error => { return of({});
this.errorService.add(errorToCustomError(error)); })
return of({}); );
})
);
} }
private _fetchDriversLicense$(id: string): Observable<DriversLicense | Partial<DriversLicense>> { private _fetchDriversLicense$(id: string): Observable<DriversLicense | Partial<DriversLicense>> {
return this.httpClient return this.httpClient.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${id}/driverlicense`).pipe(
.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${id}/driverlicense`, { ...API_HEADERS }) map(({ data }) => mapResponseToDriversLicense(data)),
.pipe( catchError(error => {
map(({ data }) => mapResponseToDriversLicense(data)), this.errorService.add(errorToCustomError(error));
catchError(error => { return of({});
this.errorService.add(errorToCustomError(error)); })
return of({}); );
})
);
} }
private _fetchHighestEducation$(id: string): Observable<HighestEducation | Partial<HighestEducation>> { private _fetchHighestEducation$(id: string): Observable<HighestEducation | Partial<HighestEducation>> {
return this.httpClient return this.httpClient
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${id}/educationlevels/highest`, { ...API_HEADERS }) .get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${id}/educationlevels/highest`)
.pipe( .pipe(
map(({ data }) => mapResponseToHighestEducation(data)), map(({ data }) => mapResponseToHighestEducation(data)),
catchError(error => { catchError(error => {
@@ -148,88 +141,76 @@ export class DeltagareService extends UnsubscribeDirective {
} }
private _fetchEducations$(id: string): Observable<Education[]> { private _fetchEducations$(id: string): Observable<Education[]> {
return this.httpClient return this.httpClient.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${id}/educations`).pipe(
.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${id}/educations`, { ...API_HEADERS }) map(({ data }) =>
.pipe( data.utbildningar
map(({ data }) => ? data.utbildningar.sort((a, b) =>
data.utbildningar sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom })
? data.utbildningar.sort((a, b) => )
sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom }) : []
) ),
: [] map(educations => educations.map(utbildning => mapResponseToEducation(utbildning))),
), catchError(error => {
map(educations => educations.map(utbildning => mapResponseToEducation(utbildning))), this.errorService.add(errorToCustomError(error));
catchError(error => { return of([]);
this.errorService.add(errorToCustomError(error)); })
return of([]); );
})
);
} }
private _fetchTranslator$(id: string): Observable<string> { private _fetchTranslator$(id: string): Observable<string> {
return this.httpClient return this.httpClient.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${id}/translator`).pipe(
.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${id}/translator`, { ...API_HEADERS }) map(({ data }) => data.sprak?.beskrivning || null),
.pipe( catchError(error => {
map(({ data }) => data.sprak?.beskrivning || null), this.errorService.add(errorToCustomError(error));
catchError(error => { return of('');
this.errorService.add(errorToCustomError(error)); })
return of(''); );
})
);
} }
private _fetchWorkLanguages$(id: string): Observable<string[]> { private _fetchWorkLanguages$(id: string): Observable<string[]> {
return this.httpClient return this.httpClient.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${id}/work/languages`).pipe(
.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${id}/work/languages`, { ...API_HEADERS }) map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []),
.pipe( catchError(error => {
map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []), this.errorService.add(errorToCustomError(error));
catchError(error => { return of([]);
this.errorService.add(errorToCustomError(error)); })
return of([]); );
})
);
} }
private _fetchDisabilities$(id: string): Observable<Disability[]> { private _fetchDisabilities$(id: string): Observable<Disability[]> {
return this.httpClient return this.httpClient.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${id}/work/disabilities`).pipe(
.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${id}/work/disabilities`, { ...API_HEADERS }) map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []),
.pipe( catchError(error => {
map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []), this.errorService.add(errorToCustomError(error));
catchError(error => { return of([]);
this.errorService.add(errorToCustomError(error)); })
return of([]); );
})
);
} }
private _fetchWorkExperiences$(id: string): Observable<WorkExperience[]> { private _fetchWorkExperiences$(id: string): Observable<WorkExperience[]> {
return this.httpClient return this.httpClient.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${id}/work/experiences`).pipe(
.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${id}/work/experiences`, { ...API_HEADERS }) map(
.pipe( ({ data }) =>
map( data?.arbetslivserfarenheter?.sort((a, b) =>
({ data }) => sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom })
data?.arbetslivserfarenheter?.sort((a, b) => ) || []
sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom }) ),
) || [] map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))),
), catchError(error => {
map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))), this.errorService.add(errorToCustomError(error));
catchError(error => { return of([]);
this.errorService.add(errorToCustomError(error)); })
return of([]); );
})
);
} }
private _fetchAvropInformation$(id: string): Observable<Avrop | Partial<Avrop>> { private _fetchAvropInformation$(id: string): Observable<Avrop | Partial<Avrop>> {
return this.httpClient return this.httpClient.get<{ data: AvropResponse }>(`${this._apiBaseUrl}/${id}/avrop`).pipe(
.get<{ data: AvropResponse }>(`${this._apiBaseUrl}/${id}/avrop`, { ...API_HEADERS }) map(({ data }) => (data ? mapAvropResponseToAvrop(data) : {})),
.pipe( catchError(error => {
map(({ data }) => (data ? mapAvropResponseToAvrop(data) : {})), this.errorService.add(errorToCustomError(error));
catchError(error => { return of({});
this.errorService.add(errorToCustomError(error)); })
return of({}); );
})
);
} }
// As TypeScript has some limitations regarding combining Observables this way, // As TypeScript has some limitations regarding combining Observables this way,

View File

@@ -26,13 +26,11 @@ import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs
import { catchError, filter, map, switchMap, take } from 'rxjs/operators'; import { catchError, filter, map, switchMap, take } from 'rxjs/operators';
import { TjanstService } from './tjanst.service'; import { TjanstService } from './tjanst.service';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EmployeeService extends UnsubscribeDirective { export class EmployeeService extends UnsubscribeDirective {
private _apiUrl = `${environment.api.url}/users`; private _apiBaseUrl = `${environment.api.url}/users`;
private _currentEmployeeId$ = new BehaviorSubject<string>(null); private _currentEmployeeId$ = new BehaviorSubject<string>(null);
private _limit$ = new BehaviorSubject<number>(20); private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1); private _page$ = new BehaviorSubject<number>(1);
@@ -43,6 +41,7 @@ export class EmployeeService extends UnsubscribeDirective {
private _employee$ = new BehaviorSubject<Employee>(null); private _employee$ = new BehaviorSubject<Employee>(null);
public employee$: Observable<Employee> = this._employee$.asObservable(); public employee$: Observable<Employee> = this._employee$.asObservable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private errorService: ErrorService, private errorService: ErrorService,
@@ -118,10 +117,7 @@ export class EmployeeService extends UnsubscribeDirective {
} }
return this.httpClient return this.httpClient
.get<EmployeesApiResponse>(this._apiUrl, { .get<EmployeesApiResponse>(this._apiBaseUrl, { params })
...API_HEADERS,
params,
})
.pipe( .pipe(
map(({ data, meta }) => { map(({ data, meta }) => {
return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta }; return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
@@ -130,15 +126,13 @@ export class EmployeeService extends UnsubscribeDirective {
} }
private _fetchEmployee$(id: string): Observable<Employee | Partial<Employee>> { private _fetchEmployee$(id: string): Observable<Employee | Partial<Employee>> {
return this.httpClient return this.httpClient.get<{ data: EmployeeResponse }>(`${this._apiBaseUrl}/${id}`).pipe(
.get<{ data: EmployeeResponse }>(`${this._apiUrl}/${id}`, { ...API_HEADERS }) map(({ data }) => mapResponseToEmployee(data)),
.pipe( catchError(error => {
map(({ data }) => mapResponseToEmployee(data)), this.errorService.add(errorToCustomError(error));
catchError(error => { return of({});
this.errorService.add(errorToCustomError(error)); })
return of({}); );
})
);
} }
public setSearchFilter(value: string): void { public setSearchFilter(value: string): void {
@@ -151,18 +145,16 @@ export class EmployeeService extends UnsubscribeDirective {
// Not done, waiting for delete api http response // Not done, waiting for delete api http response
public deleteEmployee(id: string): Observable<any> { public deleteEmployee(id: string): Observable<any> {
return this.httpClient return this.httpClient.delete<DeleteEmployeeMockApiResponse>(`${this._apiBaseUrl}/${id}`).pipe(
.delete<DeleteEmployeeMockApiResponse>(`${this._apiUrl}/${id}`, { ...API_HEADERS }) take(1),
.pipe( map(response => {
take(1), return {
map(response => { status: response.status || 200, // mockresponse
return { message: response.message || 'deleted succeeded', // mockresponse
status: response.status || 200, // mockresponse };
message: response.message || 'deleted succeeded', // mockresponse }),
}; catchError(error => throwError({ message: error as string, type: ErrorType.API }))
}), );
catchError(error => throwError({ message: error as string, type: ErrorType.API }))
);
} }
public setSort(newSortKey: keyof EmployeeCompactResponse): void { public setSort(newSortKey: keyof EmployeeCompactResponse): void {
@@ -178,7 +170,7 @@ export class EmployeeService extends UnsubscribeDirective {
} }
public postNewEmployee(employeeData: Employee): Observable<string> { public postNewEmployee(employeeData: Employee): Observable<string> {
return this.httpClient.post<{ id: string }>(this._apiUrl, mapEmployeeToRequestData(employeeData), API_HEADERS).pipe( return this.httpClient.post<{ id: string }>(this._apiBaseUrl, mapEmployeeToRequestData(employeeData)).pipe(
map(({ id }) => id), map(({ id }) => id),
catchError(error => throwError({ message: error as string, type: ErrorType.API })) catchError(error => throwError({ message: error as string, type: ErrorType.API }))
); );
@@ -186,7 +178,7 @@ export class EmployeeService extends UnsubscribeDirective {
public postEmployeeInvitation(email: string): Observable<EmployeeInviteMockaData> { public postEmployeeInvitation(email: string): Observable<EmployeeInviteMockaData> {
return this.httpClient return this.httpClient
.post<{ data: EmployeeInviteMockApiResponse }>(`${this._apiUrl}/invite`, { email }, API_HEADERS) .post<{ data: EmployeeInviteMockApiResponse }>(`${this._apiBaseUrl}/invite`, { email })
.pipe( .pipe(
take(1), take(1),
map(res => res.data), map(res => res.data),

View File

@@ -23,15 +23,13 @@ function filterParticipants(participants: Participant[], searchFilter: string):
}); });
} }
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ParticipantsService { export class ParticipantsService {
private _apiUrl = `${environment.api.url}/participants`; private _apiBaseUrl = `${environment.api.url}/participants`;
private _allParticipants$: Observable<Participant[]> = this.httpClient private _allParticipants$: Observable<Participant[]> = this.httpClient
.get<ParticipantsApiResponse>(this._apiUrl) .get<ParticipantsApiResponse>(this._apiBaseUrl)
.pipe(map(response => response.data.map(participant => mapParticipantApiResponseToParticipant(participant)))); .pipe(map(response => response.data.map(participant => mapParticipantApiResponseToParticipant(participant))));
private _activeParticipantsSortBy$ = new BehaviorSubject<Sort<keyof Participant> | null>({ private _activeParticipantsSortBy$ = new BehaviorSubject<Sort<keyof Participant> | null>({
key: 'handleBefore', key: 'handleBefore',
@@ -79,7 +77,7 @@ export class ParticipantsService {
public fetchDetailedParticipantData$(id: string): Observable<Participant> { public fetchDetailedParticipantData$(id: string): Observable<Participant> {
return this.httpClient return this.httpClient
.get<ParticipantApiResponse>(`${this._apiUrl}/${id}`, { ...API_HEADERS }) .get<ParticipantApiResponse>(`${this._apiBaseUrl}/${id}`)
.pipe(map(result => mapParticipantApiResponseToParticipant(result.data))); .pipe(map(result => mapParticipantApiResponseToParticipant(result.data)));
} }

View File

@@ -5,15 +5,13 @@ import { mapServiceApiResponseToService, Service, ServiceApiResponse } from '@ms
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ServiceService { export class ServiceService {
private _servicesApiUrl = `${environment.api.url}/services`; private _apiBaseUrl = `${environment.api.url}/services`;
public services$: Observable<Service[]> = this.httpClient public services$: Observable<Service[]> = this.httpClient
.get<ServiceApiResponse>(this._servicesApiUrl, API_HEADERS) .get<ServiceApiResponse>(this._apiBaseUrl)
.pipe(map(response => response.data.map(service => mapServiceApiResponseToService(service)))); .pipe(map(response => response.data.map(service => mapServiceApiResponseToService(service))));
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}

View File

@@ -7,13 +7,11 @@ import { mapResponseToTjanst, Tjanst } from '@msfa-models/tjanst.model';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TjanstService extends UnsubscribeDirective { export class TjanstService extends UnsubscribeDirective {
private _apiUrl = `${environment.api.url}/tjanster`; private _apiBaseUrl = `${environment.api.url}/tjanster`;
private _tjanster$ = new BehaviorSubject<Tjanst[]>(null); private _tjanster$ = new BehaviorSubject<Tjanst[]>(null);
public tjanster$: Observable<Tjanst[]> = this._tjanster$.asObservable(); public tjanster$: Observable<Tjanst[]> = this._tjanster$.asObservable();
@@ -22,7 +20,7 @@ export class TjanstService extends UnsubscribeDirective {
filter(tjanster => !tjanster?.length), filter(tjanster => !tjanster?.length),
switchMap(() => switchMap(() =>
this.httpClient this.httpClient
.get<{ data: TjanstResponse[] }>(this._apiUrl, API_HEADERS) .get<{ data: TjanstResponse[] }>(this._apiBaseUrl)
.pipe(map(({ data }) => data.map(tjanst => mapResponseToTjanst(tjanst)))) .pipe(map(({ data }) => data.map(tjanst => mapResponseToTjanst(tjanst))))
) )
); );

View File

@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { selectedUserOrganizationNumberKey } from '@msfa-constants/local-storage-keys'; import { SELECTED_ORGANIZATION_NUMBER_KEY } from '@msfa-constants/local-storage-keys';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive'; import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { OrganizationResponse } from '@msfa-models/api/organization.response.model'; import { OrganizationResponse } from '@msfa-models/api/organization.response.model';
@@ -9,59 +9,64 @@ import { mapResponseToOrganization, Organization } from '@msfa-models/organizati
import { mapResponseToUserInfo, UserInfo } from '@msfa-models/user-info.model'; import { mapResponseToUserInfo, UserInfo } from '@msfa-models/user-info.model';
import { User } from '@msfa-models/user.model'; import { User } from '@msfa-models/user.model';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
import { AuthenticationService } from './authentication.service';
const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UserService extends UnsubscribeDirective { export class UserService extends UnsubscribeDirective {
private readonly selectedUserOrganizationNumberKey = 'selectedOrganizationId'; private _apiBaseUrl = `${environment.api.url}/auth`;
private _authApiUrl = `${environment.api.url}/auth`;
private _user$ = new BehaviorSubject<User>(null); private _user$ = new BehaviorSubject<User>(null);
public user$: Observable<User> = this._user$.asObservable(); public user$: Observable<User> = this._user$.asObservable();
private _selectedOrganizationNumber$ = new BehaviorSubject<string>(null);
constructor(private httpClient: HttpClient) { constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) {
super(); super();
super.unsubscribeOnDestroy( super.unsubscribeOnDestroy(
combineLatest([this._fetchUserInfo$(), this._fetchOrganizations$()]).subscribe(([userInfo, organizations]) => { this.authenticationService.isLoggedIn$
this._user$.next({ ...userInfo, organizations }); .pipe(
}) filter(loggedIn => !!loggedIn),
switchMap(() => combineLatest([this._fetchUserInfo$(), this._fetchOrganizations$()]))
)
.subscribe(([userInfo, organizations]) => {
this._user$.next({ ...userInfo, organizations });
})
); );
} this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
getSelectedUserOrganization(user: User): Organization {
if (!user) {
return null;
}
return user.organizations.find(
organization => organization.organizationNumber === localStorage.getItem(selectedUserOrganizationNumberKey)
);
}
setSelectedUserOrganization(organization: Organization): void {
if (!organization) {
return;
}
localStorage.setItem(selectedUserOrganizationNumberKey, organization?.organizationNumber);
} }
private _fetchOrganizations$(): Observable<Organization[]> { private _fetchOrganizations$(): Observable<Organization[]> {
return this.httpClient.get<{ data: OrganizationResponse[] }>(`${this._authApiUrl}/organizations`, API_HEADERS).pipe( return this.httpClient.get<{ data: OrganizationResponse[] }>(`${this._apiBaseUrl}/organizations`).pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(organization => mapResponseToOrganization(organization))) map(({ data }) => data.map(organization => mapResponseToOrganization(organization)))
); );
} }
private _fetchUserInfo$(): Observable<UserInfo> { private _fetchUserInfo$(): Observable<UserInfo> {
return this.httpClient.get<{ data: UserInfoResponse }>(`${this._authApiUrl}/userinfo`, API_HEADERS).pipe( return this.httpClient.get<{ data: UserInfoResponse }>(`${this._apiBaseUrl}/userinfo`).pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => mapResponseToUserInfo(data)) map(({ data }) => mapResponseToUserInfo(data))
); );
} }
private get _selectedOrganizationNumber(): string | null {
return localStorage.getItem(SELECTED_ORGANIZATION_NUMBER_KEY);
}
public get selectedOrganization$(): Observable<Organization | null> {
return combineLatest([this._selectedOrganizationNumber$, this._user$]).pipe(
filter(([, user]) => !!user),
map(([organizationNumber, user]) => {
return organizationNumber
? user.organizations.find(organization => organization.organizationNumber === organizationNumber)
: null;
})
);
}
public setSelectedOrganization(organization: Organization): void {
localStorage.setItem(SELECTED_ORGANIZATION_NUMBER_KEY, organization.organizationNumber);
this._selectedOrganizationNumber$.next(organization.organizationNumber);
}
} }

View File

@@ -8,8 +8,6 @@ export const environment: Environment = {
production: false, production: false,
api: { api: {
url: '/api', url: '/api',
headers: { headers: {},
orgnr: '5564673381', // Until we have an organisation-selector
},
}, },
}; };