diff --git a/src/app/app.component.ts b/src/app/app.component.ts index be0a70745..58b587072 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -303,7 +303,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.subs.add( this.store$ .select(userStore.getUserAuth) - .pipe(filter((userAuth) => !!userAuth?.token)) + .pipe(filter((userAuth) => !!userAuth.token)) .subscribe((_) => this.store$.dispatch(new userStore.LoadProfile())), ); @@ -361,13 +361,10 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { ), ); - this.subs.add( - this.authService.getUser().subscribe((user) => { - if (user?.id) { - this.store$.dispatch(new userStore.Login(user)); - } - }), - ); + const user = this.authService.getUser(); + if (user.id) { + this.store$.dispatch(new userStore.Login(user)); + } this.subs.add( this.actions$ diff --git a/src/app/components/header/header-buttons/header-buttons.component.ts b/src/app/components/header/header-buttons/header-buttons.component.ts index e09de5402..021276ad5 100644 --- a/src/app/components/header/header-buttons/header-buttons.component.ts +++ b/src/app/components/header/header-buttons/header-buttons.component.ts @@ -26,7 +26,6 @@ import { AsfApiService, EnvironmentService, ScreenSizeService, - UserDataService, } from '@services'; import { CMRProduct, @@ -80,7 +79,6 @@ declare global { }) export class HeaderButtonsComponent implements OnInit, OnDestroy { authService = inject(AuthService); - userData = inject(UserDataService); env = inject(EnvironmentService); private http = inject(HttpClient); asfApiService = inject(AsfApiService); @@ -140,9 +138,9 @@ export class HeaderButtonsComponent implements OnInit, OnDestroy { ngOnInit() { this.subs.add( - this.store$.select(userStore.getUserAuth).subscribe((user) => { - this.userAuth = user; - }), + this.store$ + .select(userStore.getUserAuth) + .subscribe((user) => (this.userAuth = user)), ); this.subs.add( diff --git a/src/app/components/header/header-buttons/preferences/preferences.component.html b/src/app/components/header/header-buttons/preferences/preferences.component.html index c2967b398..e3193a416 100644 --- a/src/app/components/header/header-buttons/preferences/preferences.component.html +++ b/src/app/components/header/header-buttons/preferences/preferences.component.html @@ -1,6 +1,6 @@
- {{ 'PREFERENCES_FOR' | translate }} {{ userAuth?.id }} + {{ 'PREFERENCES_FOR' | translate }} {{ userAuth.id }}
diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts index 303bd1e7d..193ef43e8 100644 --- a/src/app/models/user.model.ts +++ b/src/app/models/user.model.ts @@ -4,7 +4,6 @@ export interface UserAuth { id: string | null; token: string | null; groups: URSGroup[]; - exp: number; } export interface UserProfile { diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 9c6b32f09..38a8e7def 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -2,19 +2,12 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { interval, Subject, Observable, of } from 'rxjs'; -import { - map, - takeUntil, - take, - catchError, - filter, - switchMap, - retry, -} from 'rxjs/operators'; +import { map, takeUntil, take, filter, catchError } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@store'; import { EnvironmentService } from './environment.service'; +import jwt_decode from 'jwt-decode'; import * as userStore from '@store/user'; import * as models from '@models'; @@ -28,25 +21,18 @@ export class AuthService { private http = inject(HttpClient); private notificationService = inject(NotificationService); private store$ = inject>(Store); - private existingUserInfo: models.UserAuth = null; private bc: BroadcastChannel; constructor() { if (typeof BroadcastChannel !== 'undefined') { this.bc = new BroadcastChannel('asf-vertex'); this.bc.onmessage = (_event: MessageEvent) => { - this.getUser() - .pipe( - take(1), - map((user) => { - if (!user?.id) { - this.store$.dispatch(new userStore.Logout()); - } else { - this.store$.dispatch(new userStore.Login(user)); - } - }), - ) - .subscribe(); + const user = this.getUser(); + if (!user.id) { + this.store$.dispatch(new userStore.Logout()); + } else { + this.store$.dispatch(new userStore.Login(user)); + } }; } } @@ -79,25 +65,29 @@ export class AuthService { ); const loginWindowClosed = new Subject(); + return interval(500).pipe( takeUntil(loginWindowClosed), - switchMap((_) => { + map((_) => { + let user = null; + if (loginWindow.closed) { loginWindowClosed.next(); - return this.getUser(); } + try { if (loginWindow.location.host === window.location.host) { loginWindow.close(); + user = this.getUser(); this.bc.postMessage({ event: 'login', }); - return this.getUser(); } } catch (_e) { // Do nothing } - return of(null); + + return user; }), catchError((_) => { this.notificationService.error('Trouble logging in', 'Error', { @@ -113,7 +103,7 @@ export class AuthService { public logout$(): Observable { return this.http - .get(`${this.authUrl}/logout`, { + .get(`${this.authUrl}/loginservice/logout`, { responseType: 'text', withCredentials: true, }) @@ -122,65 +112,70 @@ export class AuthService { this.bc.postMessage({ event: 'logout', }); - return this.existingUserInfo; + return this.getUser(); }), catchError((_) => { - // For now this always throws a CORS error, but it does still logout successfully - // this.notificationService.error('Trouble logging out', 'Error', { - // timeOut: 5000, - // }); - return of(this.existingUserInfo); + this.notificationService.error('Trouble logging out', 'Error', { + timeOut: 5000, + }); + return of(this.getUser()); }), take(1), ); } - public getUser(): Observable { - return this.http - .get(`${this.env.currentEnv.user_data}/info/cookie`, { - withCredentials: true, - }) - .pipe( - retry({ - count: 3, - delay: 100, - }), - map((user) => { - return this.makeUser( - user['urs-user-id'], - user['urs-groups'], - user['urs-access-token'], - user['exp'], - ); - }), - map((final) => { - this.existingUserInfo = final; - setTimeout( - () => { - this.store$.dispatch(new userStore.Logout()); - this.notificationService.info( - 'Session Expired', - 'Please login again', - ); - }, - final.exp * 1000 - Date.now(), + public getUser(): models.UserAuth { + const cookies = this.loadCookies(); + const token = cookies['asf-urs']; + + if (!token) { + return this.nullUser(); + } + try { + const user = jwt_decode(token); + + if (this.isExpired(user)) { + return this.nullUser(); + } + + setTimeout( + () => { + this.store$.dispatch(new userStore.Logout()); + this.notificationService.info( + 'Session Expired', + 'Please login again', ); - return final; - }), - catchError((_error) => { - console.error('Failed to get user info'); - return of(null); - }), - take(1), + }, + user.exp * 1000 - Date.now(), ); + + return this.makeUser(user['urs-user-id'], user['urs-groups'], token); + } catch (_error) { + return this.nullUser(); + } } private makeUser( id: string, groups: models.URSGroup[], token: string, - exp: number, ): models.UserAuth { - return { id, token, groups, exp }; + return { id, token, groups }; + } + + private nullUser(): models.UserAuth { + return { id: null, token: null, groups: [] }; + } + + private isExpired(userToken): boolean { + return Date.now() > userToken.exp * 1000; + } + + private loadCookies() { + return document.cookie + .split(';') + .map((s) => s.trim().split('=')) + .map(([name, val]) => ({ [name]: val })) + .reduce((allCookies, cookie) => ({ ...allCookies, ...cookie })); } } diff --git a/src/app/services/env.ts b/src/app/services/env.ts index 0993bb406..d67a2e094 100644 --- a/src/app/services/env.ts +++ b/src/app/services/env.ts @@ -12,11 +12,11 @@ export const env = { test: { api: 'https://api-test.asf.alaska.edu', api_maturity: 'prod', - auth: 'https://cumulus.asf.alaska.edu', + auth: 'https://auth.asf.alaska.edu', urs: 'https://urs.earthdata.nasa.gov', urs_client_id: 'BO_n7nTIlMljdvU6kRRB3g', banner: 'https://banners.asf.alaska.edu', - user_data: 'https://appdata-test.asf.alaska.edu', + user_data: 'https://cgdjuem3wc.execute-api.us-east-1.amazonaws.com/prod/', unzip: 'https://unzip.asf.alaska.edu', bulk_download: 'https://bulk-download.asf.alaska.edu', }, diff --git a/src/app/services/environment.service.ts b/src/app/services/environment.service.ts index af18b5051..7ad0f27fc 100644 --- a/src/app/services/environment.service.ts +++ b/src/app/services/environment.service.ts @@ -18,7 +18,7 @@ export interface Environment { api_maturity?: string; urs_client_id: string; unzip: string; - datapool?: string; + datapool: string; banner: string; user_data: string; bulk_download: string; diff --git a/src/app/services/envs/env-devel.ts b/src/app/services/envs/env-devel.ts index fe38a6043..d67a2e094 100644 --- a/src/app/services/envs/env-devel.ts +++ b/src/app/services/envs/env-devel.ts @@ -16,7 +16,7 @@ export const env = { urs: 'https://urs.earthdata.nasa.gov', urs_client_id: 'BO_n7nTIlMljdvU6kRRB3g', banner: 'https://banners.asf.alaska.edu', - user_data: 'https://appdata-test.asf.alaska.edu', + user_data: 'https://cgdjuem3wc.execute-api.us-east-1.amazonaws.com/prod/', unzip: 'https://unzip.asf.alaska.edu', bulk_download: 'https://bulk-download.asf.alaska.edu', }, diff --git a/src/app/services/envs/env-prod.ts b/src/app/services/envs/env-prod.ts index 12773835e..870283201 100644 --- a/src/app/services/envs/env-prod.ts +++ b/src/app/services/envs/env-prod.ts @@ -12,11 +12,11 @@ export const env = { test: { api: 'https://api-test.asf.alaska.edu', api_maturity: 'test', - auth: 'https://cumulus.asf.alaska.edu', + auth: 'https://auth.asf.alaska.edu', urs: 'https://urs.earthdata.nasa.gov', urs_client_id: 'BO_n7nTIlMljdvU6kRRB3g', banner: 'https://banners.asf.alaska.edu', - user_data: 'https://appdata-test.asf.alaska.edu', + user_data: 'https://cgdjuem3wc.execute-api.us-east-1.amazonaws.com/prod/', unzip: 'https://unzip.asf.alaska.edu', bulk_download: 'https://bulk-download.asf.alaska.edu', }, diff --git a/src/app/services/envs/env-test.ts b/src/app/services/envs/env-test.ts index 0993bb406..d67a2e094 100644 --- a/src/app/services/envs/env-test.ts +++ b/src/app/services/envs/env-test.ts @@ -12,11 +12,11 @@ export const env = { test: { api: 'https://api-test.asf.alaska.edu', api_maturity: 'prod', - auth: 'https://cumulus.asf.alaska.edu', + auth: 'https://auth.asf.alaska.edu', urs: 'https://urs.earthdata.nasa.gov', urs_client_id: 'BO_n7nTIlMljdvU6kRRB3g', banner: 'https://banners.asf.alaska.edu', - user_data: 'https://appdata-test.asf.alaska.edu', + user_data: 'https://cgdjuem3wc.execute-api.us-east-1.amazonaws.com/prod/', unzip: 'https://unzip.asf.alaska.edu', bulk_download: 'https://bulk-download.asf.alaska.edu', }, diff --git a/src/app/services/user-data.service.ts b/src/app/services/user-data.service.ts index 117ade3aa..437ae40d7 100644 --- a/src/app/services/user-data.service.ts +++ b/src/app/services/user-data.service.ts @@ -1,5 +1,5 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -7,15 +7,6 @@ import { EnvironmentService } from './environment.service'; import { UserAuth } from '@models'; import { NotificationService } from './notification.service'; -interface UserInfo { - uid: string; - first_name: string; - last_name: string; - country: string; - email_address: string; - organization: string; -} - @Injectable({ providedIn: 'root', }) @@ -26,11 +17,13 @@ export class UserDataService { private baseUrl = this.getBaseUrlFrom(); - public getUserInfo$(_userAuth: UserAuth): Observable { - const url = this.getUserInfoURL(this.baseUrl); + public getUserInfo$(userAuth: UserAuth): Observable { + const url = this.getUserInfoURL(this.baseUrl, userAuth.id); + const headers = this.makeAuthHeader(userAuth.token); + return this.http - .get(url, { - withCredentials: true, + .get(url, { + headers, }) .pipe( catchError((error) => { @@ -55,11 +48,12 @@ export class UserDataService { userAuth: UserAuth, attribute: string, ): Observable { - const url = this.makeEndpoint(this.baseUrl, userAuth?.id, attribute); + const url = this.makeEndpoint(this.baseUrl, userAuth.id, attribute); + const headers = this.makeAuthHeader(userAuth.token); return this.http .get(url, { - withCredentials: true, + headers, }) .pipe( catchError((error) => { @@ -85,11 +79,12 @@ export class UserDataService { attribute: string, value: T, ): Observable { - const url = this.makeEndpoint(this.baseUrl, userAuth?.id, attribute); + const url = this.makeEndpoint(this.baseUrl, userAuth.id, attribute); + const headers = this.makeAuthHeader(userAuth.token); return this.http .post(url, value, { - withCredentials: true, + headers, }) .pipe( catchError((_) => { @@ -114,10 +109,14 @@ export class UserDataService { return `${baseUrl}/vertex/${userId}/${attributeName}`; } + private makeAuthHeader(token: string): HttpHeaders { + return new HttpHeaders().set('Authorization', `Bearer ${token}`); + } + private getBaseUrlFrom(): string { return this.env.currentEnv.user_data; } - public getUserInfoURL(baseUrl: string): string { - return `${baseUrl}/info/`; + public getUserInfoURL(baseUrl: string, userId: string): string { + return `${baseUrl}/vertex/${userId}/`; } } diff --git a/src/app/store/user/user.effect.ts b/src/app/store/user/user.effect.ts index 3e43ec988..531694f16 100644 --- a/src/app/store/user/user.effect.ts +++ b/src/app/store/user/user.effect.ts @@ -147,7 +147,7 @@ export class UserEffects { this.store$.select(userReducer.getSearchHistory), ]), ), - filter(([_, [userAuth, _searches]]) => userAuth?.id !== null), + filter(([_, [userAuth, _searches]]) => userAuth.id !== null), switchMap(([_, [userAuth, searches]]) => this.userDataService.setAttribute$(userAuth, 'History', searches), ), diff --git a/src/app/store/user/user.reducer.ts b/src/app/store/user/user.reducer.ts index ad536de61..c06dfe545 100644 --- a/src/app/store/user/user.reducer.ts +++ b/src/app/store/user/user.reducer.ts @@ -2,6 +2,7 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; import { UserActionType, UserActions } from './user.action'; import * as models from '@models'; +import jwt_decode from 'jwt-decode'; /* State */ export interface UserState { @@ -19,7 +20,6 @@ export const initState: UserState = { id: null, token: null, groups: [], - exp: null, }, profile: { defaultDataset: 'SENTINEL-1', @@ -62,7 +62,6 @@ export function userReducer(state = initState, action: UserActions): UserState { id: null, token: null, groups: [], - exp: null, }, }; } @@ -264,7 +263,11 @@ export const getIsUserLoggedIn = createSelector( export const getUserEDLToken = createSelector( getUserState, (state: UserState) => { - return state.auth.token ?? null; + if (state.auth.token) { + const decoded = jwt_decode(state.auth.token); + return decoded['urs-access-token'] ?? null; + } + return null; }, );