[Session] Refactor to prepare for V2 (#3781)
* Move types to another file Co-authored-by: dan <dan.abramov@gmail.com> * Move utilities out Co-authored-by: dan <dan.abramov@gmail.com> * Move PUBLIC_BSKY_AGENT Co-authored-by: dan <dan.abramov@gmail.com> * Move createPersistSessionHandler inline Co-authored-by: dan <dan.abramov@gmail.com> * Call configureModeration when clearing account too This ensures that the app labelers get reset in a test environment. Co-authored-by: dan <dan.abramov@gmail.com> * Make guest configureModeration sync, non-guest async * Extract isSessionExpired Co-authored-by: dan <dan.abramov@gmail.com> * Flip isSessionExpired condition Co-authored-by: dan <dan.abramov@gmail.com> * Extract agentToSessionAccount Co-authored-by: dan <dan.abramov@gmail.com> * Extract createAgent* Co-authored-by: dan <dan.abramov@gmail.com> * Simplify isSessionExpired --------- Co-authored-by: Eric Bailey <git@esb.lol>zio/stable
parent
66ad5543f1
commit
39807a8630
|
@ -18,7 +18,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||||
import {useNotificationsListener} from 'lib/notifications/notifications'
|
import {useNotificationsListener} from 'lib/notifications/notifications'
|
||||||
import {QueryProvider} from 'lib/react-query'
|
import {QueryProvider} from 'lib/react-query'
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||||
import {QueryProvider} from 'lib/react-query'
|
import {QueryProvider} from 'lib/react-query'
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
import {ThemeProvider} from 'lib/ThemeContext'
|
||||||
|
|
|
@ -1,11 +1,3 @@
|
||||||
import {BskyAgent} from '@atproto/api'
|
|
||||||
|
|
||||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
|
||||||
|
|
||||||
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
|
||||||
service: PUBLIC_BSKY_SERVICE,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const STALE = {
|
export const STALE = {
|
||||||
SECONDS: {
|
SECONDS: {
|
||||||
FIFTEEN: 1e3 * 15,
|
FIFTEEN: 1e3 * 15,
|
||||||
|
|
|
@ -1,100 +1,41 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api'
|
||||||
AtpPersistSessionHandler,
|
|
||||||
BSKY_LABELER_DID,
|
|
||||||
BskyAgent,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {jwtDecode} from 'jwt-decode'
|
|
||||||
|
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {IS_TEST_USER} from '#/lib/constants'
|
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||||
import {logEvent, LogEvents, tryFetchGates} from '#/lib/statsig/statsig'
|
import {logEvent, tryFetchGates} from '#/lib/statsig/statsig'
|
||||||
import {hasProp} from '#/lib/type-guards'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV} from '#/env'
|
||||||
import {emitSessionDropped} from '../events'
|
import {emitSessionDropped} from '../events'
|
||||||
import {readLabelers} from './agent-config'
|
import {
|
||||||
|
agentToSessionAccount,
|
||||||
|
configureModerationForAccount,
|
||||||
|
configureModerationForGuest,
|
||||||
|
createAgentAndCreateAccount,
|
||||||
|
createAgentAndLogin,
|
||||||
|
isSessionDeactivated,
|
||||||
|
isSessionExpired,
|
||||||
|
} from './util'
|
||||||
|
|
||||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
export type {SessionAccount} from '#/state/session/types'
|
||||||
|
import {
|
||||||
|
SessionAccount,
|
||||||
|
SessionApiContext,
|
||||||
|
SessionState,
|
||||||
|
SessionStateContext,
|
||||||
|
} from '#/state/session/types'
|
||||||
|
|
||||||
function __getAgent() {
|
export {isSessionDeactivated}
|
||||||
return __globalAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAgent() {
|
const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
|
||||||
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
configureModerationForGuest()
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionAccount = persisted.PersistedAccount
|
const StateContext = React.createContext<SessionStateContext>({
|
||||||
|
|
||||||
export type SessionState = {
|
|
||||||
isInitialLoad: boolean
|
|
||||||
isSwitchingAccounts: boolean
|
|
||||||
accounts: SessionAccount[]
|
|
||||||
currentAccount: SessionAccount | undefined
|
|
||||||
}
|
|
||||||
export type StateContext = SessionState & {
|
|
||||||
hasSession: boolean
|
|
||||||
}
|
|
||||||
export type ApiContext = {
|
|
||||||
createAccount: (props: {
|
|
||||||
service: string
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
handle: string
|
|
||||||
inviteCode?: string
|
|
||||||
verificationPhone?: string
|
|
||||||
verificationCode?: string
|
|
||||||
}) => Promise<void>
|
|
||||||
login: (
|
|
||||||
props: {
|
|
||||||
service: string
|
|
||||||
identifier: string
|
|
||||||
password: string
|
|
||||||
authFactorToken?: string | undefined
|
|
||||||
},
|
|
||||||
logContext: LogEvents['account:loggedIn']['logContext'],
|
|
||||||
) => Promise<void>
|
|
||||||
/**
|
|
||||||
* A full logout. Clears the `currentAccount` from session, AND removes
|
|
||||||
* access tokens from all accounts, so that returning as any user will
|
|
||||||
* require a full login.
|
|
||||||
*/
|
|
||||||
logout: (
|
|
||||||
logContext: LogEvents['account:loggedOut']['logContext'],
|
|
||||||
) => Promise<void>
|
|
||||||
/**
|
|
||||||
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
|
|
||||||
* clear access tokens from accounts, allowing the user to return to their
|
|
||||||
* other accounts without logging in.
|
|
||||||
*
|
|
||||||
* Used when adding a new account, deleting an account.
|
|
||||||
*/
|
|
||||||
clearCurrentAccount: () => void
|
|
||||||
initSession: (account: SessionAccount) => Promise<void>
|
|
||||||
resumeSession: (account?: SessionAccount) => Promise<void>
|
|
||||||
removeAccount: (account: SessionAccount) => void
|
|
||||||
selectAccount: (
|
|
||||||
account: SessionAccount,
|
|
||||||
logContext: LogEvents['account:loggedIn']['logContext'],
|
|
||||||
) => Promise<void>
|
|
||||||
updateCurrentAccount: (
|
|
||||||
account: Partial<
|
|
||||||
Pick<
|
|
||||||
SessionAccount,
|
|
||||||
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
|
|
||||||
>
|
|
||||||
>,
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const StateContext = React.createContext<StateContext>({
|
|
||||||
isInitialLoad: true,
|
isInitialLoad: true,
|
||||||
isSwitchingAccounts: false,
|
isSwitchingAccounts: false,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
|
@ -102,7 +43,7 @@ const StateContext = React.createContext<StateContext>({
|
||||||
hasSession: false,
|
hasSession: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ApiContext = React.createContext<ApiContext>({
|
const ApiContext = React.createContext<SessionApiContext>({
|
||||||
createAccount: async () => {},
|
createAccount: async () => {},
|
||||||
login: async () => {},
|
login: async () => {},
|
||||||
logout: async () => {},
|
logout: async () => {},
|
||||||
|
@ -114,71 +55,10 @@ const ApiContext = React.createContext<ApiContext>({
|
||||||
clearCurrentAccount: () => {},
|
clearCurrentAccount: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
function createPersistSessionHandler(
|
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||||
agent: BskyAgent,
|
|
||||||
account: SessionAccount,
|
|
||||||
persistSessionCallback: (props: {
|
|
||||||
expired: boolean
|
|
||||||
refreshedAccount: SessionAccount
|
|
||||||
}) => void,
|
|
||||||
{
|
|
||||||
networkErrorCallback,
|
|
||||||
}: {
|
|
||||||
networkErrorCallback?: () => void
|
|
||||||
} = {},
|
|
||||||
): AtpPersistSessionHandler {
|
|
||||||
return function persistSession(event, session) {
|
|
||||||
const expired = event === 'expired' || event === 'create-failed'
|
|
||||||
|
|
||||||
if (event === 'network-error') {
|
function __getAgent() {
|
||||||
logger.warn(`session: persistSessionHandler received network-error event`)
|
return __globalAgent
|
||||||
networkErrorCallback?.()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshedAccount: SessionAccount = {
|
|
||||||
service: account.service,
|
|
||||||
did: session?.did || account.did,
|
|
||||||
handle: session?.handle || account.handle,
|
|
||||||
email: session?.email || account.email,
|
|
||||||
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
|
||||||
emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor,
|
|
||||||
deactivated: isSessionDeactivated(session?.accessJwt),
|
|
||||||
pdsUrl: agent.pdsUrl?.toString(),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Tokens are undefined if the session expires, or if creation fails for
|
|
||||||
* any reason e.g. tokens are invalid, network error, etc.
|
|
||||||
*/
|
|
||||||
refreshJwt: session?.refreshJwt,
|
|
||||||
accessJwt: session?.accessJwt,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`session: persistSession`, {
|
|
||||||
event,
|
|
||||||
deactivated: refreshedAccount.deactivated,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (expired) {
|
|
||||||
logger.warn(`session: expired`)
|
|
||||||
emitSessionDropped()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If the session expired, or it was successfully created/updated, we want
|
|
||||||
* to update/persist the data.
|
|
||||||
*
|
|
||||||
* If the session creation failed, it could be a network error, or it could
|
|
||||||
* be more serious like an invalid token(s). We can't differentiate, so in
|
|
||||||
* order to allow the user to get a fresh token (if they need it), we need
|
|
||||||
* to persist this data and wipe their tokens, effectively logging them
|
|
||||||
* out.
|
|
||||||
*/
|
|
||||||
persistSessionCallback({
|
|
||||||
expired,
|
|
||||||
refreshedAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
@ -214,13 +94,87 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const clearCurrentAccount = React.useCallback(() => {
|
const clearCurrentAccount = React.useCallback(() => {
|
||||||
logger.warn(`session: clear current account`)
|
logger.warn(`session: clear current account`)
|
||||||
__globalAgent = PUBLIC_BSKY_AGENT
|
__globalAgent = PUBLIC_BSKY_AGENT
|
||||||
|
configureModerationForGuest()
|
||||||
setStateAndPersist(s => ({
|
setStateAndPersist(s => ({
|
||||||
...s,
|
...s,
|
||||||
currentAccount: undefined,
|
currentAccount: undefined,
|
||||||
}))
|
}))
|
||||||
}, [setStateAndPersist])
|
}, [setStateAndPersist])
|
||||||
|
|
||||||
const createAccount = React.useCallback<ApiContext['createAccount']>(
|
const createPersistSessionHandler = React.useCallback(
|
||||||
|
(
|
||||||
|
agent: BskyAgent,
|
||||||
|
account: SessionAccount,
|
||||||
|
persistSessionCallback: (props: {
|
||||||
|
expired: boolean
|
||||||
|
refreshedAccount: SessionAccount
|
||||||
|
}) => void,
|
||||||
|
{
|
||||||
|
networkErrorCallback,
|
||||||
|
}: {
|
||||||
|
networkErrorCallback?: () => void
|
||||||
|
} = {},
|
||||||
|
): AtpPersistSessionHandler => {
|
||||||
|
return function persistSession(event, session) {
|
||||||
|
const expired = event === 'expired' || event === 'create-failed'
|
||||||
|
|
||||||
|
if (event === 'network-error') {
|
||||||
|
logger.warn(
|
||||||
|
`session: persistSessionHandler received network-error event`,
|
||||||
|
)
|
||||||
|
networkErrorCallback?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use agentToSessionAccount for this too.
|
||||||
|
const refreshedAccount: SessionAccount = {
|
||||||
|
service: account.service,
|
||||||
|
did: session?.did || account.did,
|
||||||
|
handle: session?.handle || account.handle,
|
||||||
|
email: session?.email || account.email,
|
||||||
|
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
||||||
|
emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor,
|
||||||
|
deactivated: isSessionDeactivated(session?.accessJwt),
|
||||||
|
pdsUrl: agent.pdsUrl?.toString(),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tokens are undefined if the session expires, or if creation fails for
|
||||||
|
* any reason e.g. tokens are invalid, network error, etc.
|
||||||
|
*/
|
||||||
|
refreshJwt: session?.refreshJwt,
|
||||||
|
accessJwt: session?.accessJwt,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`session: persistSession`, {
|
||||||
|
event,
|
||||||
|
deactivated: refreshedAccount.deactivated,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
|
logger.warn(`session: expired`)
|
||||||
|
emitSessionDropped()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the session expired, or it was successfully created/updated, we want
|
||||||
|
* to update/persist the data.
|
||||||
|
*
|
||||||
|
* If the session creation failed, it could be a network error, or it could
|
||||||
|
* be more serious like an invalid token(s). We can't differentiate, so in
|
||||||
|
* order to allow the user to get a fresh token (if they need it), we need
|
||||||
|
* to persist this data and wipe their tokens, effectively logging them
|
||||||
|
* out.
|
||||||
|
*/
|
||||||
|
persistSessionCallback({
|
||||||
|
expired,
|
||||||
|
refreshedAccount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
|
||||||
async ({
|
async ({
|
||||||
service,
|
service,
|
||||||
email,
|
email,
|
||||||
|
@ -229,60 +183,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
verificationPhone,
|
verificationPhone,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
}: any) => {
|
}) => {
|
||||||
logger.info(`session: creating account`)
|
logger.info(`session: creating account`)
|
||||||
track('Try Create Account')
|
track('Try Create Account')
|
||||||
logEvent('account:create:begin', {})
|
logEvent('account:create:begin', {})
|
||||||
|
const {agent, account, fetchingGates} = await createAgentAndCreateAccount(
|
||||||
const agent = new BskyAgent({service})
|
{
|
||||||
|
service,
|
||||||
await agent.createAccount({
|
email,
|
||||||
handle,
|
password,
|
||||||
password,
|
handle,
|
||||||
email,
|
inviteCode,
|
||||||
inviteCode,
|
verificationPhone,
|
||||||
verificationPhone,
|
verificationCode,
|
||||||
verificationCode,
|
},
|
||||||
})
|
|
||||||
|
|
||||||
if (!agent.session) {
|
|
||||||
throw new Error(`session: createAccount failed to establish a session`)
|
|
||||||
}
|
|
||||||
const fetchingGates = tryFetchGates(
|
|
||||||
agent.session.did,
|
|
||||||
'prefer-fresh-gates',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const deactivated = isSessionDeactivated(agent.session.accessJwt)
|
|
||||||
if (!deactivated) {
|
|
||||||
/*dont await*/ agent.upsertProfile(_existing => {
|
|
||||||
return {
|
|
||||||
displayName: '',
|
|
||||||
|
|
||||||
// HACKFIX
|
|
||||||
// creating a bunch of identical profile objects is breaking the relay
|
|
||||||
// tossing this unspecced field onto it to reduce the size of the problem
|
|
||||||
// -prf
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const account: SessionAccount = {
|
|
||||||
service: agent.service.toString(),
|
|
||||||
did: agent.session.did,
|
|
||||||
handle: agent.session.handle,
|
|
||||||
email: agent.session.email,
|
|
||||||
emailConfirmed: agent.session.emailConfirmed,
|
|
||||||
emailAuthFactor: agent.session.emailAuthFactor,
|
|
||||||
refreshJwt: agent.session.refreshJwt,
|
|
||||||
accessJwt: agent.session.accessJwt,
|
|
||||||
deactivated,
|
|
||||||
pdsUrl: agent.pdsUrl?.toString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
await configureModeration(agent, account)
|
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(
|
createPersistSessionHandler(
|
||||||
agent,
|
agent,
|
||||||
|
@ -302,39 +218,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
track('Create Account')
|
track('Create Account')
|
||||||
logEvent('account:create:success', {})
|
logEvent('account:create:success', {})
|
||||||
},
|
},
|
||||||
[upsertAccount, clearCurrentAccount],
|
[upsertAccount, clearCurrentAccount, createPersistSessionHandler],
|
||||||
)
|
)
|
||||||
|
|
||||||
const login = React.useCallback<ApiContext['login']>(
|
const login = React.useCallback<SessionApiContext['login']>(
|
||||||
async ({service, identifier, password, authFactorToken}, logContext) => {
|
async ({service, identifier, password, authFactorToken}, logContext) => {
|
||||||
logger.debug(`session: login`, {}, logger.DebugContext.session)
|
logger.debug(`session: login`, {}, logger.DebugContext.session)
|
||||||
|
const {agent, account, fetchingGates} = await createAgentAndLogin({
|
||||||
const agent = new BskyAgent({service})
|
service,
|
||||||
|
identifier,
|
||||||
await agent.login({identifier, password, authFactorToken})
|
password,
|
||||||
|
authFactorToken,
|
||||||
if (!agent.session) {
|
})
|
||||||
throw new Error(`session: login failed to establish a session`)
|
|
||||||
}
|
|
||||||
const fetchingGates = tryFetchGates(
|
|
||||||
agent.session.did,
|
|
||||||
'prefer-fresh-gates',
|
|
||||||
)
|
|
||||||
|
|
||||||
const account: SessionAccount = {
|
|
||||||
service: agent.service.toString(),
|
|
||||||
did: agent.session.did,
|
|
||||||
handle: agent.session.handle,
|
|
||||||
email: agent.session.email,
|
|
||||||
emailConfirmed: agent.session.emailConfirmed,
|
|
||||||
emailAuthFactor: agent.session.emailAuthFactor,
|
|
||||||
refreshJwt: agent.session.refreshJwt,
|
|
||||||
accessJwt: agent.session.accessJwt,
|
|
||||||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
|
||||||
pdsUrl: agent.pdsUrl?.toString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
await configureModeration(agent, account)
|
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(
|
createPersistSessionHandler(
|
||||||
|
@ -358,10 +253,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
track('Sign In', {resumedSession: false})
|
track('Sign In', {resumedSession: false})
|
||||||
logEvent('account:loggedIn', {logContext, withPassword: true})
|
logEvent('account:loggedIn', {logContext, withPassword: true})
|
||||||
},
|
},
|
||||||
[upsertAccount, clearCurrentAccount],
|
[upsertAccount, clearCurrentAccount, createPersistSessionHandler],
|
||||||
)
|
)
|
||||||
|
|
||||||
const logout = React.useCallback<ApiContext['logout']>(
|
const logout = React.useCallback<SessionApiContext['logout']>(
|
||||||
async logContext => {
|
async logContext => {
|
||||||
logger.debug(`session: logout`)
|
logger.debug(`session: logout`)
|
||||||
clearCurrentAccount()
|
clearCurrentAccount()
|
||||||
|
@ -380,7 +275,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
[clearCurrentAccount, setStateAndPersist],
|
[clearCurrentAccount, setStateAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
const initSession = React.useCallback<ApiContext['initSession']>(
|
const initSession = React.useCallback<SessionApiContext['initSession']>(
|
||||||
async account => {
|
async account => {
|
||||||
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
||||||
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
||||||
|
@ -405,22 +300,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (IS_DEV && isWeb) window.agent = agent
|
if (IS_DEV && isWeb) window.agent = agent
|
||||||
await configureModeration(agent, account)
|
await configureModerationForAccount(agent, account)
|
||||||
|
|
||||||
let canReusePrevSession = false
|
|
||||||
try {
|
|
||||||
if (account.accessJwt) {
|
|
||||||
const decoded = jwtDecode(account.accessJwt)
|
|
||||||
if (decoded.exp) {
|
|
||||||
const didExpire = Date.now() >= decoded.exp * 1000
|
|
||||||
if (!didExpire) {
|
|
||||||
canReusePrevSession = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`session: could not decode jwt`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountOrSessionDeactivated =
|
const accountOrSessionDeactivated =
|
||||||
isSessionDeactivated(account.accessJwt) || account.deactivated
|
isSessionDeactivated(account.accessJwt) || account.deactivated
|
||||||
|
@ -432,7 +312,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
handle: account.handle,
|
handle: account.handle,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReusePrevSession) {
|
if (isSessionExpired(account)) {
|
||||||
|
logger.debug(`session: attempting to resume using previous session`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const freshAccount = await resumeSessionWithFreshAccount()
|
||||||
|
__globalAgent = agent
|
||||||
|
await fetchingGates
|
||||||
|
upsertAccount(freshAccount)
|
||||||
|
} catch (e) {
|
||||||
|
/*
|
||||||
|
* Note: `agent.persistSession` is also called when this fails, and
|
||||||
|
* we handle that failure via `createPersistSessionHandler`
|
||||||
|
*/
|
||||||
|
logger.info(`session: resumeSessionWithFreshAccount failed`, {
|
||||||
|
message: e,
|
||||||
|
})
|
||||||
|
|
||||||
|
__globalAgent = PUBLIC_BSKY_AGENT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
logger.debug(`session: attempting to reuse previous session`)
|
logger.debug(`session: attempting to reuse previous session`)
|
||||||
|
|
||||||
agent.session = prevSession
|
agent.session = prevSession
|
||||||
|
@ -469,59 +368,27 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
|
||||||
__globalAgent = PUBLIC_BSKY_AGENT
|
__globalAgent = PUBLIC_BSKY_AGENT
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
logger.debug(`session: attempting to resume using previous session`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const freshAccount = await resumeSessionWithFreshAccount()
|
|
||||||
__globalAgent = agent
|
|
||||||
await fetchingGates
|
|
||||||
upsertAccount(freshAccount)
|
|
||||||
} catch (e) {
|
|
||||||
/*
|
|
||||||
* Note: `agent.persistSession` is also called when this fails, and
|
|
||||||
* we handle that failure via `createPersistSessionHandler`
|
|
||||||
*/
|
|
||||||
logger.info(`session: resumeSessionWithFreshAccount failed`, {
|
|
||||||
message: e,
|
|
||||||
})
|
|
||||||
|
|
||||||
__globalAgent = PUBLIC_BSKY_AGENT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
|
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
|
||||||
logger.debug(`session: resumeSessionWithFreshAccount`)
|
logger.debug(`session: resumeSessionWithFreshAccount`)
|
||||||
|
|
||||||
await networkRetry(1, () => agent.resumeSession(prevSession))
|
await networkRetry(1, () => agent.resumeSession(prevSession))
|
||||||
|
const sessionAccount = agentToSessionAccount(agent)
|
||||||
/*
|
/*
|
||||||
* If `agent.resumeSession` fails above, it'll throw. This is just to
|
* If `agent.resumeSession` fails above, it'll throw. This is just to
|
||||||
* make TypeScript happy.
|
* make TypeScript happy.
|
||||||
*/
|
*/
|
||||||
if (!agent.session) {
|
if (!sessionAccount) {
|
||||||
throw new Error(`session: initSession failed to establish a session`)
|
throw new Error(`session: initSession failed to establish a session`)
|
||||||
}
|
}
|
||||||
|
return sessionAccount
|
||||||
// ensure changes in handle/email etc are captured on reload
|
|
||||||
return {
|
|
||||||
service: agent.service.toString(),
|
|
||||||
did: agent.session.did,
|
|
||||||
handle: agent.session.handle,
|
|
||||||
email: agent.session.email,
|
|
||||||
emailConfirmed: agent.session.emailConfirmed,
|
|
||||||
emailAuthFactor: agent.session.emailAuthFactor,
|
|
||||||
refreshJwt: agent.session.refreshJwt,
|
|
||||||
accessJwt: agent.session.accessJwt,
|
|
||||||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
|
||||||
pdsUrl: agent.pdsUrl?.toString(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[upsertAccount, clearCurrentAccount],
|
[upsertAccount, clearCurrentAccount, createPersistSessionHandler],
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
|
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
|
||||||
async account => {
|
async account => {
|
||||||
try {
|
try {
|
||||||
if (account) {
|
if (account) {
|
||||||
|
@ -539,7 +406,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
[initSession],
|
[initSession],
|
||||||
)
|
)
|
||||||
|
|
||||||
const removeAccount = React.useCallback<ApiContext['removeAccount']>(
|
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
||||||
account => {
|
account => {
|
||||||
setStateAndPersist(s => {
|
setStateAndPersist(s => {
|
||||||
return {
|
return {
|
||||||
|
@ -552,7 +419,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateCurrentAccount = React.useCallback<
|
const updateCurrentAccount = React.useCallback<
|
||||||
ApiContext['updateCurrentAccount']
|
SessionApiContext['updateCurrentAccount']
|
||||||
>(
|
>(
|
||||||
account => {
|
account => {
|
||||||
setStateAndPersist(s => {
|
setStateAndPersist(s => {
|
||||||
|
@ -588,7 +455,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
[setStateAndPersist],
|
[setStateAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectAccount = React.useCallback<ApiContext['selectAccount']>(
|
const selectAccount = React.useCallback<SessionApiContext['selectAccount']>(
|
||||||
async (account, logContext) => {
|
async (account, logContext) => {
|
||||||
setState(s => ({...s, isSwitchingAccounts: true}))
|
setState(s => ({...s, isSwitchingAccounts: true}))
|
||||||
try {
|
try {
|
||||||
|
@ -714,28 +581,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureModeration(agent: BskyAgent, account: SessionAccount) {
|
|
||||||
if (IS_TEST_USER(account.handle)) {
|
|
||||||
const did = (
|
|
||||||
await agent
|
|
||||||
.resolveHandle({handle: 'mod-authority.test'})
|
|
||||||
.catch(_ => undefined)
|
|
||||||
)?.data.did
|
|
||||||
if (did) {
|
|
||||||
console.warn('USING TEST ENV MODERATION')
|
|
||||||
BskyAgent.configure({appLabelers: [did]})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
|
|
||||||
const labelerDids = await readLabelers(account.did).catch(_ => {})
|
|
||||||
if (labelerDids) {
|
|
||||||
agent.configureLabelersHeader(
|
|
||||||
labelerDids.filter(did => did !== BSKY_LABELER_DID),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSession() {
|
export function useSession() {
|
||||||
return React.useContext(StateContext)
|
return React.useContext(StateContext)
|
||||||
}
|
}
|
||||||
|
@ -762,12 +607,6 @@ export function useRequireAuth() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSessionDeactivated(accessJwt: string | undefined) {
|
export function useAgent() {
|
||||||
if (accessJwt) {
|
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
||||||
const sessData = jwtDecode(accessJwt)
|
|
||||||
return (
|
|
||||||
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {LogEvents} from '#/lib/statsig/statsig'
|
||||||
|
import {PersistedAccount} from '#/state/persisted'
|
||||||
|
|
||||||
|
export type SessionAccount = PersistedAccount
|
||||||
|
|
||||||
|
export type SessionState = {
|
||||||
|
isInitialLoad: boolean
|
||||||
|
isSwitchingAccounts: boolean
|
||||||
|
accounts: SessionAccount[]
|
||||||
|
currentAccount: SessionAccount | undefined
|
||||||
|
}
|
||||||
|
export type SessionStateContext = SessionState & {
|
||||||
|
hasSession: boolean
|
||||||
|
}
|
||||||
|
export type SessionApiContext = {
|
||||||
|
createAccount: (props: {
|
||||||
|
service: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
handle: string
|
||||||
|
inviteCode?: string
|
||||||
|
verificationPhone?: string
|
||||||
|
verificationCode?: string
|
||||||
|
}) => Promise<void>
|
||||||
|
login: (
|
||||||
|
props: {
|
||||||
|
service: string
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
authFactorToken?: string | undefined
|
||||||
|
},
|
||||||
|
logContext: LogEvents['account:loggedIn']['logContext'],
|
||||||
|
) => Promise<void>
|
||||||
|
/**
|
||||||
|
* A full logout. Clears the `currentAccount` from session, AND removes
|
||||||
|
* access tokens from all accounts, so that returning as any user will
|
||||||
|
* require a full login.
|
||||||
|
*/
|
||||||
|
logout: (
|
||||||
|
logContext: LogEvents['account:loggedOut']['logContext'],
|
||||||
|
) => Promise<void>
|
||||||
|
/**
|
||||||
|
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
|
||||||
|
* clear access tokens from accounts, allowing the user to return to their
|
||||||
|
* other accounts without logging in.
|
||||||
|
*
|
||||||
|
* Used when adding a new account, deleting an account.
|
||||||
|
*/
|
||||||
|
clearCurrentAccount: () => void
|
||||||
|
initSession: (account: SessionAccount) => Promise<void>
|
||||||
|
resumeSession: (account?: SessionAccount) => Promise<void>
|
||||||
|
removeAccount: (account: SessionAccount) => void
|
||||||
|
selectAccount: (
|
||||||
|
account: SessionAccount,
|
||||||
|
logContext: LogEvents['account:loggedIn']['logContext'],
|
||||||
|
) => Promise<void>
|
||||||
|
updateCurrentAccount: (
|
||||||
|
account: Partial<
|
||||||
|
Pick<
|
||||||
|
SessionAccount,
|
||||||
|
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
) => void
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
|
||||||
|
import {jwtDecode} from 'jwt-decode'
|
||||||
|
|
||||||
|
import {IS_TEST_USER} from '#/lib/constants'
|
||||||
|
import {tryFetchGates} from '#/lib/statsig/statsig'
|
||||||
|
import {hasProp} from '#/lib/type-guards'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
import {readLabelers} from '../agent-config'
|
||||||
|
import {SessionAccount, SessionApiContext} from '../types'
|
||||||
|
|
||||||
|
export function isSessionDeactivated(accessJwt: string | undefined) {
|
||||||
|
if (accessJwt) {
|
||||||
|
const sessData = jwtDecode(accessJwt)
|
||||||
|
return (
|
||||||
|
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readLastActiveAccount() {
|
||||||
|
const {currentAccount, accounts} = persisted.get('session')
|
||||||
|
return accounts.find(a => a.did === currentAccount?.did)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function agentToSessionAccount(
|
||||||
|
agent: BskyAgent,
|
||||||
|
): SessionAccount | undefined {
|
||||||
|
if (!agent.session) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
service: agent.service.toString(),
|
||||||
|
did: agent.session.did,
|
||||||
|
handle: agent.session.handle,
|
||||||
|
email: agent.session.email,
|
||||||
|
emailConfirmed: agent.session.emailConfirmed || false,
|
||||||
|
emailAuthFactor: agent.session.emailAuthFactor || false,
|
||||||
|
refreshJwt: agent.session.refreshJwt,
|
||||||
|
accessJwt: agent.session.accessJwt,
|
||||||
|
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||||
|
pdsUrl: agent.pdsUrl?.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureModerationForGuest() {
|
||||||
|
switchToBskyAppLabeler()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function configureModerationForAccount(
|
||||||
|
agent: BskyAgent,
|
||||||
|
account: SessionAccount,
|
||||||
|
) {
|
||||||
|
switchToBskyAppLabeler()
|
||||||
|
if (IS_TEST_USER(account.handle)) {
|
||||||
|
await trySwitchToTestAppLabeler(agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelerDids = await readLabelers(account.did).catch(_ => {})
|
||||||
|
if (labelerDids) {
|
||||||
|
agent.configureLabelersHeader(
|
||||||
|
labelerDids.filter(did => did !== BSKY_LABELER_DID),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// If there are no headers in the storage, we'll not send them on the initial requests.
|
||||||
|
// If we wanted to fix this, we could block on the preferences query here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToBskyAppLabeler() {
|
||||||
|
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trySwitchToTestAppLabeler(agent: BskyAgent) {
|
||||||
|
const did = (
|
||||||
|
await agent
|
||||||
|
.resolveHandle({handle: 'mod-authority.test'})
|
||||||
|
.catch(_ => undefined)
|
||||||
|
)?.data.did
|
||||||
|
if (did) {
|
||||||
|
console.warn('USING TEST ENV MODERATION')
|
||||||
|
BskyAgent.configure({appLabelers: [did]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSessionExpired(account: SessionAccount) {
|
||||||
|
try {
|
||||||
|
if (account.accessJwt) {
|
||||||
|
const decoded = jwtDecode(account.accessJwt)
|
||||||
|
if (decoded.exp) {
|
||||||
|
const didExpire = Date.now() >= decoded.exp * 1000
|
||||||
|
return didExpire
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`session: could not decode jwt`)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAgentAndLogin({
|
||||||
|
service,
|
||||||
|
identifier,
|
||||||
|
password,
|
||||||
|
authFactorToken,
|
||||||
|
}: {
|
||||||
|
service: string
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
authFactorToken?: string
|
||||||
|
}) {
|
||||||
|
const agent = new BskyAgent({service})
|
||||||
|
await agent.login({identifier, password, authFactorToken})
|
||||||
|
|
||||||
|
const account = agentToSessionAccount(agent)
|
||||||
|
if (!agent.session || !account) {
|
||||||
|
throw new Error(`session: login failed to establish a session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||||
|
await configureModerationForAccount(agent, account)
|
||||||
|
|
||||||
|
return {
|
||||||
|
agent,
|
||||||
|
account,
|
||||||
|
fetchingGates,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAgentAndCreateAccount({
|
||||||
|
service,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
handle,
|
||||||
|
inviteCode,
|
||||||
|
verificationPhone,
|
||||||
|
verificationCode,
|
||||||
|
}: Parameters<SessionApiContext['createAccount']>[0]) {
|
||||||
|
const agent = new BskyAgent({service})
|
||||||
|
await agent.createAccount({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
handle,
|
||||||
|
inviteCode,
|
||||||
|
verificationPhone,
|
||||||
|
verificationCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const account = agentToSessionAccount(agent)!
|
||||||
|
if (!agent.session || !account) {
|
||||||
|
throw new Error(`session: createAccount failed to establish a session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||||
|
|
||||||
|
if (!account.deactivated) {
|
||||||
|
/*dont await*/ agent.upsertProfile(_existing => {
|
||||||
|
return {
|
||||||
|
displayName: '',
|
||||||
|
|
||||||
|
// HACKFIX
|
||||||
|
// creating a bunch of identical profile objects is breaking the relay
|
||||||
|
// tossing this unspecced field onto it to reduce the size of the problem
|
||||||
|
// -prf
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await configureModerationForAccount(agent, account)
|
||||||
|
|
||||||
|
return {
|
||||||
|
agent,
|
||||||
|
account,
|
||||||
|
fetchingGates,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
|
|
||||||
export function readLastActiveAccount() {
|
|
||||||
const {currentAccount, accounts} = persisted.get('session')
|
|
||||||
return accounts.find(a => a.did === currentAccount?.did)
|
|
||||||
}
|
|
Loading…
Reference in New Issue