Skip to content

Instantly share code, notes, and snippets.

@mrflip
Last active November 7, 2022 23:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrflip/1e9755a2d99dee197b2bd95e4ec67fe6 to your computer and use it in GitHub Desktop.
Save mrflip/1e9755a2d99dee197b2bd95e4ec67fe6 to your computer and use it in GitHub Desktop.
URQL + AWS Cognito Auth code

If you just care about basic URQL stuff, almost everything you need is in AuthExchange and index.js. The rest of it is the skeleton of a production React Native mobile app. I've ripped this out of our full app and so you'll need to do further surgery to remove references to our internal code, but it should be good as an inspiration/example.

The /index.js entry point just takes care of loading certain polyfills and libraries, including CognitoSetup. The referenced AppConfig.js file (not included) grabs secrets and configuration from the extras section of the expo app.config.js via Expo Constants.expoConfig

From there, GlobalWrapper coordinates acquiring a session (if the customer is logged in) and all asynchronous tasks that can be launched before and without authentication --

  1. set a deadline timer; if the intital tasks have not completed, hide the resplash screen anyway and log an error to bugsnag.
  2. Resplash replaces the native-app splash screen with a one that overlays a dynamic message, and is only dismissed when either the deadline is hit or all startup chores are done
  3. Acquires a cognito session, if any, and stores it into a React context provider.
  4. Grab stored preferences from AsyncStorage
  5. Preload fonts and images
  6. Fetches the 'sup, dawg page -- a static independently-hosted json payload that lets us force an app upgrade, display a maintenance mode message, etc. If so, the global wrapper will bail out to just that page.

The Main app grabs the session context; if the customer is not logged in, it returns the signin/signout/etc flow navigator; otherwise it

  1. acquires the graphql context provider
  2. warms the cache with a series of graphql calls:
  • customer account information that we will save as global context
  • first round of eager queries for the landing page; once this completes, the splash screen is revealed
  • second round of eager queries that do not block using the app
  1. Wraps a couple other providers (feature flags, UI frameworks) and finally the app itself.

Other notes:

  • The "readier" stuff is just a contraption to track the startup flow.
  • say.hi is a convenience wrapper around conole.log that we can knock out in production; Log.info/warn/error records events to console, bugsnag and elsewhere.
  • Very excited to get feedback on better ways to do any of the stuff we're doing here.
