mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-09-29 14:22:18 +00:00
WIP
This commit is contained in:
@@ -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<void>;
|
||||
}
|
||||
|
||||
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():
|
||||
|
||||
@@ -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<SCIdCard[]> {
|
||||
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<SCIdCard[]>('id-cards') as Promise<SCIdCard[]>,
|
||||
).pipe(filter(it => it !== undefined));
|
||||
this.encryptedStorageProvider.get<SCIdCard[]>(
|
||||
'id-cards',
|
||||
) as Promise<SCIdCard[]>,
|
||||
).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<SCIdCard[]> {
|
||||
private fetchIdCards(
|
||||
url: string,
|
||||
token: string,
|
||||
): Observable<SCIdCard[]> {
|
||||
console.log(
|
||||
'[IdCardsProvider] fetching ID cards from:',
|
||||
url,
|
||||
);
|
||||
|
||||
return this.httpClient
|
||||
.get<SCIdCard[]>(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<SCIdCard[]> {
|
||||
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<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(),
|
||||
},
|
||||
},
|
||||
]),
|
||||
private fetchFallbackIdCards(
|
||||
user: SCUserConfiguration,
|
||||
): Observable<SCIdCard[]> {
|
||||
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(),
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user