diff --git a/angular/src/app/app-routing.module.ts b/angular/src/app/app-routing.module.ts index 9e48abbd..7ea77ffc 100644 --- a/angular/src/app/app-routing.module.ts +++ b/angular/src/app/app-routing.module.ts @@ -46,7 +46,7 @@ const routes: Routes = [ }, ], }, - { path: 'callback', component: CallbackComponent }, + { path: 'handle-login', component: CallbackComponent }, { path: 'streetview/callback', component: StreetviewCallbackComponent }, { path: '404', component: NotFoundComponent }, { path: '**', redirectTo: '', pathMatch: 'full' }, diff --git a/angular/src/app/app.interceptors.ts b/angular/src/app/app.interceptors.ts index 85989f18..4401366c 100644 --- a/angular/src/app/app.interceptors.ts +++ b/angular/src/app/app.interceptors.ts @@ -1,34 +1,41 @@ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './services/authentication.service'; import { EnvService } from './services/env.service'; -import { catchError } from 'rxjs/operators'; import { StreetviewAuthenticationService } from './services/streetview-authentication.service'; -import { NotificationsService } from './services/notifications.service'; import { v4 as uuidv4 } from 'uuid'; +import { LOGIN } from './constants/routes'; @Injectable() export class JwtInterceptor implements HttpInterceptor { constructor( + private router: Router, private authSvc: AuthService, private envService: EnvService, private streetviewAuthService: StreetviewAuthenticationService ) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - // TODO_TAPISV3 put the tapis url in envService const isTargetUrl = request.url.includes(this.envService.tapisUrl) || request.url.includes(this.envService.apiUrl) || request.url.includes(this.envService.designSafeUrl); - if (isTargetUrl && this.authSvc.isLoggedIn()) { - // add tapis token to Geoapi or Tapis requests - request = request.clone({ - setHeaders: { - 'X-Tapis-Token': this.authSvc.userToken.token, - }, - }); + if (isTargetUrl) { + if (this.authSvc.isLoggedInButTokenExpired()) { + // check for an expired user token and get user to relogin if expired + this.router.navigateByUrl(LOGIN + '?to=' + encodeURIComponent(this.router.url)); + } + + if (this.authSvc.isLoggedIn()) { + // add tapis token to Geoapi or Tapis requests + request = request.clone({ + setHeaders: { + 'X-Tapis-Token': this.authSvc.userToken.token, + }, + }); + } } if (request.url.indexOf(this.envService.apiUrl) > -1) { @@ -99,28 +106,3 @@ export class JwtInterceptor implements HttpInterceptor { return request.method === 'GET' && urlPattern.test(request.url); } } - -@Injectable() -export class AuthInterceptor implements HttpInterceptor { - constructor( - private authService: AuthService, - private envService: EnvService, - private streetviewAuthService: StreetviewAuthenticationService, - private notificationService: NotificationsService - ) {} - - intercept(request: HttpRequest, next: HttpHandler): Observable> { - return next.handle(request).pipe( - catchError((err) => { - if (err.status === 401) { - // auto logout if 401 response returned from api - // https://jira.tacc.utexas.edu/browse/DES-1999 - // TODO_TAPISV3 renable these - // this.authService.logout(); - // location.reload(); - } - throw err; - }) - ); - } -} diff --git a/angular/src/app/app.module.ts b/angular/src/app/app.module.ts index a3571191..7b7d6ab8 100644 --- a/angular/src/app/app.module.ts +++ b/angular/src/app/app.module.ts @@ -29,7 +29,7 @@ import { AuthService } from './services/authentication.service'; import { ModalService } from './services/modal.service'; import { EnvService } from './services/env.service'; import { CallbackComponent } from './components/callback/callback.component'; -import { AuthInterceptor, JwtInterceptor } from './app.interceptors'; +import { JwtInterceptor } from './app.interceptors'; import { ModalCreateProjectComponent } from './components/modal-create-project/modal-create-project.component'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { ModalFileBrowserComponent } from './components/modal-file-browser/modal-file-browser.component'; @@ -154,11 +154,6 @@ import { QuestionnaireDetailComponent } from './components/questionnaire-detail/ useClass: JwtInterceptor, multi: true, }, - { - provide: HTTP_INTERCEPTORS, - multi: true, - useClass: AuthInterceptor, - }, { provide: CDK_DRAG_CONFIG, useValue: { diff --git a/angular/src/app/components/callback/callback.component.ts b/angular/src/app/components/callback/callback.component.ts index 02cb46c7..f2dc8623 100644 --- a/angular/src/app/components/callback/callback.component.ts +++ b/angular/src/app/components/callback/callback.component.ts @@ -11,8 +11,6 @@ export class CallbackComponent implements OnInit { constructor(private route: ActivatedRoute, private auth: AuthService) {} ngOnInit() { - const frag = this.route.snapshot.fragment; - const params = new URLSearchParams(frag); const token = this.route.snapshot.queryParams.access_token; const expires_in = this.route.snapshot.queryParams.expires_in; this.auth.setToken(token, expires_in); diff --git a/angular/src/app/components/file-browser/file-browser.component.ts b/angular/src/app/components/file-browser/file-browser.component.ts index f496f4a1..8e4c5be5 100644 --- a/angular/src/app/components/file-browser/file-browser.component.ts +++ b/angular/src/app/components/file-browser/file-browser.component.ts @@ -120,17 +120,19 @@ export class FileBrowserComponent implements OnInit { this.currentPath.next(this.currentDirectory.path); - // Add '..' entry for users to move to parent path - const backPath = { - name: '..', - format: 'folder', - type: 'dir', - mimeType: 'test/directory', - size: 8192, - path: this.tapisFilesService.getParentPath(this.currentDirectory.path), - system: this.currentDirectory.system, - }; - files.unshift(backPath); + // If this is the first load, add the '..' entry for users to move to parent path + if (this.offset === 0) { + const backPath = { + name: '..', + format: 'folder', + type: 'dir', + mimeType: 'test/directory', + size: 8192, + path: this.tapisFilesService.getParentPath(this.currentDirectory.path), + system: this.currentDirectory.system, + }; + this.filesList.unshift(backPath); + } this.inProgress = false; this.filesList = this.filesList.concat(files); diff --git a/angular/src/app/models/models.ts b/angular/src/app/models/models.ts index c9b58f7e..972ecdf4 100644 --- a/angular/src/app/models/models.ts +++ b/angular/src/app/models/models.ts @@ -93,19 +93,25 @@ export class AuthToken { this.expires = new Date(expires); } + /** Creates an AuthToken instance from a token and an expiration time in seconds. */ static fromExpiresIn(token: string, expires_in: number) { const expires = new Date(new Date().getTime() + expires_in * 1000); return new AuthToken(token, expires); } /** - * Checks if the token is expired or not + * Checks if the token is expired or not. + * A 5 minute buffer is used to consider a token as expired slightly before its actual expiration time. + * @returns True if the token is expired, false otherwise. */ public isExpired(): boolean { + const buffer = 300000; // 5 minutes in milliseconds if (this.expires) { - return new Date().getTime() > this.expires.getTime(); + // Subtract buffer from the expiration time and compare with the current time + return new Date().getTime() > this.expires.getTime() - buffer; } else { - return false; + // If expires is not set, consider the token as not expired + return false; // TODO_V3 this affects the streetview token; should be confirmed or refactored. } } } diff --git a/angular/src/app/services/authentication.service.ts b/angular/src/app/services/authentication.service.ts index 01161f2f..1d0d7101 100644 --- a/angular/src/app/services/authentication.service.ts +++ b/angular/src/app/services/authentication.service.ts @@ -23,7 +23,7 @@ export class AuthService { constructor(private http: HttpClient, private envService: EnvService, private router: Router) {} public getTokenKeyword() { - return `${this.envService.env}HazmapperToken`; + return `${this.envService.env}HazmapperV3Token`; } public getRedirectKeyword() { @@ -31,31 +31,14 @@ export class AuthService { } public login(requestedUrl: string) { + this.logout(); localStorage.setItem(this.getRedirectKeyword(), requestedUrl); - - // First, check if the user has a token in localStorage - const tokenStr = localStorage.getItem(this.getTokenKeyword()); - if (!tokenStr) { - this.redirectToAuthenticator(); - } else { - const token = JSON.parse(tokenStr); - this.userToken = new AuthToken(token.token, new Date(token.expires)); - if (!this.userToken || this.userToken.isExpired()) { - this.logout(); - this.redirectToAuthenticator(); - } - this.getUserInfoFromToken(); - } + this.redirectToAuthenticator(requestedUrl); } - public redirectToAuthenticator() { - const client_id = this.envService.clientId; - const callback = location.origin + this.envService.baseHref + 'callback'; - const state = Math.random().toString(36); - // tslint:disable-next-line:max-line-length - const AUTH_URL_V3 = `${this.envService.tapisUrl}/v3/oauth2/authorize?client_id=${client_id}&response_type=token&redirect_uri=${callback}`; - - window.location.href = AUTH_URL_V3; + public redirectToAuthenticator(requestedUrl: string) { + const GEOAPI_AUTH_URL = `${this.envService.apiUrl}/auth/login?to=${requestedUrl}`; + window.location.href = GEOAPI_AUTH_URL; } /** @@ -71,6 +54,19 @@ export class AuthService { return false; } + /** + * Checks to see if there is a logged in user but token is expired; + */ + public isLoggedInButTokenExpired(): boolean { + const tokenStr = localStorage.getItem(this.getTokenKeyword()); + if (tokenStr) { + const token = JSON.parse(tokenStr); + this.userToken = new AuthToken(token.token, new Date(token.expires)); + return this.userToken && this.userToken.isExpired(); + } + return false; + } + public logout(): void { this.userToken = null; localStorage.removeItem(this.getTokenKeyword()); @@ -93,11 +89,4 @@ export class AuthService { const u = new AuthenticatedUser(decodedJwt['tapis/username']); this._currentUser.next(u); } - - checkLoggedIn(): void { - if (!this.isLoggedIn()) { - this.logout(); - this.redirectToAuthenticator(); - } - } } diff --git a/angular/src/app/services/notifications.service.ts b/angular/src/app/services/notifications.service.ts index df0e84e8..94a3c366 100644 --- a/angular/src/app/services/notifications.service.ts +++ b/angular/src/app/services/notifications.service.ts @@ -29,7 +29,6 @@ export class NotificationsService { } getRecent(): void { - this.authService.checkLoggedIn(); const baseUrl = this.envService.apiUrl + '/notifications/'; const now = new Date(); const then = new Date(now.getTime() - this.TIMEOUT); @@ -75,7 +74,6 @@ export class NotificationsService { } getRecentProgress(): void { - this.authService.checkLoggedIn(); const baseUrl = this.envService.apiUrl + '/notifications/progress'; this.http.get>(baseUrl).subscribe((notes) => { this._progressNotifications.next(notes);