From c756eaa29e01ee5bf8051e7ffe83cceb7d998aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jovan=20Kruni=C4=87?= Date: Thu, 17 Sep 2026 17:38:20 +0200 Subject: [PATCH] WIP --- .../app/src/app/modules/auth/auth.service.ts | 57 +++- .../app/modules/profile/id-cards.provider.ts | 258 +++++++++++++++--- .../profile/page/profile-page.component.ts | 17 +- 3 files changed, 270 insertions(+), 62 deletions(-) diff --git a/frontend/app/src/app/modules/auth/auth.service.ts b/frontend/app/src/app/modules/auth/auth.service.ts index 22c18ef2..1544ce6d 100644 --- a/frontend/app/src/app/modules/auth/auth.service.ts +++ b/frontend/app/src/app/modules/auth/auth.service.ts @@ -24,9 +24,11 @@ import { AuthorizationResponse, AuthorizationServiceConfiguration, BaseTokenRequestHandler, + BasicQueryStringUtils, DefaultCrypto, GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN, + LocationLike, RedirectRequestHandler, Requestor, RevokeTokenRequest, @@ -53,7 +55,7 @@ import {requestorFactory} from './factories/requestor.factory'; import {storageFactory} from './factories/storage.factory'; const TOKEN_RESPONSE_KEY = 'token_response'; -const AUTH_EXPIRY_BUFFER = -10 * 60; +const AUTH_EXPIRY_BUFFER = -5 * 60; export interface AuthConfig { server_host: string; @@ -120,6 +122,18 @@ interface AuthorizationHandler { Promise; } +class NoHashQueryStringUtils extends BasicQueryStringUtils { + override parse( + input: LocationLike, + _useHash?: boolean, + ): StringMap { + return super.parse( + input, + false, + ); + } +} + @Injectable({ providedIn: 'root', }) @@ -183,6 +197,7 @@ export class AuthService { ) : new RedirectRequestHandler( this.storage, + new NoHashQueryStringUtils(), ); this.tokenHandler = @@ -524,32 +539,48 @@ export class AuthService { const currentToken = this.tokenSubject.value; - if ( - currentToken?.isValid(buffer) - ) { + /* + * Access token still valid for longer than the configured + * expiry buffer. + */ + if (currentToken?.isValid(buffer)) { return currentToken; } + /* + * Access token is expired or will expire soon. + * We can only recover automatically if a refresh token exists. + */ if (!currentToken?.refreshToken) { throw new Error( - 'Unable To Obtain Valid Token', + 'Unable To Obtain Valid Token: No Refresh Token Available', ); } - await this.refreshToken(); + /* + * Refresh immediately. + * + * requestTokenRefresh() stores the newly returned token + * and updates tokenSubject via RefreshSuccess. + */ + await this.requestTokenRefresh(); const refreshedToken = this.tokenSubject.value; - if ( - refreshedToken?.isValid(buffer) - ) { - return refreshedToken; + if (!refreshedToken) { + throw new Error( + 'Unable To Obtain Valid Token: Refresh Returned No Token', + ); } - throw new Error( - 'Unable To Obtain Valid Token', - ); + if (!refreshedToken.isValid(buffer)) { + throw new Error( + 'Unable To Obtain Valid Token: Refreshed Token Is Already Too Close To Expiry', + ); + } + + return refreshedToken; } private setupAuthorizationNotifier(): diff --git a/frontend/app/src/app/modules/profile/id-cards.provider.ts b/frontend/app/src/app/modules/profile/id-cards.provider.ts index b3723ce0..46a4660d 100644 --- a/frontend/app/src/app/modules/profile/id-cards.provider.ts +++ b/frontend/app/src/app/modules/profile/id-cards.provider.ts @@ -1,12 +1,25 @@ import {Injectable} from '@angular/core'; -import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core'; +import { + SCIdCard, + SCThingOriginType, + SCThingType, + SCUserConfiguration, +} from '@openstapps/core'; import {from, of, Observable} from 'rxjs'; import {AuthHelperService} from '../auth/auth-helper.service'; -import {mergeMap, concatWith, filter, map, startWith, catchError, tap} from 'rxjs/operators'; +import { + mergeMap, + concatWith, + filter, + map, + startWith, + catchError, + tap, +} from 'rxjs/operators'; import {ConfigProvider} from '../config/config.provider'; import {HttpClient} from '@angular/common/http'; import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider'; -import {AuthService} from "../auth/auth.service"; +import {AuthService} from '../auth/auth.service'; @Injectable({providedIn: 'root'}) export class IdCardsProvider { @@ -19,35 +32,136 @@ export class IdCardsProvider { ) {} getIdCards(): Observable { - const feature = this.config.config.app.features.extern?.['idCards']; + const feature = + this.config.config.app.features.extern?.['idCards']; + + console.log( + '[IdCardsProvider] idCards feature:', + feature, + ); + const storedIdCards = from( - this.encryptedStorageProvider.get('id-cards') as Promise, - ).pipe(filter(it => it !== undefined)); + this.encryptedStorageProvider.get( + 'id-cards', + ) as Promise, + ).pipe( + tap(idCards => { + console.log( + '[IdCardsProvider] stored ID cards:', + idCards, + ); + }), + filter(it => it !== undefined), + ); return this.authService.isLoggedIn$.pipe( + tap(isLoggedIn => { + console.log( + '[IdCardsProvider] isLoggedIn:', + isLoggedIn, + ); + }), + mergeMap(isLoggedIn => isLoggedIn ? feature ? storedIdCards.pipe( - concatWith( - from(this.authService.getValidToken()).pipe( - mergeMap(token => this.fetchIdCards(feature.url, token.accessToken)), - catchError(() => storedIdCards), + concatWith( + from( + this.authService.getValidToken(), + ).pipe( + tap(() => { + console.log( + '[IdCardsProvider] valid token available', + ); + }), + + mergeMap(token => + this.fetchIdCards( + feature.url, + token.accessToken, + ), ), + + catchError(error => { + console.error( + '[IdCardsProvider] ID card request failed:', + error, + ); + + return storedIdCards; + }), ), - ) + ), + ) : this.authService.user$.pipe( - filter(user => user !== undefined), - map(userInfo => this.authHelper.getUserFromUserInfo(userInfo as object)), - mergeMap(user => this.fetchFallbackIdCards(user)), - startWith([]), - ) - : of([]).pipe(tap({next: () => this.encryptedStorageProvider.delete('id-cards')})), + tap(user => { + console.log( + '[IdCardsProvider] user:', + user, + ); + }), + + filter( + user => + user !== undefined, + ), + + map(userInfo => { + const user = + this.authHelper.getUserFromUserInfo( + userInfo as object, + ); + + console.log( + '[IdCardsProvider] mapped user:', + user, + ); + + return user; + }), + + mergeMap(user => + this.fetchFallbackIdCards( + user, + ), + ), + + startWith([]), + ) + : of([]).pipe( + tap({ + next: () => { + console.log( + '[IdCardsProvider] not logged in - deleting stored ID cards', + ); + + void this.encryptedStorageProvider.delete( + 'id-cards', + ); + }, + }), + ), ), + + tap(idCards => { + console.log( + '[IdCardsProvider] resulting ID cards:', + idCards, + ); + }), ); } - private fetchIdCards(url: string, token: string): Observable { + private fetchIdCards( + url: string, + token: string, + ): Observable { + console.log( + '[IdCardsProvider] fetching ID cards from:', + url, + ); + return this.httpClient .get(url, { headers: { @@ -55,31 +169,93 @@ export class IdCardsProvider { }, responseType: 'json', }) - .pipe(tap({next: idCards => this.encryptedStorageProvider.set('id-cards', idCards)})); + .pipe( + tap({ + next: idCards => { + console.log( + '[IdCardsProvider] fetched ID cards:', + idCards, + ); + + void this.encryptedStorageProvider.set( + 'id-cards', + idCards, + ); + }, + + error: error => { + console.error( + '[IdCardsProvider] fetchIdCards failed:', + error, + ); + }, + }), + ); } - private fetchFallbackIdCards(user: SCUserConfiguration): Observable { - return this.httpClient.get('/assets/examples/student-id.sample.svg', {responseType: 'text'}).pipe( - map(svg => { - let result = svg; - for (const key in user) { - result = result.replaceAll(`{{${key}}}`, (user as unknown as Record)[key]); - } - return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`; - }), - map(image => [ - { - name: 'Student ID', - image, - type: SCThingType.IdCard, - uid: '1234', - origin: { - name: 'Sample Origin', - type: SCThingOriginType.Remote, - indexed: new Date().toISOString(), - }, - }, - ]), + private fetchFallbackIdCards( + user: SCUserConfiguration, + ): Observable { + console.log( + '[IdCardsProvider] creating fallback ID card for user:', + user, ); + + return this.httpClient + .get( + '/assets/examples/student-id.sample.svg', + { + responseType: 'text', + }, + ) + .pipe( + tap({ + next: () => { + console.log( + '[IdCardsProvider] fallback SVG loaded', + ); + }, + + error: error => { + console.error( + '[IdCardsProvider] loading fallback SVG failed:', + error, + ); + }, + }), + + map(svg => { + let result = svg; + + for (const key in user) { + result = result.replaceAll( + `{{${key}}}`, + ( + user as unknown as Record< + string, + string + > + )[key], + ); + } + + return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`; + }), + + map(image => [ + { + name: 'Student ID', + image, + type: SCThingType.IdCard, + uid: '1234', + origin: { + name: 'Sample Origin', + type: SCThingOriginType.Remote, + indexed: + new Date().toISOString(), + }, + }, + ]), + ); } } diff --git a/frontend/app/src/app/modules/profile/page/profile-page.component.ts b/frontend/app/src/app/modules/profile/page/profile-page.component.ts index 6dc1477c..d8acb09d 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page.component.ts @@ -17,7 +17,7 @@ import {AuthHelperService} from '../../auth/auth-helper.service'; import {ActivatedRoute} from '@angular/router'; import {ScheduleProvider} from '../../calendar/schedule.provider'; import {profilePageSections} from '../../../../config/profile-page-sections'; -import {AuthService} from "../../auth/auth.service"; +import {AuthService} from '../../auth/auth.service'; @Component({ selector: 'app-home', @@ -36,7 +36,13 @@ export class ProfilePageComponent { async signIn() { const originPath = this.activatedRoute.snapshot.queryParamMap.get('origin_path'); - await (originPath ? this.authHelper.setOriginPath(originPath) : this.authHelper.deleteOriginPath()); + + await ( + originPath + ? this.authHelper.setOriginPath(originPath) + : this.authHelper.deleteOriginPath() + ); + await this.authService.signIn(); } @@ -45,11 +51,6 @@ export class ProfilePageComponent { } ionViewWillEnter() { - this.authService - .getValidToken() - .then(() => void this.authService.loadUserInfo()) - .catch(() => { - // noop - }); + void this.authService.loadUserInfo(); } }