// --- path: src/graphql/AuthExchange.js
import _ /**/ from 'lodash'
import * as Urql from 'urql'
import { authExchange } from '@urql/exchange-auth'
import { Auth } from 'aws-amplify'
import { Buffer } from 'buffer'
//
import * as Utils from '../utils'
import AppConfig from '../utils/AppConfig'
import * as SessionContext from '../auth/SessionContext'
const { say, Log } = Utils
function getHeaders(authState) {
const headers = {
claimedorg: authState.activeOkey,
claimedrole: authState.claimedrole,
rqTC: Utils.IDHelpers.nowTC(),
Authorization: `Bearer ${authState.token}`,
}
return headers
}
function authFetchOptions(operation) {
if (typeof operation.context.fetchOptions === 'function') {
return operation.context.fetchOptions()
}
return operation.context.fetchOptions || {}
}
export function AuthExchange() {
const config = {
getAuth: async (args) => {
const cogsession = await Auth.currentAuthenticatedUser()
.catch((err) => say.error(err, { ...args, act: 'getAuth', during: 'currentAuthenticatedUser' }))
if (cogsession) {
const tksession = SessionContext.parseCogession(cogsession)
return { ...tksession, token: tksession.jwt }
}
return null
},
addAuthToOperation({ authState, operation }) {
try {
if ((! authState?.token)) { return operation /* not logged in */ }
//
const fetchOptions = authFetchOptions(operation)
fetchOptions.headers = { ...fetchOptions.headers, ...getHeaders(authState) }
// do not replace with _.merge. do not talk to flip about why.
const context = { ...operation.context, fetchOptions }
const newOp = Urql.makeOperation(operation.kind, operation, context)
return newOp
} catch (err) {
Log.error(err, { act: 'willAuthError' })
return operation
}
},
willAuthError({ authState }) {
if (! authState) { return true }
try {
const [_rest, payload] = authState.token.split('.')
const raw = Buffer.from(payload, 'base64').toString()
const parsed = JSON.parse(raw)
const exp = parsed?.exp
if (! (_.isFinite(exp) && (exp > 1200100))) { return true }
const nowSeconds = Date.now() / 1000
return (nowSeconds > (exp - 120))
} catch (err) {
Log.error(err, { act: 'willAuthError' })
return true
}
},
didAuthError({ error }) {
try {
// FIXME -- should this be narrowed to just the error that an expired JWT would cause, or any unauth'ed action?
return error.graphQLErrors.some((err) => (/FORBIDDEN|Unauthorized/.test(err.extensions?.code)))
} catch (err) {
Log.error(err, { act: 'didAuthError' })
return false
}
},
}
return authExchange(config)
}
export default AuthExchange
// path: src/auth/CognitoSetup.js
import Amplify from 'aws-amplify'
//
import AppConfig from '../utils/AppConfig'
import say from '../utils/say'
const AmplifyConfig = {
Auth: {
userPoolId: AppConfig.Secrets.cogUserpool,
userPoolWebClientId: AppConfig.Secrets.cogClientid,
identityPoolId: AppConfig.Secrets.cogIdpool,
mandatorySignIn: true,
region: AppConfig.cogRegion,
},
Storage: {
region: AppConfig.s3Region,
bucket: AppConfig.s3UploadBucket,
identityPoolId: AppConfig.Secrets.cogIdpool,
},
API: {
endpoints: [
{
name: "graphql",
endpoint: AppConfig.paladinUrl,
region: AppConfig.apiRegion,
},
],
},
}
say.hi('auth', 'AmplifyConfig', AmplifyConfig)
Amplify.configure(AmplifyConfig)
// path: src/boot/GlobalWrapper.js
import _ /**/ from 'lodash'
import React, { useRef, useState } /**/ from 'react'
import { Provider as PaperProvider } from 'react-native-paper'
import { StatusBar } from 'expo-status-bar'
import { AppState, StyleSheet, View } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import * as SplashScreen from 'expo-splash-screen'
import AuthenticatorContext from '../auth/AuthenticatorContext'
//
import ErrorHandler, { Toast } from '../utils/ErrorHandler'
import * as Utils from '../utils'
import loadAssets from './LoadAssets'
//
import { SessionContext } from '../auth/SessionContext'
import * as Readiness from './Readiness'
// import PreflightWidget from '../widgets/PreflightWidget'
import GraphqlWrapper from '../graphql/GraphqlWrapper'
import NavigationWrapper from './NavigationWrapper'
import Resplash from './Resplash'
//
import { fetchSupdawgStatus, MustUpgradeScreen, MaintenanceModeScreen,
} from './SupdawgScreen'
// all startup tasks should take less time than this
const STARTUP_DEADLINE = 6000
const {
say, Log, AppConfig, UNSET,
} = Utils
say.hi('GlobalWrapper', AppConfig.brand, AppConfig.stage)
const GlobalWrapper = ({ children }) => {
const act = 'GlobalWrapper'
const [supdawg, setSupdawg] = useState({})
const [session, setSession] = useState(null)
const [authStatus, setAuthStatus] = useState(UNSET)
const [revealable, setRevealable] = useState(false)
const [prefs, setPrefs] = useState(Utils.Prefs.defaultPrefs)
const [startupTS, setStartupTS] = useState(0)
const [deadlineTS, setDeadlineTS] = useState(0)
const appState = useRef(AppState.currentState)
const [_appStateVisible, setAppStateVisible] = useState(appState.current)
//
const readier = Readiness.useReadier()
const showSplash = (! revealable)
const onPrefsLoaded = (val) => { setPrefs(val) }
const sessionSetter = React.useCallback((sess) => setSession(sess), [])
//
const sessionUpdaters = React.useMemo(
() => SessionContext.getSessionUpdaters({ setSession: sessionSetter, setAuthStatus }),
[],
)
//
React.useEffect(() => {
startupTasks({ readier, setSupdawg, sessionUpdaters, onPrefsLoaded, setDeadlineTS, setStartupTS })
.catch((err) => Log.error(err, { act: 'startupTasks' }))
}, [])
React.useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === "active") {
loadSessionTask({ readier, sessionUpdaters, startupTS })
supdawgTask({ readier, setSupdawg, startupTS })
}
appState.current = nextAppState
setAppStateVisible(appState.current)
})
return () => {
subscription.remove()
}
}, [])
const resplashReady = React.useCallback(() => (
SplashScreen.hideAsync().then(() => readier({ unsplashed: true }))
), [])
React.useLayoutEffect(() => {
if (deadlineTS && (! revealable)) {
Log.warn('bored now: deadline hit before splash naturally revealed', { deadlineTS, revealable, startupTS })
setRevealable(true)
}
}, [deadlineTS, revealable, startupTS])
//
// -----------------
//
say.hi(act, { authStatus, revealable, deadlineTS })
if (supdawg?.mustUpgrade) {
say.hi(act, 'mustUpgrade', supdawg)
Log.info('mustUpgrade', { act })
return (<MustUpgradeScreen supdawg={supdawg} />)
}
if (supdawg?.maintenanceMode) {
Log.info('maintenanceMode', { act })
return (<MaintenanceModeScreen supdawg={supdawg} />)
}
return (
<>
<Utils.Prefs.PrefsContext.Provider value={prefs}>
<Readiness.RevealableContext.Provider value={setRevealable}>
{showSplash && (<View style={styles.resplash}><Resplash onLayout={resplashReady} /></View>)}
<SafeAreaProvider>
<NavigationWrapper>
<StatusBar />
<AuthenticatorContext.Provider value={sessionUpdaters}>
<SessionContext.Provider value={session}>
<View style={styles.container}>
{children}
</View>
</SessionContext.Provider>
<Toast config={ErrorHandler.toastConfig} />
</AuthenticatorContext.Provider>
</NavigationWrapper>
</SafeAreaProvider>
</Readiness.RevealableContext.Provider>
</Utils.Prefs.PrefsContext.Provider>
</>
)
}
async function startupTasks(args) {
const { setStartupTS, readier } = args
const startupTS = Date.now()
setStartupTS(startupTS)
readier({ startupTS })
const settledResults = await Promise.allSettled([
supdawgTask(args),
loadAssetTask(args),
startDeadlineTask(args),
loadPrefsTask(args),
loadSessionTask(args),
]).then((tasks) => _.map(tasks, (task) => (task.value ?? task)))
const results = _.zipObject(['supdawg', 'assets', 'deadline', 'prefs', 'session'], settledResults)
say.banner('GlobalSetup', 'startupTasks end', results)
readier({ startupDur: (Date.now() - startupTS) })
return results
}
async function supdawgTask({ readier, setSupdawg, startupTS }) {
const supdawg = await fetchSupdawgStatus()
setSupdawg(supdawg)
readier({ supdawged: Date.now() - startupTS })
return supdawg
}
async function loadAssetTask({ readier, startupTS }) {
const result = await loadAssets()
readier({ asseted: Date.now() - startupTS })
return result
}
async function startDeadlineTask({ readier, setDeadlineTS, startupTS }) {
await Utils.sleep(STARTUP_DEADLINE)
setDeadlineTS(Date.now())
readier({ deadlined: Date.now() - startupTS })
return { slept: STARTUP_DEADLINE }
}
async function loadPrefsTask({ readier, onPrefsLoaded, startupTS }) {
const prefs = await Utils.Prefs.loadPrefs()
readier({ prefed: Date.now() - startupTS, prefs })
onPrefsLoaded(prefs)
return prefs
}
async function loadSessionTask({ readier, sessionUpdaters, startupTS }) {
const session = await sessionUpdaters.refreshSession()
readier({ authLoaded: Date.now() - startupTS, sessioned: (!! session) })
return session
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
resplash: {
position: "absolute",
height: "100%",
width: "100%",
justifyContent: "center",
alignItems: "stretch",
bottom: 0,
zIndex: 10,
},
})
export default GlobalWrapper
// --- path: src/graphql/GraphqlWrapper.js
/* eslint-disable no-unused-vars */
import _ /**/ from 'lodash'
import React /**/ from 'react'
import * as Urql from 'urql'
import { devtoolsExchange } from '@urql/devtools'
import { cacheExchange } from '@urql/exchange-graphcache'
import { requestPolicyExchange } from '@urql/exchange-request-policy'
import { retryExchange } from '@urql/exchange-retry'
//
import PaladinSchema from '../../gen/graphql/PaladinSchema'
import * as urqlHelpers from '../../gen/graphql/urqlHelpers'
import * as WsGraphqlClient from './WsGraphqlClient'
import cacheUpdaters from './cacheUpdaters'
//
import * as Utils from '../utils'
import AuthExchange from './AuthExchange'
const { dedupExchange, fetchExchange, subscriptionExchange } = Urql
const { say, Log, AppConfig } = Utils
const retryOptions = {
initialDelayMs: 2000, // default 1000
maxNumberAttempts: 2, // default 2
retryIf: (err) => (err && err.networkError),
}
export const graphqlClientCheat = {}
const BaseHeaders = {
//
appchannel: AppConfig.brand,
appstage: AppConfig.stage,
appcode: AppConfig.brand,
appname: AppConfig.appname,
appver: AppConfig.version,
appbuild: AppConfig.buildVersion,
tkver: '^v2.5.0',
}
function setGraphqlClient() {
const graphqlClient = Urql.createClient({
url: AppConfig.paladinURL,
requestPolicy: 'cache-first',
exchanges: [
// synchronous exchanges first
devtoolsExchange,
dedupExchange,
requestPolicyExchange({
ttl: 60 * 5 * 1000, // time until an operation becomes cache-and-network
}),
cacheExchange({
schema: PaladinSchema,
resolvers: urqlHelpers.cacheResolvers,
updates: cacheUpdaters,
}),
AuthExchange(),
fetchExchange,
retryExchange(retryOptions),
],
fetchOptions: () => ({
redirect: 'follow',
headers: BaseHeaders,
}),
})
graphqlClientCheat.graphqlClient = graphqlClient
return graphqlClient
}
export const GraphqlWrapper = ({ children }) => {
const session = Utils.useActiveSession()
const client = React.useMemo(() => setGraphqlClient(), [session])
return (
<Urql.Provider value={client}>
{children}
</Urql.Provider>
)
}
export default GraphqlWrapper
/* eslint-disable import/first */
// --- path: index.js (at top level of project)
// These are placed in a very special order, largely to ensure that certain
// side-effects of loading them happen in the given order. Do not
// change this order without a good reason and careful verification.
import _ExpoRandom from 'expo-random'
// eslint-disable-next-line import/newline-after-import
import { polyfillWebCrypto } from 'expo-standard-web-crypto'
polyfillWebCrypto()
import _RNG from 'react-native-get-random-values'
import _ /**/ from 'lodash'
import _ExpoDevClient /**/ from 'expo-dev-client'
import _AmazonCognitoIdentity from 'amazon-cognito-identity-js'
import React /**/ from 'react'
import { LogBox } from 'react-native'
import { registerRootComponent } from 'expo'
import * as _Paper from 'react-native-paper'
//
import _AppConfig from './src/utils/AppConfig'
import _CognitoSetup from './src/auth/CognitoSetup'
import _Bugsnag from './src/utils/Bugsnag'
//
import GlobalWrapper from './src/boot/GlobalWrapper'
import Main from './src/boot/Main'
// eslint-disable-next-line import/order
import * as SplashScreen from 'expo-splash-screen'
import * as Utils from './src/utils'
//
const { say, Log } = Utils
// quash known errors/warnings in packages beyond our control
LogBox.ignoreLogs(['AnimatedComponent', /UNSAFE_componentWillReceiveProps.*AnimatedComponent/])
const App = (props) => {
say.hi('app', 'started')
return (
<React.StrictMode>
<GlobalWrapper>
<Main {...props} />
</GlobalWrapper>
</React.StrictMode>
)
}
// registerRootComponent calls AppRegistry.registerComponent('main', () => App)
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately.
registerRootComponent(App)
SplashScreen.preventAutoHideAsync()

