2019-12-08 00:32:00 +05:30
import { browserHistory } from 'app/services/history' ;
import logger from 'app/services/logger' ;
import localStorage from 'app/services/localStorage' ;
2024-08-28 16:37:23 +05:30
import { Store , State as RootState , Dispatch } from 'app/types' ;
import {
activate as activateAccount ,
authenticate ,
logoutAll as logout ,
remove as removeAccount ,
} from 'app/components/accounts/actions' ;
import * as actions from 'app/components/auth/actions' ;
import { updateUser } from 'app/components/user/actions' ;
2024-12-18 03:41:39 +05:30
import dispatchBsod from 'app/components/ui/bsod/dispatchBsod' ;
2016-12-07 02:38:51 +05:30
2024-12-18 03:41:39 +05:30
import FinishState from './FinishState' ;
2016-03-02 02:06:14 +05:30
import RegisterState from './RegisterState' ;
import LoginState from './LoginState' ;
2024-12-11 01:12:06 +05:30
import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState' ;
import InitOAuthDeviceCodeFlowState from './InitOAuthDeviceCodeFlowState' ;
2016-03-02 02:06:14 +05:30
import ForgotPasswordState from './ForgotPasswordState' ;
2016-05-15 02:23:58 +05:30
import RecoverPasswordState from './RecoverPasswordState' ;
2016-06-05 17:36:14 +05:30
import ActivationState from './ActivationState' ;
2016-08-27 15:49:02 +05:30
import CompleteState from './CompleteState' ;
2018-02-18 01:29:35 +05:30
import ChooseAccountState from './ChooseAccountState' ;
2016-05-23 00:28:43 +05:30
import ResendActivationState from './ResendActivationState' ;
2024-08-28 16:37:23 +05:30
import State from './State' ;
2017-06-08 01:52:51 +05:30
2024-12-11 01:12:06 +05:30
interface Request {
2020-05-24 04:38:24 +05:30
path : string ;
query : URLSearchParams ;
params : Record < string , any > ;
2024-12-11 01:12:06 +05:30
}
2017-09-09 20:34:26 +05:30
2024-08-28 16:37:23 +05:30
export const availableActions = {
updateUser ,
authenticate ,
activateAccount ,
removeAccount ,
logout ,
goBack : actions.goBack ,
redirect : actions.redirect ,
login : actions.login ,
acceptRules : actions.acceptRules ,
forgotPassword : actions.forgotPassword ,
recoverPassword : actions.recoverPassword ,
register : actions.register ,
activate : actions.activate ,
resendActivation : actions.resendActivation ,
contactUs : actions.contactUs ,
setLogin : actions.setLogin ,
setAccountSwitcher : actions.setAccountSwitcher ,
setErrors : actions.setErrors ,
clearErrors : actions.clearErrors ,
oAuthValidate : actions.oAuthValidate ,
oAuthComplete : actions.oAuthComplete ,
setClient : actions.setClient ,
resetOAuth : actions.resetOAuth ,
resetAuth : actions.resetAuth ,
setOAuthRequest : actions.setOAuthRequest ,
setOAuthCode : actions.setOAuthCode ,
requirePermissionsAccept : actions.requirePermissionsAccept ,
setScopes : actions.setScopes ,
setLoadingState : actions.setLoadingState ,
} ;
type ActionId = keyof typeof availableActions ;
2017-09-09 20:34:26 +05:30
2017-08-23 00:09:08 +05:30
export interface AuthContext {
2024-08-28 16:37:23 +05:30
run < T extends ActionId > ( actionId : T , payload? : Parameters < typeof availableActions [ T ] > [ 0 ] ) : Promise < any > ; // TODO: can't find a way to explain to TS the returned type
setState ( newState : State ) : Promise < void > | void ; // TODO: always return promise
2020-05-24 04:38:24 +05:30
getState ( ) : RootState ;
navigate ( route : string , options ? : { replace? : boolean } ) : void ;
getRequest ( ) : Request ;
2024-08-28 16:37:23 +05:30
prevState : State ;
2017-08-23 00:09:08 +05:30
}
export default class AuthFlow implements AuthContext {
2024-08-28 16:37:23 +05:30
actions : Readonly < typeof availableActions > ;
state : State ;
prevState : State ;
2020-05-24 04:38:24 +05:30
/ * *
* A callback from router , that allows to replace ( perform redirect ) route
* during route transition
* /
replace : ( ( path : string ) = > void ) | null ;
onReady : ( ) = > void ;
navigate : ( route : string , options : { replace? : boolean } ) = > void ;
currentRequest : Partial < Request > = { } ;
oAuthStateRestored = false ;
2024-08-28 16:37:23 +05:30
dispatch : Dispatch ;
2020-05-24 04:38:24 +05:30
getState : ( ) = > RootState ;
2024-08-28 16:37:23 +05:30
constructor ( actions : typeof availableActions ) {
2020-05-24 04:38:24 +05:30
this . actions = Object . freeze ( actions ) ;
}
2016-04-12 09:19:58 +05:30
2020-05-24 04:38:24 +05:30
setStore ( store : Store ) : void {
this . navigate = ( route : string , options : { replace? : boolean } = { } ) : void = > {
const { path : currentPath } = this . getRequest ( ) ;
2016-04-12 09:19:58 +05:30
2020-05-24 04:38:24 +05:30
if ( currentPath !== route ) {
if ( currentPath . startsWith ( '/oauth2/v1' ) && options . replace === undefined ) {
options . replace = true ;
}
2016-03-02 02:06:14 +05:30
2020-05-24 04:38:24 +05:30
if ( this . replace ) {
this . replace ( route ) ;
}
2016-03-02 02:06:14 +05:30
2024-12-11 01:12:06 +05:30
if ( options . replace ) {
browserHistory . replace ( route ) ;
} else {
browserHistory . push ( route ) ;
}
2020-05-24 04:38:24 +05:30
}
2016-03-02 02:06:14 +05:30
2020-05-24 04:38:24 +05:30
this . replace = null ;
} ;
2016-03-02 02:06:14 +05:30
2020-05-24 04:38:24 +05:30
this . getState = store . getState . bind ( store ) ;
this . dispatch = store . dispatch . bind ( store ) ;
}
2016-03-02 02:06:14 +05:30
2024-08-28 16:37:23 +05:30
resolve ( payload : Record < string , any > = { } ) {
2024-12-18 03:41:39 +05:30
const maybePromise = this . state . resolve ( this , payload ) ;
if ( maybePromise && maybePromise . catch ) {
maybePromise . catch ( ( err ) = > {
dispatchBsod ( ) ;
throw err ;
} ) ;
}
2020-05-24 04:38:24 +05:30
}
2017-12-31 00:34:31 +05:30
2024-08-28 16:37:23 +05:30
reject ( payload : Record < string , any > = { } ) {
2024-12-18 03:41:39 +05:30
try {
this . state . reject ( this , payload ) ;
} catch ( err ) {
dispatchBsod ( ) ;
throw err ;
}
2020-05-24 04:38:24 +05:30
}
2016-03-02 02:06:14 +05:30
2020-05-24 04:38:24 +05:30
goBack() {
this . state . goBack ( this ) ;
2016-03-02 02:06:14 +05:30
}
2024-08-28 16:37:23 +05:30
run < T extends ActionId > ( actionId : T , payload? : Parameters < typeof availableActions [ T ] > [ 0 ] ) : Promise < any > {
2024-12-18 03:41:39 +05:30
// @ts-expect-error the extended version of redux with thunk will return the correct promise
2024-08-28 16:37:23 +05:30
return Promise . resolve ( this . dispatch ( this . actions [ actionId ] ( payload ) ) ) ;
2016-03-02 02:06:14 +05:30
}
2024-12-18 03:41:39 +05:30
setState ( state : State ) : Promise < void > | void {
2024-12-11 01:12:06 +05:30
this . state ? . leave ( this ) ;
2020-05-24 04:38:24 +05:30
this . prevState = this . state ;
this . state = state ;
const resp = this . state . enter ( this ) ;
2019-11-27 14:33:32 +05:30
2020-05-24 04:38:24 +05:30
if ( resp && resp . then ) {
// this is a state with an async enter phase
// block route components from mounting, till promise will be resolved
if ( this . onReady ) {
const callback = this . onReady ;
this . onReady = ( ) = > { } ;
2019-11-27 14:33:32 +05:30
2020-05-24 04:38:24 +05:30
return resp . then ( callback , ( error ) = > {
logger . error ( 'State transition error' , { error } ) ;
2019-12-26 17:48:58 +05:30
2020-05-24 04:38:24 +05:30
return error ;
} ) ;
}
2016-08-07 19:24:59 +05:30
2020-05-24 04:38:24 +05:30
return resp ;
}
2019-11-27 14:33:32 +05:30
}
2020-05-24 04:38:24 +05:30
getRequest() {
return {
path : '' ,
query : new URLSearchParams ( ) ,
params : { } ,
. . . this . currentRequest ,
} ;
2019-11-27 14:33:32 +05:30
}
2016-06-15 11:31:41 +05:30
2020-05-24 04:38:24 +05:30
/ * *
* This should be called from onEnter prop of react - router Route component
* /
2024-12-11 01:12:06 +05:30
handleRequest ( request : Request , replace : ( path : string ) = > void , callback : ( ) = > void = ( ) = > { } ) : void {
2020-05-24 04:38:24 +05:30
const { path } = request ;
this . replace = replace ;
this . onReady = callback ;
if ( ! path ) {
throw new Error ( 'The request.path is required' ) ;
}
if ( this . getRequest ( ) . path === path ) {
// we are already handling this path
this . onReady ( ) ;
2016-06-15 11:31:41 +05:30
2020-05-24 04:38:24 +05:30
return ;
}
2016-08-27 15:49:02 +05:30
2020-05-24 04:38:24 +05:30
this . currentRequest = request ;
2016-06-02 23:16:49 +05:30
2020-05-24 04:38:24 +05:30
if ( this . restoreOAuthState ( ) ) {
return ;
}
2016-08-12 00:50:14 +05:30
2020-05-24 04:38:24 +05:30
switch ( path ) {
case '/register' :
this . setState ( new RegisterState ( ) ) ;
break ;
case '/forgot-password' :
this . setState ( new ForgotPasswordState ( ) ) ;
break ;
case '/resend-activation' :
this . setState ( new ResendActivationState ( ) ) ;
break ;
case '/choose-account' :
this . setState ( new ChooseAccountState ( ) ) ;
break ;
2024-12-11 01:12:06 +05:30
case '/code' :
this . setState ( new InitOAuthDeviceCodeFlowState ( ) ) ;
break ;
case '/oauth/finish' :
this . setState ( new FinishState ( ) ) ;
break ;
2020-05-24 04:38:24 +05:30
case '/' :
case '/login' :
case '/password' :
case '/mfa' :
case '/accept-rules' :
case '/oauth/permissions' :
case '/oauth/choose-account' :
this . setState ( new LoginState ( ) ) ;
break ;
default :
switch (
path . replace ( /(.)\/.+/ , '$1' ) // use only first part of an url
) {
case '/oauth2' :
2024-12-11 01:12:06 +05:30
this . setState ( new InitOAuthAuthCodeFlowState ( ) ) ;
2020-05-24 04:38:24 +05:30
break ;
case '/activation' :
this . setState ( new ActivationState ( ) ) ;
break ;
case '/recover-password' :
this . setState ( new RecoverPasswordState ( ) ) ;
break ;
default :
replace ( '/404' ) ;
break ;
}
2016-10-25 11:31:51 +05:30
}
2016-10-25 05:10:05 +05:30
2020-05-24 04:38:24 +05:30
this . onReady ( ) ;
2019-12-28 14:58:25 +05:30
}
2020-05-24 04:38:24 +05:30
/ * *
* Tries to restore last oauth request , if it was stored in localStorage
* in last 2 hours
* /
private restoreOAuthState ( ) : boolean {
if ( this . oAuthStateRestored ) {
return false ;
}
2019-12-28 14:58:25 +05:30
2020-05-24 04:38:24 +05:30
this . oAuthStateRestored = true ;
2019-12-29 21:56:51 +05:30
2020-05-24 04:38:24 +05:30
if ( /^\/(register|oauth2)/ . test ( this . getRequest ( ) . path ) ) {
// allow register or the new oauth requests
return false ;
}
2016-08-12 00:50:14 +05:30
2020-05-24 04:38:24 +05:30
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const data = JSON . parse ( localStorage . getItem ( 'oauthData' ) ! ) ;
const expirationTime = 2 * 60 * 60 * 1000 ; // 2h
2016-08-27 15:49:02 +05:30
2020-05-24 04:38:24 +05:30
if ( Date . now ( ) - data . timestamp < expirationTime ) {
this . run ( 'oAuthValidate' , data . payload )
. then ( ( ) = > this . setState ( new CompleteState ( ) ) )
. then ( ( ) = > this . onReady ( ) ) ;
2019-11-27 14:33:32 +05:30
2020-05-24 04:38:24 +05:30
return true ;
}
} catch ( err ) {
/* bad luck :( */
}
return false ;
}
2016-03-02 02:06:14 +05:30
}