UNPKG

@pvway/alpha-oas

Version:

Alpha OAuth Service by p.v.Way

583 lines (571 loc) 20.4 kB
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