diff options
Diffstat (limited to 'src/state/models/session.ts')
-rw-r--r-- | src/state/models/session.ts | 472 |
1 files changed, 0 insertions, 472 deletions
diff --git a/src/state/models/session.ts b/src/state/models/session.ts deleted file mode 100644 index 5b95c7d32..000000000 --- a/src/state/models/session.ts +++ /dev/null @@ -1,472 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import { - BskyAgent, - AtpSessionEvent, - AtpSessionData, - ComAtprotoServerDescribeServer as DescribeServer, -} from '@atproto/api' -import normalizeUrl from 'normalize-url' -import {isObj, hasProp} from 'lib/type-guards' -import {networkRetry} from 'lib/async/retry' -import {z} from 'zod' -import {RootStoreModel} from './root-store' -import {IS_PROD} from 'lib/constants' -import {track} from 'lib/analytics/analytics' -import {logger} from '#/logger' - -export type ServiceDescription = DescribeServer.OutputSchema - -export const activeSession = z.object({ - service: z.string(), - did: z.string(), -}) -export type ActiveSession = z.infer<typeof activeSession> - -export const accountData = z.object({ - service: z.string(), - refreshJwt: z.string().optional(), - accessJwt: z.string().optional(), - handle: z.string(), - did: z.string(), - email: z.string().optional(), - displayName: z.string().optional(), - aviUrl: z.string().optional(), - emailConfirmed: z.boolean().optional(), -}) -export type AccountData = z.infer<typeof accountData> - -interface AdditionalAccountData { - displayName?: string - aviUrl?: string -} - -export class SessionModel { - // DEBUG - // emergency log facility to help us track down this logout issue - // remove when resolved - // -prf - _log(message: string, details?: Record<string, any>) { - details = details || {} - details.state = { - data: this.data, - accounts: this.accounts.map( - a => - `${!!a.accessJwt && !!a.refreshJwt ? '✅' : '❌'} ${a.handle} (${ - a.service - })`, - ), - isResumingSession: this.isResumingSession, - } - logger.debug(message, details, logger.DebugContext.session) - } - - /** - * Currently-active session - */ - data: ActiveSession | null = null - /** - * A listing of the currently & previous sessions - */ - accounts: AccountData[] = [] - /** - * Flag to indicate if we're doing our initial-load session resumption - */ - isResumingSession = false - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, { - rootStore: false, - serialize: false, - hydrate: false, - hasSession: false, - }) - } - - get currentSession() { - if (!this.data) { - return undefined - } - const {did, service} = this.data - return this.accounts.find( - account => - normalizeUrl(account.service) === normalizeUrl(service) && - account.did === did && - !!account.accessJwt && - !!account.refreshJwt, - ) - } - - get hasSession() { - return !!this.currentSession && !!this.rootStore.agent.session - } - - get hasAccounts() { - return this.accounts.length >= 1 - } - - get switchableAccounts() { - return this.accounts.filter(acct => acct.did !== this.data?.did) - } - - get emailNeedsConfirmation() { - return !this.currentSession?.emailConfirmed - } - - get isSandbox() { - if (!this.data) { - return false - } - return !IS_PROD(this.data.service) - } - - serialize(): unknown { - return { - data: this.data, - accounts: this.accounts, - } - } - - hydrate(v: unknown) { - this.accounts = [] - if (isObj(v)) { - if (hasProp(v, 'data') && activeSession.safeParse(v.data)) { - this.data = v.data as ActiveSession - } - if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) { - for (const account of v.accounts) { - if (accountData.safeParse(account)) { - this.accounts.push(account as AccountData) - } - } - } - } - } - - clear() { - this.data = null - } - - /** - * Attempts to resume the previous session loaded from storage - */ - async attemptSessionResumption() { - const sess = this.currentSession - if (sess) { - this._log('SessionModel:attemptSessionResumption found stored session') - this.isResumingSession = true - try { - return await this.resumeSession(sess) - } finally { - runInAction(() => { - this.isResumingSession = false - }) - } - } else { - this._log( - 'SessionModel:attemptSessionResumption has no session to resume', - ) - } - } - - /** - * Sets the active session - */ - async setActiveSession(agent: BskyAgent, did: string) { - this._log('SessionModel:setActiveSession') - const hadSession = !!this.data - this.data = { - service: agent.service.toString(), - did, - } - await this.rootStore.handleSessionChange(agent, {hadSession}) - } - - /** - * Upserts a session into the accounts - */ - persistSession( - service: string, - did: string, - event: AtpSessionEvent, - session?: AtpSessionData, - addedInfo?: AdditionalAccountData, - ) { - this._log('SessionModel:persistSession', { - service, - did, - event, - hasSession: !!session, - }) - - const existingAccount = this.accounts.find( - account => account.service === service && account.did === did, - ) - - // fall back to any preexisting access tokens - let refreshJwt = session?.refreshJwt || existingAccount?.refreshJwt - let accessJwt = session?.accessJwt || existingAccount?.accessJwt - if (event === 'expired') { - // only clear the tokens when they're known to have expired - refreshJwt = undefined - accessJwt = undefined - } - - const newAccount = { - service, - did, - refreshJwt, - accessJwt, - - handle: session?.handle || existingAccount?.handle || '', - email: session?.email || existingAccount?.email || '', - displayName: addedInfo - ? addedInfo.displayName - : existingAccount?.displayName || '', - aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '', - emailConfirmed: session?.emailConfirmed, - } - if (!existingAccount) { - this.accounts.push(newAccount) - } else { - this.accounts = [ - newAccount, - ...this.accounts.filter( - account => !(account.service === service && account.did === did), - ), - ] - } - - // if the session expired, fire an event to let the user know - if (event === 'expired') { - this.rootStore.handleSessionDrop() - } - } - - /** - * Clears any session tokens from the accounts; used on logout. - */ - clearSessionTokens() { - this._log('SessionModel:clearSessionTokens') - this.accounts = this.accounts.map(acct => ({ - service: acct.service, - handle: acct.handle, - did: acct.did, - displayName: acct.displayName, - aviUrl: acct.aviUrl, - email: acct.email, - emailConfirmed: acct.emailConfirmed, - })) - } - - /** - * Fetches additional information about an account on load. - */ - async loadAccountInfo(agent: BskyAgent, did: string) { - const res = await agent.getProfile({actor: did}).catch(_e => undefined) - if (res) { - return { - displayName: res.data.displayName, - aviUrl: res.data.avatar, - } - } - } - - /** - * Helper to fetch the accounts config settings from an account. - */ - async describeService(service: string): Promise<ServiceDescription> { - const agent = new BskyAgent({service}) - const res = await agent.com.atproto.server.describeServer({}) - return res.data - } - - /** - * Attempt to resume a session that we still have access tokens for. - */ - async resumeSession(account: AccountData): Promise<boolean> { - this._log('SessionModel:resumeSession') - if (!(account.accessJwt && account.refreshJwt && account.service)) { - this._log( - 'SessionModel:resumeSession aborted due to lack of access tokens', - ) - return false - } - - const agent = new BskyAgent({ - service: account.service, - persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(account.service, account.did, evt, sess) - }, - }) - - try { - await networkRetry(3, () => - agent.resumeSession({ - accessJwt: account.accessJwt || '', - refreshJwt: account.refreshJwt || '', - did: account.did, - handle: account.handle, - email: account.email, - emailConfirmed: account.emailConfirmed, - }), - ) - const addedInfo = await this.loadAccountInfo(agent, account.did) - this.persistSession( - account.service, - account.did, - 'create', - agent.session, - addedInfo, - ) - this._log('SessionModel:resumeSession succeeded') - } catch (e: any) { - this._log('SessionModel:resumeSession failed', { - error: e.toString(), - }) - return false - } - - await this.setActiveSession(agent, account.did) - return true - } - - /** - * Create a new session. - */ - async login({ - service, - identifier, - password, - }: { - service: string - identifier: string - password: string - }) { - this._log('SessionModel:login') - const agent = new BskyAgent({service}) - await agent.login({identifier, password}) - if (!agent.session) { - throw new Error('Failed to establish session') - } - - const did = agent.session.did - const addedInfo = await this.loadAccountInfo(agent, did) - - this.persistSession(service, did, 'create', agent.session, addedInfo) - agent.setPersistSessionHandler( - (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(service, did, evt, sess) - }, - ) - - await this.setActiveSession(agent, did) - this._log('SessionModel:login succeeded') - } - - async createAccount({ - service, - email, - password, - handle, - inviteCode, - }: { - service: string - email: string - password: string - handle: string - inviteCode?: string - }) { - this._log('SessionModel:createAccount') - const agent = new BskyAgent({service}) - await agent.createAccount({ - handle, - password, - email, - inviteCode, - }) - if (!agent.session) { - throw new Error('Failed to establish session') - } - - const did = agent.session.did - const addedInfo = await this.loadAccountInfo(agent, did) - - this.persistSession(service, did, 'create', agent.session, addedInfo) - agent.setPersistSessionHandler( - (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(service, did, evt, sess) - }, - ) - - await this.setActiveSession(agent, did) - this._log('SessionModel:createAccount succeeded') - track('Create Account Successfully') - } - - /** - * Close all sessions across all accounts. - */ - async logout() { - this._log('SessionModel:logout') - // TODO - // need to evaluate why deleting the session has caused errors at times - // -prf - /*if (this.hasSession) { - this.rootStore.agent.com.atproto.session.delete().catch((e: any) => { - this.rootStore.log.warn( - '(Minor issue) Failed to delete session on the server', - e, - ) - }) - }*/ - this.clearSessionTokens() - this.rootStore.clearAllSessionState() - } - - /** - * Removes an account from the list of stored accounts. - */ - removeAccount(handle: string) { - this.accounts = this.accounts.filter(acc => acc.handle !== handle) - } - - /** - * Reloads the session from the server. Useful when account details change, like the handle. - */ - async reloadFromServer() { - const sess = this.currentSession - if (!sess) { - return - } - const res = await this.rootStore.agent - .getProfile({actor: sess.did}) - .catch(_e => undefined) - if (res?.success) { - const updated = { - ...sess, - handle: res.data.handle, - displayName: res.data.displayName, - aviUrl: res.data.avatar, - } - runInAction(() => { - this.accounts = [ - updated, - ...this.accounts.filter( - account => - !( - account.service === updated.service && - account.did === updated.did - ), - ), - ] - }) - await this.rootStore.me.load() - } - } - - updateLocalAccountData(changes: Partial<AccountData>) { - this.accounts = this.accounts.map(acct => - acct.did === this.data?.did ? {...acct, ...changes} : acct, - ) - } -} |