Copyright 2022 Tookstock, Inc

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// --- path: src/boot/Main.js
import _ /**/ from 'lodash'
import React /**/ from 'react'
import { Provider as PaperProvider } from 'react-native-paper'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import * as Urql from 'urql'
//
import * as Utils from '../utils'
import * as Readiness from './Readiness'
import GraphqlWrapper from '../graphql/GraphqlWrapper'
import CustomerWrapper from './CustomerWrapper'
//
import AppNavigator from '../navigation/BottomTabNavigator'
import SignedoutApp from './SignedoutApp'
import * as InitialQueries from './InitialQueries'
const { say, Log, AppConfig, mixpanel } = Utils
function walkGqlErrors(results, story) {
_.each(results, (result) => {
if (result.errors) {
_.each(result.errors, (err) => Log.error(err, story))
}
})
}
const SessionedMain = ({ session }) => {
const act = 'SessionedMain'
const graphqlClient = Urql.useClient()
const setRevealable = Readiness.useRevealable()
const readier = Readiness.useReadier()
const [bootstrapped, setBootstrapped] = React.useState(false)
//
// Initialize MixPanel
///
React.useEffect(() => { mixpanel.init() }, [])
React.useEffect(() => {
async function initialQueries() {
if (! (graphqlClient && session)) { return }
say.hi(act, 'starting initial queries')
// queries for the wrapper tree to complete (customer, tooksubs)
const bootstrapQs_P = InitialQueries.runBootstrapQueries(graphqlClient, { session }, { act })
.catch((err) => Log.error(err, { act, during: 'startupQueries' }))
.then((qr) => { setBootstrapped(true); readier({ bootstrapQed: Date.now() }); return qr })
// queries that the initial screens need
const startupQs_P = InitialQueries.runStartupQueries(graphqlClient, { session }, { act })
.catch((err) => Log.error(err, { act, during: 'startupQueries' }))
.then((qr) => { readier({ startupQed: Date.now() }); return qr })
// wait on both
const [bootstrapQs, startupQs] = await Promise.all([bootstrapQs_P, startupQs_P])
walkGqlErrors(bootstrapQs, { act: 'bootstrapQs' })
walkGqlErrors(startupQs, { act: 'startupQs' })
readier({ revealable: Date.now() })
setRevealable(true)
//
await InitialQueries.runEagerQueries(graphqlClient, { ...bootstrapQs, session }, { act })
.catch((err) => Log.error(err, { act, during: 'startupQueries' }))
.then(() => readier({ eagerQed: Date.now() }))
}
initialQueries().catch((err) => Log.error(err, { act }))
}, [graphqlClient, session])
if (! bootstrapped) { return null }
return (
<CustomerWrapper>
<FeaturedWrapper>
<PaperProvider theme={Utils.theme}>
<BottomSheetModalProvider>
<AppNavigator />
</BottomSheetModalProvider>
</PaperProvider>
</FeaturedWrapper>
</CustomerWrapper>
)
}
const Main = () => {
say.hi('Main', AppConfig.brand, AppConfig.stage)
const session = Utils.useMaybeSession()
const _navigator = Utils.useNavigator()
if (! session) {
return (
<PaperProvider theme={Utils.theme}>
<SignedoutApp />
</PaperProvider>
)
}
return (
<GraphqlWrapper>
<SessionedMain session={session} />
</GraphqlWrapper>
)
}
export default Main
// --- path: src/auth/SessionContext.js
import _ /**/ from 'lodash'
import React /**/ from 'react'
import { Auth } from 'aws-amplify'
//
import { ALLOW, ERRORED, UNSET, VALID } from '../utils/Symbols'
import say from '../utils/say'
import * as Errors from '../utils/Errors'
import * as UF from '../utils/Useful'
import * as Log from '../utils/Log'
import * as IDHelpers from '../utils/IDHelpers'
export const SessionContext = React.createContext(null)
export function getSessionUpdaters({ setSession, setAuthStatus, setIsOnboarded }) {
const erroredSession = async (err, story = {}) => {
try {
setSession(null)
setAuthStatus(ERRORED)
return Log.error(err, story)
} catch (err2) {
console.error('double error in erroredSession')
return err2
}
}
const unsetSession = async (story = {}) => {
setSession(null)
setAuthStatus(UNSET)
return Log.info('unsetSession', story)
}
const setValidSession = async (cogsession, story = {}) => {
try {
if (isValidCogsession) {
say.sess('app', 'setValidSession', cogsession, story)
const session = parseCogession(cogsession, story)
//
setSession(session)
setAuthStatus(VALID)
await Log.startSession(session)
return session
}
await Promise.all([
Log.warn('setValidSession with invalid session', { ...story, cogsession, during: 'setValidSession' }),
unsetSession(story),
])
} catch (err) {
erroredSession(err, { ...story, during: 'setValidSession' })
}
return null
}
//
const logout = async (options = {}, storyIn = {}) => {
const { global = false, otherTasks = [] } = options
const story = { ...storyIn, ...options, during: 'logout' }
await Promise.all([
unsetSession(story),
Auth.signOut({ global }),
..._.map(otherTasks, (task) => task(options, storyIn)),
]).catch((err) => { erroredSession(err, story) })
say.hi('auth', 'logged out')
}
//
const refreshSession = async (story = {}) => (
Auth.currentAuthenticatedUser().then(async (session) => {
if (session) {
say.sess('app', 'get auth', story, session)
const result = await setValidSession(session, story)
return result
}
say.sess('app', 'no user', story, session)
await unsetSession(story)
return null
}).catch(async (err) => {
if (/The user is not authenticated/.test(err)) { // WTH Amazon Cognito is THE WORST
await unsetSession(story).catch((err2) => { console.error('double error in sessionRefresh', err, err2); return null })
return null
}
if (err.code) {
Log.error(err, { during: 'app auth error', ...story })
await unsetSession(story).catch((err2) => { console.error('double error in sessionRefresh', err, err2); return null })
return null
}
Log.error(err, { during: 'app session refresh error', ...story })
await erroredSession(err, story)
.catch((err2) => { console.error('double error in sessionRefresh', err, err2) })
throw err
})
)
//
return { refreshSession, logout, setValidSession, unsetSession, erroredSession, setIsOnboarded }
}
export function isValidCogsession(cogsession) {
return (!! (
cogsession?.signInUserSession?.idToken?.jwtToken
&& cogsession?.username
&& cogsession?.attributes?.email
))
}
export function parseCogession(cogsession, story = {}) {
// skipped: username, pool, Session, client, authenticationFlowType, storage, pool, keyPrefix, userDataKey, attributes, preferredMFA
//
if (! isValidCogsession(cogsession)) {
throw Errors.BadValue({ val: { cogsession }, want: 'raw cognito session' })
}
//
const { username:cogkey, attributes:rawattrs, signInUserSession:tokens } = cogsession
const jwt = tokens.idToken.jwtToken
const cogattrs = parseCogattrs(rawattrs)
//
const session = { ...cogattrs, cogkey }
// sugar to set a non-enumerable property
UF.adorn(session, 'cogsession', cogsession)
UF.adorn(session, 'jwt', jwt)
UF.adorn(session, 'tokens', tokens)
// say.hi('auth', 'session', session, session.jwt, session.tokens)
//
say.hi('SessionContext', 'parse cogsession', story, session)
return session
}
// export const isManagerish = () => {
// const activeRole = getActiveRole().toLowerCase()
// return false
// }
const GUEST_ABILITIES = {
addToBasket: ALLOW,
}
const EDITOR_ABILITIES = {
addContact: ALLOW, addLoc: ALLOW, editPurchase: ALLOW, editChat: ALLOW,
usePicker: ALLOW, addSemaphore: ALLOW,
}
const MANAGER_ABILITIES = {
placePurchase: ALLOW, editTeam: ALLOW,
}
function getAbilities(session) {
const { claimedrole } = session
const managerish = /^(owner|manager|admin)$/.test(claimedrole)
const editorish = /^(owner|manager|admin|editor)$/.test(claimedrole)
const guestish = true
const abilities = {}
if (managerish) { Object.assign(abilities, MANAGER_ABILITIES) }
if (editorish) { Object.assign(abilities, EDITOR_ABILITIES) }
if (guestish) { Object.assign(abilities, GUEST_ABILITIES) }
return abilities
}
export function parseCogattrs(rawattrs) {
try {
const {
email_verified:emailVerified,
phone_number_verified:phoneVerified,
locale,
'custom:orgs':orgRolesJSON,
"custom:activeOrg":activeOrgJSON,
email: cogemail,
} = rawattrs || {}
//
const orgRoles = orgRolesJSON ? JSON.parse(orgRolesJSON) : {}
if (_.isEmpty(orgRoles)) { Log.warn('No Org Roles found!', { during: 'parseCogattrs', rawattrs }) }
const okeys = _.keys(orgRoles)
//
const activeOrg = JSON.parse(activeOrgJSON)
//
const { okey:activeOkey, id:activeOrgID } = activeOrg
const identkey = IDHelpers.identkeyFromEmail0({ email0: cogemail })
const identID = `dnt.tk:${identkey}`
const identingID = `dng.tk/${identID}/${activeOrgID}`
const claimedrole = orgRoles?.[activeOkey]?.role
//
const session = {
cogemail, identkey, claimedrole, identID, identingID, activeOkey, activeOrgID, orgRoles, okeys, emailVerified, phoneVerified, locale,
}
UF.requireValues({ cogemail, identkey, claimedrole, identID, identingID, activeOkey, activeOrgID, orgRoles, okeys, emailVerified })
//
session.abilities = getAbilities(session)
session.can = (ability) => (session.abilities[ability] === ALLOW)
return session
//
} catch (err) {
Log.error(err, { during: 'parseCogattrs', rawattrs })
throw err
}
}
SessionContext.getSessionUpdaters = getSessionUpdaters
export default SessionContext
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment