mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-10-03 16:22:05 +00:00
WIP
This commit is contained in:
@@ -24,9 +24,11 @@ import {
|
|||||||
AuthorizationResponse,
|
AuthorizationResponse,
|
||||||
AuthorizationServiceConfiguration,
|
AuthorizationServiceConfiguration,
|
||||||
BaseTokenRequestHandler,
|
BaseTokenRequestHandler,
|
||||||
|
BasicQueryStringUtils,
|
||||||
DefaultCrypto,
|
DefaultCrypto,
|
||||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||||
GRANT_TYPE_REFRESH_TOKEN,
|
GRANT_TYPE_REFRESH_TOKEN,
|
||||||
|
LocationLike,
|
||||||
RedirectRequestHandler,
|
RedirectRequestHandler,
|
||||||
Requestor,
|
Requestor,
|
||||||
RevokeTokenRequest,
|
RevokeTokenRequest,
|
||||||
@@ -53,7 +55,7 @@ import {requestorFactory} from './factories/requestor.factory';
|
|||||||
import {storageFactory} from './factories/storage.factory';
|
import {storageFactory} from './factories/storage.factory';
|
||||||
|
|
||||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||||
const AUTH_EXPIRY_BUFFER = -10 * 60;
|
const AUTH_EXPIRY_BUFFER = -5 * 60;
|
||||||
|
|
||||||
export interface AuthConfig {
|
export interface AuthConfig {
|
||||||
server_host: string;
|
server_host: string;
|
||||||
@@ -120,6 +122,18 @@ interface AuthorizationHandler {
|
|||||||
Promise<void>;
|
Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NoHashQueryStringUtils extends BasicQueryStringUtils {
|
||||||
|
override parse(
|
||||||
|
input: LocationLike,
|
||||||
|
_useHash?: boolean,
|
||||||
|
): StringMap {
|
||||||
|
return super.parse(
|
||||||
|
input,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -183,6 +197,7 @@ export class AuthService {
|
|||||||
)
|
)
|
||||||
: new RedirectRequestHandler(
|
: new RedirectRequestHandler(
|
||||||
this.storage,
|
this.storage,
|
||||||
|
new NoHashQueryStringUtils(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.tokenHandler =
|
this.tokenHandler =
|
||||||
@@ -524,34 +539,50 @@ export class AuthService {
|
|||||||
const currentToken =
|
const currentToken =
|
||||||
this.tokenSubject.value;
|
this.tokenSubject.value;
|
||||||
|
|
||||||
if (
|
/*
|
||||||
currentToken?.isValid(buffer)
|
* Access token still valid for longer than the configured
|
||||||
) {
|
* expiry buffer.
|
||||||
|
*/
|
||||||
|
if (currentToken?.isValid(buffer)) {
|
||||||
return currentToken;
|
return currentToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Access token is expired or will expire soon.
|
||||||
|
* We can only recover automatically if a refresh token exists.
|
||||||
|
*/
|
||||||
if (!currentToken?.refreshToken) {
|
if (!currentToken?.refreshToken) {
|
||||||
throw new Error(
|
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 =
|
const refreshedToken =
|
||||||
this.tokenSubject.value;
|
this.tokenSubject.value;
|
||||||
|
|
||||||
if (
|
if (!refreshedToken) {
|
||||||
refreshedToken?.isValid(buffer)
|
throw new Error(
|
||||||
) {
|
'Unable To Obtain Valid Token: Refresh Returned No Token',
|
||||||
return refreshedToken;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!refreshedToken.isValid(buffer)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Unable To Obtain Valid Token',
|
'Unable To Obtain Valid Token: Refreshed Token Is Already Too Close To Expiry',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return refreshedToken;
|
||||||
|
}
|
||||||
|
|
||||||
private setupAuthorizationNotifier():
|
private setupAuthorizationNotifier():
|
||||||
void {
|
void {
|
||||||
if (this.notifierInitialized) {
|
if (this.notifierInitialized) {
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import {Injectable} from '@angular/core';
|
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 {from, of, Observable} from 'rxjs';
|
||||||
import {AuthHelperService} from '../auth/auth-helper.service';
|
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 {ConfigProvider} from '../config/config.provider';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
||||||
import {AuthService} from "../auth/auth.service";
|
import {AuthService} from '../auth/auth.service';
|
||||||
|
|
||||||
@Injectable({providedIn: 'root'})
|
@Injectable({providedIn: 'root'})
|
||||||
export class IdCardsProvider {
|
export class IdCardsProvider {
|
||||||
@@ -19,35 +32,136 @@ export class IdCardsProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
getIdCards(): Observable<SCIdCard[]> {
|
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(
|
const storedIdCards = from(
|
||||||
this.encryptedStorageProvider.get<SCIdCard[]>('id-cards') as Promise<SCIdCard[]>,
|
this.encryptedStorageProvider.get<SCIdCard[]>(
|
||||||
).pipe(filter(it => it !== undefined));
|
'id-cards',
|
||||||
|
) as Promise<SCIdCard[]>,
|
||||||
|
).pipe(
|
||||||
|
tap(idCards => {
|
||||||
|
console.log(
|
||||||
|
'[IdCardsProvider] stored ID cards:',
|
||||||
|
idCards,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
filter(it => it !== undefined),
|
||||||
|
);
|
||||||
|
|
||||||
return this.authService.isLoggedIn$.pipe(
|
return this.authService.isLoggedIn$.pipe(
|
||||||
|
tap(isLoggedIn => {
|
||||||
|
console.log(
|
||||||
|
'[IdCardsProvider] isLoggedIn:',
|
||||||
|
isLoggedIn,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
mergeMap(isLoggedIn =>
|
mergeMap(isLoggedIn =>
|
||||||
isLoggedIn
|
isLoggedIn
|
||||||
? feature
|
? feature
|
||||||
? storedIdCards.pipe(
|
? storedIdCards.pipe(
|
||||||
concatWith(
|
concatWith(
|
||||||
from(this.authService.getValidToken()).pipe(
|
from(
|
||||||
mergeMap(token => this.fetchIdCards(feature.url, token.accessToken)),
|
this.authService.getValidToken(),
|
||||||
catchError(() => storedIdCards),
|
).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(
|
: this.authService.user$.pipe(
|
||||||
filter(user => user !== undefined),
|
tap(user => {
|
||||||
map(userInfo => this.authHelper.getUserFromUserInfo(userInfo as object)),
|
console.log(
|
||||||
mergeMap(user => this.fetchFallbackIdCards(user)),
|
'[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([]),
|
startWith([]),
|
||||||
)
|
)
|
||||||
: of([]).pipe(tap({next: () => this.encryptedStorageProvider.delete('id-cards')})),
|
: 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
|
return this.httpClient
|
||||||
.get<SCIdCard[]>(url, {
|
.get<SCIdCard[]>(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -55,18 +169,79 @@ export class IdCardsProvider {
|
|||||||
},
|
},
|
||||||
responseType: 'json',
|
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[]> {
|
private fetchFallbackIdCards(
|
||||||
return this.httpClient.get('/assets/examples/student-id.sample.svg', {responseType: 'text'}).pipe(
|
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 => {
|
map(svg => {
|
||||||
let result = svg;
|
let result = svg;
|
||||||
|
|
||||||
for (const key in user) {
|
for (const key in user) {
|
||||||
result = result.replaceAll(`{{${key}}}`, (user as unknown as Record<string, string>)[key]);
|
result = result.replaceAll(
|
||||||
|
`{{${key}}}`,
|
||||||
|
(
|
||||||
|
user as unknown as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>
|
||||||
|
)[key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`;
|
return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
map(image => [
|
map(image => [
|
||||||
{
|
{
|
||||||
name: 'Student ID',
|
name: 'Student ID',
|
||||||
@@ -76,7 +251,8 @@ export class IdCardsProvider {
|
|||||||
origin: {
|
origin: {
|
||||||
name: 'Sample Origin',
|
name: 'Sample Origin',
|
||||||
type: SCThingOriginType.Remote,
|
type: SCThingOriginType.Remote,
|
||||||
indexed: new Date().toISOString(),
|
indexed:
|
||||||
|
new Date().toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {AuthHelperService} from '../../auth/auth-helper.service';
|
|||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {ScheduleProvider} from '../../calendar/schedule.provider';
|
import {ScheduleProvider} from '../../calendar/schedule.provider';
|
||||||
import {profilePageSections} from '../../../../config/profile-page-sections';
|
import {profilePageSections} from '../../../../config/profile-page-sections';
|
||||||
import {AuthService} from "../../auth/auth.service";
|
import {AuthService} from '../../auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -36,7 +36,13 @@ export class ProfilePageComponent {
|
|||||||
|
|
||||||
async signIn() {
|
async signIn() {
|
||||||
const originPath = this.activatedRoute.snapshot.queryParamMap.get('origin_path');
|
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();
|
await this.authService.signIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +51,6 @@ export class ProfilePageComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ionViewWillEnter() {
|
ionViewWillEnter() {
|
||||||
this.authService
|
void this.authService.loadUserInfo();
|
||||||
.getValidToken()
|
|
||||||
.then(() => void this.authService.loadUserInfo())
|
|
||||||
.catch(() => {
|
|
||||||
// noop
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user