@baqhub/sdk-react
Version:
The official React SDK for the BAQ federated app platform.
329 lines (328 loc) • 14.9 kB
JavaScript
"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 };
}