UNPKG

@baqhub/sdk-react

Version:

The official React SDK for the BAQ federated app platform.

329 lines (328 loc) 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectError = exports.ConnectStatus = exports.AuthenticationStatus = void 0; exports.buildAuthentication = buildAuthentication; const tslib_1 = require("tslib"); const sdk_1 = require("@baqhub/sdk"); const isEqual_js_1 = tslib_1.__importDefault(require("lodash/isEqual.js")); const react_1 = require("react"); const hooks_js_1 = require("../helpers/hooks.js"); const stateErrors_js_1 = require("../helpers/stateErrors.js"); const storage_js_1 = require("../helpers/storage.js"); const suspense_js_1 = require("../helpers/suspense.js"); const storeIdentity_js_1 = require("./store/storeIdentity.js"); // // State. // var AuthenticationStatus; (function (AuthenticationStatus) { AuthenticationStatus["UNAUTHENTICATED"] = "unauthenticated"; AuthenticationStatus["AUTHENTICATED"] = "authenticated"; })(AuthenticationStatus || (exports.AuthenticationStatus = AuthenticationStatus = {})); var ConnectStatus; (function (ConnectStatus) { ConnectStatus["IDLE"] = "idle"; ConnectStatus["CONNECTING"] = "connecting"; ConnectStatus["WAITING_ON_FLOW"] = "waiting_on_flow"; })(ConnectStatus || (exports.ConnectStatus = ConnectStatus = {})); var ConnectError; (function (ConnectError) { ConnectError["ENTITY_NOT_FOUND"] = "entity_not_found"; ConnectError["BAD_APP_RECORD"] = "bad_app_record"; ConnectError["OTHER"] = "other"; })(ConnectError || (exports.ConnectError = ConnectError = {})); // // Actions. // var UseAuthenticationActionType; (function (UseAuthenticationActionType) { UseAuthenticationActionType["INITIALIZE_SUCCESS"] = "INITIALIZE_SUCCESS"; UseAuthenticationActionType["CONNECT_START"] = "CONNECT_START"; UseAuthenticationActionType["CONNECT_SUCCESS"] = "CONNECT_SUCCESS"; UseAuthenticationActionType["CONNECT_FAILURE"] = "CONNECT_FAILURE"; UseAuthenticationActionType["AUTHORIZATION_SUCCESS"] = "AUTHORIZATION_SUCCESS"; UseAuthenticationActionType["AUTHORIZATION_FAILURE"] = "AUTHORIZATION_FAILURE"; UseAuthenticationActionType["DISCONNECT"] = "DISCONNECT"; })(UseAuthenticationActionType || (UseAuthenticationActionType = {})); // // Reducer. // function reducer(state, action) { switch (action.type) { case UseAuthenticationActionType.INITIALIZE_SUCCESS: if (state.status !== AuthenticationStatus.AUTHENTICATED || !("localState" in state)) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.AUTHENTICATED, identity: action.identity || state.identity, }; case UseAuthenticationActionType.CONNECT_START: if (state.status !== AuthenticationStatus.UNAUTHENTICATED || state.connectStatus !== ConnectStatus.IDLE) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.CONNECTING, entity: action.entity, }; case UseAuthenticationActionType.CONNECT_SUCCESS: if (state.status !== AuthenticationStatus.UNAUTHENTICATED || state.connectStatus !== ConnectStatus.CONNECTING) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.WAITING_ON_FLOW, flowUrl: action.flowUrl, localState: action.localState, }; case UseAuthenticationActionType.CONNECT_FAILURE: if (state.status !== AuthenticationStatus.UNAUTHENTICATED || state.connectStatus !== ConnectStatus.CONNECTING) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.IDLE, error: action.error, }; case UseAuthenticationActionType.AUTHORIZATION_SUCCESS: if (state.status !== AuthenticationStatus.UNAUTHENTICATED || state.connectStatus !== ConnectStatus.WAITING_ON_FLOW) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.AUTHENTICATED, localState: state.localState, authorizationId: action.authorizationId, identity: action.identity, }; case UseAuthenticationActionType.AUTHORIZATION_FAILURE: if (state.status !== AuthenticationStatus.UNAUTHENTICATED || state.connectStatus !== ConnectStatus.WAITING_ON_FLOW) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.IDLE, }; case UseAuthenticationActionType.DISCONNECT: if (state.status !== AuthenticationStatus.AUTHENTICATED) { throw new stateErrors_js_1.InvalidActionError(state, action); } return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.IDLE, }; default: (0, sdk_1.unreachable)(action); } } function buildAuthentication(options) { const { storage, secureStorage, app } = options; const authenticationStorage = new storage_js_1.AuthenticationStorage(storage, secureStorage); const findLocalState = (0, suspense_js_1.buildFetcher)(async () => { return authenticationStorage.read(); }); function buildInitialState(newAuthorizationId) { const localState = findLocalState(); if (!localState) { return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.IDLE, }; } const authorizationId = newAuthorizationId || localState.authorizationId; if (!authorizationId) { return { status: AuthenticationStatus.UNAUTHENTICATED, connectStatus: ConnectStatus.IDLE, }; } const localStateWithAuthorization = sdk_1.Authentication.complete(localState, authorizationId); return { status: AuthenticationStatus.AUTHENTICATED, authorizationId, localState, identity: storeIdentity_js_1.StoreIdentity.new(localStateWithAuthorization), }; } function useAuthentication(options = {}) { const { appIconUrl, authorizationId } = options; const [authenticationState, dispatch] = (0, react_1.useReducer)(reducer, authorizationId || undefined, buildInitialState); // // API. // const onConnectRequest = (0, react_1.useCallback)((entity) => { dispatch({ type: UseAuthenticationActionType.CONNECT_START, entity, }); }, []); const waitingOnFlowLocalState = authenticationState.status === AuthenticationStatus.UNAUTHENTICATED && authenticationState.connectStatus === ConnectStatus.WAITING_ON_FLOW && authenticationState; const onAuthorizationResult = (0, react_1.useCallback)((authorizationId) => { if (!waitingOnFlowLocalState) { throw new Error("Authentication not waiting on flow."); } if (!authorizationId) { dispatch({ type: UseAuthenticationActionType.AUTHORIZATION_FAILURE }); return; } const { localState } = waitingOnFlowLocalState; const localStateWithAuthorization = { ...localState, authorizationId: authorizationId || localState.authorizationId, }; dispatch({ type: UseAuthenticationActionType.AUTHORIZATION_SUCCESS, identity: storeIdentity_js_1.StoreIdentity.new(localStateWithAuthorization), authorizationId, }); }, [waitingOnFlowLocalState]); const onDisconnectRequest = (0, react_1.useCallback)(() => { dispatch({ type: UseAuthenticationActionType.DISCONNECT }); }, []); // // Clear local state. // const unauthenticatedLocalState = authenticationState.status === AuthenticationStatus.UNAUTHENTICATED && authenticationState.connectStatus === ConnectStatus.IDLE && authenticationState; (0, react_1.useEffect)(() => { if (!unauthenticatedLocalState) { return; } authenticationStorage.write(undefined); }, [unauthenticatedLocalState]); // // Initialization. // const initializingLocalState = authenticationState.status === AuthenticationStatus.AUTHENTICATED && "localState" in authenticationState && authenticationState; (0, react_1.useEffect)(() => { if (!initializingLocalState) { return; } const { authorizationId, localState, identity } = initializingLocalState; const { findClient } = identity; return (0, hooks_js_1.abortable)(async (signal) => { try { const client = findClient(localState.entityRecord.author.entity); const [serverEntityRecord, serverAppRecord] = await Promise.all([ client.getOwnRecord(sdk_1.AnyRecord, sdk_1.EntityRecord, localState.entityRecord.id, { signal }), client.getOwnRecord(sdk_1.AnyRecord, sdk_1.AppRecord, localState.appRecord.id, { signal, }), ]); const updatedState = { ...localState, authorizationId, entityRecord: serverEntityRecord.record, appRecord: serverAppRecord.record, }; // Check compatibility of app record scopes with what we require. if (!sdk_1.AppScopes.hasScopes(updatedState.appRecord, app.scopeRequest)) { dispatch({ type: UseAuthenticationActionType.DISCONNECT }); return; } const authorizationIdChanged = updatedState.authorizationId !== localState.authorizationId; const recordsChanged = !(0, isEqual_js_1.default)(updatedState.entityRecord, serverEntityRecord.record) || !(0, isEqual_js_1.default)(updatedState.appRecord, serverAppRecord.record); if (authorizationIdChanged || recordsChanged) { await authenticationStorage.write(updatedState); } if (!recordsChanged) { dispatch({ type: UseAuthenticationActionType.INITIALIZE_SUCCESS, identity: undefined, }); return; } await authenticationStorage.write(updatedState); dispatch({ type: UseAuthenticationActionType.INITIALIZE_SUCCESS, identity: storeIdentity_js_1.StoreIdentity.new(updatedState), }); } catch (error) { if (error instanceof sdk_1.AbortedError || signal.aborted) { return; } if (error instanceof sdk_1.RequestError && [ sdk_1.HttpStatusCode.NOT_FOUND, sdk_1.HttpStatusCode.FORBIDDEN, sdk_1.HttpStatusCode.UNAUTHORIZED, ].includes(error.status)) { dispatch({ type: UseAuthenticationActionType.DISCONNECT }); return; } throw error; } }); }, [initializingLocalState]); // // Connection. // const appIconPromise = (0, react_1.useMemo)(() => { if (!appIconUrl) { return undefined; } if (authenticationState.status !== AuthenticationStatus.UNAUTHENTICATED) { return undefined; } return sdk_1.Http.download(appIconUrl).then(([_, b]) => b); }, [appIconUrl, authenticationState.status]); const connectingState = authenticationState.status === AuthenticationStatus.UNAUTHENTICATED && authenticationState.connectStatus === ConnectStatus.CONNECTING && authenticationState; (0, react_1.useEffect)(() => { if (!connectingState) { return; } console.log("Starting auth."); const { entity } = connectingState; return (0, hooks_js_1.abortable)(async (signal) => { try { const icon = await appIconPromise; const auth = await sdk_1.Authentication.register(entity, app, { icon, signal, }); const { flowUrl, state } = auth; // Save the local state. await authenticationStorage.write(state); // Hand it off to the authentication flow. dispatch({ type: UseAuthenticationActionType.CONNECT_SUCCESS, flowUrl, localState: state, }); } catch (error) { if (error instanceof sdk_1.AbortedError || signal.aborted) { return; } console.log("Sign-in error:", error); dispatch({ type: UseAuthenticationActionType.CONNECT_FAILURE, error: ConnectError.OTHER, }); } }); }, [appIconPromise, connectingState]); return { state: authenticationState, onConnectRequest, onAuthorizationResult, onDisconnectRequest, }; } return { useAuthentication }; }