2019-12-07 21:02:00 +02:00
import { browserHistory } from 'app/services/history' ;
import logger from 'app/services/logger' ;
import localStorage from 'app/services/localStorage' ;
2024-08-28 13:07:23 +02:00
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-10 20:42:06 +01:00
import FinishState from './FinishState' ;
2016-12-06 23:08:51 +02:00
2016-03-01 22:36:14 +02:00
import RegisterState from './RegisterState' ;
import LoginState from './LoginState' ;
2024-12-10 20:42:06 +01:00
import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState' ;
import InitOAuthDeviceCodeFlowState from './InitOAuthDeviceCodeFlowState' ;
2016-03-01 22:36:14 +02:00
import ForgotPasswordState from './ForgotPasswordState' ;
2016-05-14 23:53:58 +03:00
import RecoverPasswordState from './RecoverPasswordState' ;
2016-06-05 15:06:14 +03:00
import ActivationState from './ActivationState' ;
2016-08-27 13:19:02 +03:00
import CompleteState from './CompleteState' ;
2018-02-17 21:59:35 +02:00
import ChooseAccountState from './ChooseAccountState' ;
2016-05-22 21:58:43 +03:00
import ResendActivationState from './ResendActivationState' ;
2024-08-28 13:07:23 +02:00
import State from './State' ;
2017-06-07 23:22:51 +03:00
2024-12-10 20:42:06 +01:00
interface Request {
2020-05-24 02:08:24 +03:00
path : string ;
query : URLSearchParams ;
params : Record < string , any > ;
2024-12-10 20:42:06 +01:00
}
2017-09-09 18:04:26 +03:00
2024-08-28 13:07:23 +02:00
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 18:04:26 +03:00
2017-08-22 21:39:08 +03:00
export interface AuthContext {
2024-08-28 13:07:23 +02:00
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 02:08:24 +03:00
getState ( ) : RootState ;
navigate ( route : string , options ? : { replace? : boolean } ) : void ;
getRequest ( ) : Request ;
2024-08-28 13:07:23 +02:00
prevState : State ;
2017-08-22 21:39:08 +03:00
}
export default class AuthFlow implements AuthContext {
2024-08-28 13:07:23 +02:00
actions : Readonly < typeof availableActions > ;
state : State ;
prevState : State ;
2020-05-24 02:08:24 +03:00
/ * *
* 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 13:07:23 +02:00
dispatch : Dispatch ;
2020-05-24 02:08:24 +03:00
getState : ( ) = > RootState ;
2024-08-28 13:07:23 +02:00
constructor ( actions : typeof availableActions ) {
2020-05-24 02:08:24 +03:00
this . actions = Object . freeze ( actions ) ;
}
2016-04-12 06:49:58 +03:00
2020-05-24 02:08:24 +03:00
setStore ( store : Store ) : void {
this . navigate = ( route : string , options : { replace? : boolean } = { } ) : void = > {
const { path : currentPath } = this . getRequest ( ) ;
2016-04-12 06:49:58 +03:00
2020-05-24 02:08:24 +03:00
if ( currentPath !== route ) {
if ( currentPath . startsWith ( '/oauth2/v1' ) && options . replace === undefined ) {
options . replace = true ;
}
2016-03-01 22:36:14 +02:00
2020-05-24 02:08:24 +03:00
if ( this . replace ) {
this . replace ( route ) ;
}
2016-03-01 22:36:14 +02:00
2024-12-10 20:42:06 +01:00
if ( options . replace ) {
browserHistory . replace ( route ) ;
} else {
browserHistory . push ( route ) ;
}
2020-05-24 02:08:24 +03:00
}
2016-03-01 22:36:14 +02:00
2020-05-24 02:08:24 +03:00
this . replace = null ;
} ;
2016-03-01 22:36:14 +02:00
2020-05-24 02:08:24 +03:00
this . getState = store . getState . bind ( store ) ;
this . dispatch = store . dispatch . bind ( store ) ;
}
2016-03-01 22:36:14 +02:00
2024-08-28 13:07:23 +02:00
resolve ( payload : Record < string , any > = { } ) {
2020-05-24 02:08:24 +03:00
this . state . resolve ( this , payload ) ;
}
2017-12-30 21:04:31 +02:00
2024-08-28 13:07:23 +02:00
reject ( payload : Record < string , any > = { } ) {
2020-05-24 02:08:24 +03:00
this . state . reject ( this , payload ) ;
}
2016-03-01 22:36:14 +02:00
2020-05-24 02:08:24 +03:00
goBack() {
this . state . goBack ( this ) ;
2016-03-01 22:36:14 +02:00
}
2024-08-28 13:07:23 +02:00
run < T extends ActionId > ( actionId : T , payload? : Parameters < typeof availableActions [ T ] > [ 0 ] ) : Promise < any > {
// @ts-ignore the extended version of redux with thunk will return the correct promise
return Promise . resolve ( this . dispatch ( this . actions [ actionId ] ( payload ) ) ) ;
2016-03-01 22:36:14 +02:00
}
2024-08-28 13:07:23 +02:00
setState ( state : State ) {
2024-12-10 20:42:06 +01:00
this . state ? . leave ( this ) ;
2020-05-24 02:08:24 +03:00
this . prevState = this . state ;
this . state = state ;
const resp = this . state . enter ( this ) ;
2019-11-27 11:03:32 +02:00
2020-05-24 02:08:24 +03:00
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 11:03:32 +02:00
2020-05-24 02:08:24 +03:00
return resp . then ( callback , ( error ) = > {
logger . error ( 'State transition error' , { error } ) ;
2019-12-26 14:18:58 +02:00
2020-05-24 02:08:24 +03:00
return error ;
} ) ;
}
2016-08-07 16:54:59 +03:00
2020-05-24 02:08:24 +03:00
return resp ;
}
2019-11-27 11:03:32 +02:00
}
2020-05-24 02:08:24 +03:00
getRequest() {
return {
path : '' ,
query : new URLSearchParams ( ) ,
params : { } ,
. . . this . currentRequest ,
} ;
2019-11-27 11:03:32 +02:00
}
2016-06-15 09:01:41 +03:00
2020-05-24 02:08:24 +03:00
/ * *
* This should be called from onEnter prop of react - router Route component
* /
2024-12-10 20:42:06 +01:00
handleRequest ( request : Request , replace : ( path : string ) = > void , callback : ( ) = > void = ( ) = > { } ) : void {
2020-05-24 02:08:24 +03:00
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 09:01:41 +03:00
2020-05-24 02:08:24 +03:00
return ;
}
2016-08-27 13:19:02 +03:00
2020-05-24 02:08:24 +03:00
this . currentRequest = request ;
2016-06-02 20:46:49 +03:00
2020-05-24 02:08:24 +03:00
if ( this . restoreOAuthState ( ) ) {
return ;
}
2016-08-11 22:20:14 +03:00
2020-05-24 02:08:24 +03:00
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-10 20:42:06 +01:00
case '/code' :
this . setState ( new InitOAuthDeviceCodeFlowState ( ) ) ;
break ;
case '/oauth/finish' :
this . setState ( new FinishState ( ) ) ;
break ;
2020-05-24 02:08:24 +03:00
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-10 20:42:06 +01:00
this . setState ( new InitOAuthAuthCodeFlowState ( ) ) ;
2020-05-24 02:08:24 +03:00
break ;
case '/activation' :
this . setState ( new ActivationState ( ) ) ;
break ;
case '/recover-password' :
this . setState ( new RecoverPasswordState ( ) ) ;
break ;
default :
replace ( '/404' ) ;
break ;
}
2016-10-25 09:01:51 +03:00
}
2016-10-25 02:40:05 +03:00
2020-05-24 02:08:24 +03:00
this . onReady ( ) ;
2019-12-28 11:28:25 +02:00
}
2020-05-24 02:08:24 +03:00
/ * *
* Tries to restore last oauth request , if it was stored in localStorage
* in last 2 hours
*
* @returns { bool } - whether oauth state is being restored
* /
private restoreOAuthState ( ) : boolean {
if ( this . oAuthStateRestored ) {
return false ;
}
2019-12-28 11:28:25 +02:00
2020-05-24 02:08:24 +03:00
this . oAuthStateRestored = true ;
2019-12-29 18:26:51 +02:00
2020-05-24 02:08:24 +03:00
if ( /^\/(register|oauth2)/ . test ( this . getRequest ( ) . path ) ) {
// allow register or the new oauth requests
return false ;
}
2016-08-11 22:20:14 +03:00
2020-05-24 02:08:24 +03:00
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 13:19:02 +03:00
2020-05-24 02:08:24 +03:00
if ( Date . now ( ) - data . timestamp < expirationTime ) {
this . run ( 'oAuthValidate' , data . payload )
. then ( ( ) = > this . setState ( new CompleteState ( ) ) )
. then ( ( ) = > this . onReady ( ) ) ;
2019-11-27 11:03:32 +02:00
2020-05-24 02:08:24 +03:00
return true ;
}
} catch ( err ) {
/* bad luck :( */
}
return false ;
}
2016-03-01 22:36:14 +02:00
}