@pvway/alpha-oas
Version:
Alpha OAuth Service by p.v.Way
583 lines (571 loc) • 20.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http';
import { Observable, map, catchError, throwError, mergeMap } from 'rxjs';
import { v4 } from 'uuid';
var AlphaAuthStatusEnum;
(function (AlphaAuthStatusEnum) {
AlphaAuthStatusEnum[AlphaAuthStatusEnum["Undefined"] = 0] = "Undefined";
AlphaAuthStatusEnum[AlphaAuthStatusEnum["Anonymous"] = 1] = "Anonymous";
AlphaAuthStatusEnum[AlphaAuthStatusEnum["Authenticating"] = 2] = "Authenticating";
AlphaAuthStatusEnum[AlphaAuthStatusEnum["Refreshing"] = 3] = "Refreshing";
AlphaAuthStatusEnum[AlphaAuthStatusEnum["Authenticated"] = 4] = "Authenticated";
})(AlphaAuthStatusEnum || (AlphaAuthStatusEnum = {}));
// noinspection JSUnresolvedReference
class AlphaPrincipal {
mStatus;
get status() {
return this.mStatus;
}
setStatus(status) {
this.mStatus = status;
}
mUser;
get user() {
return this.mUser;
}
setUser(user) {
this.mUser = user;
this.setSessionLanguageCode(user.languageCode);
}
setSessionLanguageCode(lc) {
// interceptor will use this value for inserting
// the language-code header on each outgoing request
sessionStorage.setItem('alphaLanguageCode', lc);
}
clearUser() {
this.mUser = null;
sessionStorage.removeItem('alphaLanguageCode');
}
get languageCode() {
if (this.mUser) {
return this.mUser.languageCode;
}
const lsLc = sessionStorage.getItem('alphaLanguageCode');
if (lsLc) {
return lsLc;
}
const nav = window.navigator;
const userLang = (nav.language || nav.userLanguage);
return userLang
? userLang.substring(0, 2).toLowerCase()
: 'en';
}
get isAuthenticated() {
return this.mStatus === AlphaAuthStatusEnum.Authenticated;
}
get isAnonymous() {
return this.mStatus === AlphaAuthStatusEnum.Undefined
|| this.mStatus === AlphaAuthStatusEnum.Anonymous;
}
get isAuthenticating() {
return this.mStatus === AlphaAuthStatusEnum.Authenticating
|| this.mStatus === AlphaAuthStatusEnum.Refreshing;
}
constructor() {
this.mStatus = AlphaAuthStatusEnum.Undefined;
this.mUser = null;
}
}
class AlphaSessionData {
static rememberMeFieldName = 'alphaRememberMe';
static accessTokenFieldName = 'alphaAccessToken';
static receptionTsFieldName = 'alphaReceptionTs';
static expirationTsFieldName = 'alphaExpirationTs';
rememberMe;
accessToken;
/** timestamp of token reception in ms */
receptionTs;
/** timestamp of token expiration in ms */
expirationTs;
get isExpiredOrExpiring() {
const nowTs = new Date().getTime();
return this.expirationTs - nowTs < 60000;
}
constructor(rememberMe, accessToken, receptionTs, // timestamp of token reception in ms
expirationTs) {
this.rememberMe = rememberMe;
this.accessToken = accessToken;
this.receptionTs = receptionTs;
this.expirationTs = expirationTs;
}
// expiresIn is expressed in seconds
static getTimestamps(expiresIn) {
const receptionTs = new Date().getTime();
const expirationTs = receptionTs + expiresIn * 1000;
return {
receptionTs,
expirationTs
};
}
static retrieve() {
const rmString = sessionStorage
.getItem(AlphaSessionData.rememberMeFieldName);
if (rmString == null)
return null;
const rm = rmString === 'true';
const at = sessionStorage
.getItem(AlphaSessionData.accessTokenFieldName) ?? '';
const rTsString = sessionStorage
.getItem(AlphaSessionData.receptionTsFieldName) ?? '0';
const rTs = parseInt(rTsString, 10);
const xTsString = sessionStorage
.getItem(AlphaSessionData.expirationTsFieldName) ?? '0';
const xTs = parseInt(xTsString, 10);
return new AlphaSessionData(rm, at, rTs, xTs);
}
static clear() {
sessionStorage.removeItem(AlphaSessionData.rememberMeFieldName);
sessionStorage.removeItem(AlphaSessionData.accessTokenFieldName);
sessionStorage.removeItem(AlphaSessionData.receptionTsFieldName);
sessionStorage.removeItem(AlphaSessionData.expirationTsFieldName);
}
store() {
sessionStorage.setItem(AlphaSessionData.rememberMeFieldName, this.rememberMe.toString());
sessionStorage.setItem(AlphaSessionData.accessTokenFieldName, this.accessToken);
sessionStorage.setItem(AlphaSessionData.receptionTsFieldName, this.receptionTs.toString());
sessionStorage.setItem(AlphaSessionData.expirationTsFieldName, this.expirationTs.toString());
}
}
class AlphaRefreshData {
static refreshTokenFieldName = 'alphaRefreshToken';
refreshToken;
constructor(refreshToken) {
this.refreshToken = refreshToken;
}
static retrieve() {
const rt = localStorage
.getItem(AlphaRefreshData.refreshTokenFieldName);
if (rt == null) {
return null;
}
return new AlphaRefreshData(rt);
}
static clear() {
localStorage.removeItem(AlphaRefreshData.refreshTokenFieldName);
}
store() {
localStorage.setItem(AlphaRefreshData.refreshTokenFieldName, this.refreshToken);
}
}
// noinspection JSUnresolvedReference
// FACTORY
class AlphaUserFactory {
static factorFromDso(dso) {
const ds = new DsoSlicer$1(dso);
return new AlphaUser(ds.SingleFragment);
}
}
// DSO SLICER
let DsoSlicer$1 = class DsoSlicer {
_dso;
constructor(dso) {
this._dso = dso;
}
get SingleFragment() {
const propertyArray = Object.entries(this._dso.userProperties);
return {
userId: this._dso.userId,
username: this._dso.username,
languageCode: this._dso.languageCode,
properties: new Map(propertyArray)
};
}
};
// CONCRETES
class AlphaUser {
userId;
username;
languageCode;
properties;
constructor(f0) {
this.userId = f0.userId;
this.username = f0.username;
this.languageCode = f0.languageCode;
this.properties = f0.properties;
}
}
// noinspection JSUnresolvedReference
// FACTORY
class AlphaAuthEnvelopFactory {
static factorFromDso(dso) {
const ds = new DsoSlicer(dso);
return new AuthEnvelop(ds.SingleFragment);
}
}
// DSO SLICER
class DsoSlicer {
_dso;
constructor(dso) {
this._dso = dso;
}
get SingleFragment() {
return {
accessToken: this._dso.access_token,
expiresIn: this._dso.expires_in,
refreshToken: this._dso.refresh_token,
user: AlphaUserFactory.factorFromDso(this._dso.user)
};
}
}
// CONCRETES
class AuthEnvelop {
accessToken;
expiresIn;
refreshToken;
user;
constructor(f0) {
this.accessToken = f0.accessToken;
this.expiresIn = f0.expiresIn;
this.refreshToken = f0.refreshToken;
this.user = f0.user;
}
}
class AlphaOasService {
mHttp;
mContext = 'OAuthService';
mPrincipal;
mSignInUrl;
mRefreshUrl;
mGetMeUrl;
mOnPrincipalUpdated = () => {
};
mPostErrorLog = () => {
};
get principal() {
return this.mPrincipal;
}
constructor() {
this.mPrincipal = new AlphaPrincipal();
}
/**
* Initializes the authentication process by retrieving session data, refresh data, or setting authentication to anonymous mode.
*
* @param httpClient - need to inject the httpClient here
* @param {string} [getMeUrl] - The URL for retrieving user information.
* @param {string} [refreshUrl] - The URL for refreshing authentication.
* @param {string} [signInUrl] - The URL for signing in.
* @param {function} [postErrorLog] - A function that handles error logging.
* It accepts three parameters: context, method, and error.
* @param {function} [onPrincipalUpdated] - A function that will be triggered whenever the principal is updated.
* It accepts one parameter: principal of type IAlphaPrincipal.
*
* @return {Observable} - An Observable that emits the result of the initialization process.
*/
init(httpClient, getMeUrl, refreshUrl, signInUrl, postErrorLog, onPrincipalUpdated) {
this.mHttp = httpClient;
this.mSignInUrl = signInUrl;
this.mRefreshUrl = refreshUrl;
this.mGetMeUrl = getMeUrl;
if (postErrorLog) {
this.mPostErrorLog = postErrorLog;
}
if (onPrincipalUpdated) {
this.mOnPrincipalUpdated = onPrincipalUpdated;
}
// let's first see if there is still a session data
const sd = AlphaSessionData.retrieve();
if (sd != null) {
return this.initFromSd();
}
// then check if there is a refresh data available
const rd = AlphaRefreshData.retrieve();
if (rd != null) {
return this.initFromRd();
}
// no sd and no rd found let's start as anonymous
return this.initAsAnonymous();
}
initFromSd() {
console.log('Sd found... calling getMe');
return new Observable((subscriber) => {
this.getMe().subscribe({
next: () => subscriber.next('principal reloaded'),
error: e => subscriber.error(e)
});
});
}
initFromRd() {
return new Observable((subscriber) => {
console.log('rd active... calling refresh');
this.mPrincipal.setStatus(AlphaAuthStatusEnum.Refreshing);
this.refresh().subscribe({
next: refreshed => {
if (refreshed) {
subscriber.next('identity refreshed');
}
else {
this.mPrincipal.setStatus(AlphaAuthStatusEnum.Anonymous);
subscriber.next('refreshed failed');
}
},
error: (e) => {
subscriber.error(e);
}
});
});
}
initAsAnonymous() {
return new Observable((subscriber) => {
this.mPrincipal.setStatus(AlphaAuthStatusEnum.Anonymous);
subscriber.next('anonymous');
});
}
/**
* Inject your own signIn method */
useSignIn(signIn) {
this.internalSignIn = signIn;
}
/**
* Inject your own refresh method */
useRefresh(refresh) {
this.internalRefresh = refresh;
}
/** Inject your own authorize method */
useAuthorize(authorize) {
this.internalAuthorize = authorize;
}
internalSignIn = (username, password) => {
if (this.mHttp === undefined) {
throw new Error('service is not initialized');
}
const body = 'grant_type=password' +
'&username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
const headers = new HttpHeaders()
.set('content-type', 'application/x-www-form-urlencoded');
const url = this.mSignInUrl;
return this.mHttp
.post(url, body, { headers: headers })
.pipe(map(dso => AlphaAuthEnvelopFactory.factorFromDso(dso)), catchError(error => {
this.mPostErrorLog(this.mContext, '_signIn', JSON.stringify(error));
return throwError(() => error);
}));
};
/**
* On successful login call storeIdentity.
* Remark: there is no need to call getMe from signIn as getMe
* is actually returning the same data as signIn
* @param username
* @param password
* @param rememberMe
*/
signIn(username, password, rememberMe) {
return new Observable((subscriber) => {
this.internalSignIn(username, password, rememberMe)
.subscribe({
next: (token) => {
this.storeIdentity(token, rememberMe);
subscriber.next(true);
},
error: (e) => {
this.signOut();
if (e.status === 400 || e.status === 401) {
subscriber.next(false);
}
else {
this.mPostErrorLog(this.mContext, 'login', JSON.stringify(e));
subscriber.error(e);
}
}
});
return;
});
}
/**
* default implementation of refresh
* this implementation can be overridden by calling useRefresh
*/
internalRefresh = (refreshToken) => {
if (this.mHttp === undefined) {
throw new Error('service is not initialized');
}
const body = 'grant_type=refresh_token' +
'&refresh_token=' + encodeURIComponent(refreshToken);
const headers = new HttpHeaders()
.set('content-type', 'application/x-www-form-urlencoded');
const url = this.mRefreshUrl;
return this.mHttp.post(url, body, { headers: headers })
.pipe(map(dso => AlphaAuthEnvelopFactory.factorFromDso(dso)));
};
/**
* refreshes the accessToken from the refreshToken
* found in the local storage data
*/
refresh() {
const rd = AlphaRefreshData.retrieve();
if (rd == null) {
this.mPostErrorLog(this.mContext, 'refresh', 'rd should not be null');
return throwError(() => 'rd should not be null');
}
return new Observable((subscriber) => {
this.internalRefresh(rd.refreshToken)
.subscribe({
next: (token) => {
this.storeIdentity(token, true);
subscriber.next(true);
},
error: (e) => {
this.signOut();
if (e.status == 401) {
subscriber.next(false);
}
else {
this.mPostErrorLog(this.mContext, 'refresh', JSON.stringify(e));
subscriber.error(e);
}
}
});
});
}
/**
* called from the init() method when the session data is present
*/
getMe() {
if (!this.mHttp) {
throw new Error('service is not initialized');
}
const url = this.mGetMeUrl;
const call = this.mHttp.get(url)
.pipe(map(dso => {
const user = AlphaUserFactory.factorFromDso(dso);
this.populatePrincipal(user);
return user;
}), catchError((error) => {
this.mPostErrorLog(this.mContext, url, JSON.stringify(error));
return throwError(() => error);
}));
return this.authorize(call);
}
editUserInfo(firstName, lastName, languageCode) {
if (!this.mPrincipal.user) {
return;
}
this.mPrincipal.user.username = firstName + " " + lastName;
this.mPrincipal.user.languageCode = languageCode;
this.mOnPrincipalUpdated(this.mPrincipal);
}
signOut() {
AlphaSessionData.clear();
AlphaRefreshData.clear();
this.mPrincipal.clearUser();
this.mPrincipal.setStatus(AlphaAuthStatusEnum.Anonymous);
this.mOnPrincipalUpdated(this.mPrincipal);
}
/**
* checks that the accessToken is not expired or
* expiring (expirationTime - 1 minute).
* if still valid fires the request directly else inserts a
* refresh before firing the request
*/
internalAuthorize(httpRequest) {
const sd = AlphaSessionData.retrieve();
if (sd == null || sd.isExpiredOrExpiring) {
return this.refresh()
.pipe(mergeMap(() => httpRequest), catchError(error => {
this.mPostErrorLog(this.mContext, '_authorize', JSON.stringify(error));
return throwError(() => error);
}));
}
else {
return httpRequest;
}
}
/**
* checks the accessToken and eventually refreshes it
* before calling the request
*/
authorize(httpRequest) {
return this.internalAuthorize(httpRequest);
}
/**
* (1) stores the access token in the session storage
* (2) stores the refresh token in the local storage
* (3) populates the principal using the user info
*/
storeIdentity(authEnvelop, rememberMe) {
const ts = AlphaSessionData
.getTimestamps(authEnvelop.expiresIn);
const sd = new AlphaSessionData(true, authEnvelop.accessToken, ts.receptionTs, ts.expirationTs);
sd.store();
console.log('sd populated');
if (rememberMe) {
const rd = new AlphaRefreshData(authEnvelop.refreshToken);
rd.store();
console.log('rd populated');
}
this.populatePrincipal(authEnvelop.user);
}
populatePrincipal(user) {
this.mPrincipal.setUser(user);
console.log('principal user is set');
this.mPrincipal.setStatus(AlphaAuthStatusEnum.Authenticated);
this.mOnPrincipalUpdated(this.mPrincipal);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: AlphaOasService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: AlphaOasService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: AlphaOasService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class AlphaOasInterceptor {
intercept(req, next) {
const eReq = AlphaOasInterceptor.enrichReq(req);
return next.handle(eReq);
}
static handlerFn(req, next) {
const eReq = this.enrichReq(req);
return next(eReq);
}
static enrichReq(req) {
let headers = req.headers;
// getting languageCode
// set by the principal when setting the user
let languageCode = sessionStorage.getItem('alphaLanguageCode');
if (!languageCode) {
const nav = window.navigator;
const userLang = (nav.language || nav.userLanguage);
languageCode = userLang
? userLang.substring(0, 2)?.toLowerCase()
: 'en';
}
// always add the language-code header
headers = headers.append('language-code', languageCode);
/**
* the ClientId (client-id header) identifies the client.
* There will be one client-id for each browser.
* With this in place it will be possible to
* map a new user to his browsing history
* ClientId is only generated once and stored in
* the browser localstorage associated to the url
*/
let clientId = localStorage.getItem('alphaClientId');
if (clientId == null) {
clientId = v4();
localStorage.setItem('alphaClientId', clientId);
}
// always add the client-id header
headers = headers.append('client-id', clientId);
/**
* when an accessToken is present insert the authorization header
* using the accessToken with the bearer scheme
*/
const sd = AlphaSessionData.retrieve();
const token = sd?.accessToken;
if (token) {
headers = headers.append('authorization', `bearer ${token}`);
}
return req.clone({ headers });
}
}
/*
* Public API Surface of alpha-oas
*/
/**
* Generated bundle index. Do not edit.
*/
export { AlphaAuthEnvelopFactory, AlphaAuthStatusEnum, AlphaOasInterceptor, AlphaOasService, AlphaPrincipal, AlphaSessionData, AlphaUserFactory };
//# sourceMappingURL=pvway-alpha-oas.mjs.map