Skip to content

Commit

Permalink
feat: add a logout function (#595)
Browse files Browse the repository at this point in the history
This resets the store, deleting the local agent's private keys and saved
delegations. This is better than the previous implementation, and is
appropriate for use cases like using console on a public or shared
computer.

This PR also has a small tweak to replace JSX.Element with ReactNode,
which I've found works better in downstream applications.
  • Loading branch information
travis authored Nov 30, 2023
1 parent 86f971a commit 0995fd5
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@ucanto/interface": "^9.0.0",
"@ucanto/principal": "^9.0.0",
"@ucanto/transport": "^9.0.0",
"@web3-storage/access": "^18.0.3",
"@web3-storage/access": "^18.0.5",
"@web3-storage/did-mailto": "^2.0.2",
"@web3-storage/w3up-client": "^11.2.0"
},
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { Client, create as createW3UPClient } from '@web3-storage/w3up-client'
import { Account } from '@web3-storage/w3up-client/account'
import { Space } from '@web3-storage/w3up-client/space'
import { createServiceConf } from './service'
import { Driver } from '@web3-storage/access/drivers/types'

export * from '@web3-storage/w3up-client/types'
export { Client, Account, Space, ServiceConfig }
export type Store = Driver<AgentDataExport>

const DB_NAME = '@w3ui'
const DB_STORE_NAME = 'core'
Expand All @@ -29,7 +31,12 @@ export interface ContextState {
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ContextActions {}
export interface ContextActions {
/**
* Reset local store (deleting existing agent), logging the user out.
*/
logout: () => Promise<void>
}

export interface CreateClientOptions extends ServiceConfig {
events?: EventTarget
Expand Down Expand Up @@ -59,11 +66,11 @@ class IndexedDBEventDispatcherStore extends StoreIndexedDB {
*/
export async function createClient (
options?: CreateClientOptions
): Promise<{ client: Client, events: EventTarget }> {
): Promise<{ client: Client, events: EventTarget, store: Store }> {
const dbName = `${DB_NAME}${options?.servicePrincipal != null ? '@' + options?.servicePrincipal.did() : ''}`
const events = options?.events ?? new EventTarget()
const store = new IndexedDBEventDispatcherStore(dbName, events)
const serviceConf = createServiceConf(options)
const client = await createW3UPClient({ store, serviceConf })
return { client, events }
return { client, events, store }
}
4 changes: 4 additions & 0 deletions packages/react/src/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const AuthenticatorContextDefaultValue: AuthenticatorContextValue = [
},
cancelLogin: () => {
throw new Error('missing cancel login function')
},
logout: () => {
throw new Error('missing logout function')
}
}
]
Expand Down Expand Up @@ -112,6 +115,7 @@ export const AuthenticatorRoot: Component<AuthenticatorRootProps> =
() => [
{ ...state, email, submitted, handleRegisterSubmit },
{
...actions,
setEmail,
cancelLogin: () => {
loginAbortController?.abort()
Expand Down
46 changes: 30 additions & 16 deletions packages/react/src/providers/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
Account
} from '@w3ui/core'

import React, { createContext, useState, useContext, useEffect } from 'react'
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'
import { createClient } from '@w3ui/core'

export { ContextState, ContextActions }
Expand All @@ -23,15 +23,19 @@ export const ContextDefaultValue: ContextValue = [
accounts: [],
spaces: []
},
{}
{
logout: async () => {
throw new Error('missing logout function')
}
}
]

export const Context = createContext<ContextValue>(
ContextDefaultValue
)

export interface ProviderProps extends ServiceConfig {
children?: JSX.Element
children?: ReactNode
}

/**
Expand All @@ -41,7 +45,7 @@ export function Provider ({
children,
servicePrincipal,
connection
}: ProviderProps): JSX.Element {
}: ProviderProps): ReactNode {
const [client, setClient] = useState<Client>()
const [events, setEvents] = useState<EventTarget>()
const [accounts, setAccounts] = useState<Account[]>([])
Expand All @@ -61,22 +65,32 @@ export function Provider ({
}
}, [client, events])

const getClient = async (): Promise<Client> => {
if (client == null) {
const { client, events } = await createClient({ servicePrincipal, connection })
setClient(client)
setEvents(events)
setAccounts(Object.values(client.accounts()))
setSpaces(client.spaces())
return client
}
return client
const setupClient = async (): Promise<void> => {
const { client, events } = await createClient({ servicePrincipal, connection })
setClient(client)
setEvents(events)
setAccounts(Object.values(client.accounts()))
setSpaces(client.spaces())
}

const logout = async (): Promise<void> => {
// it's possible that setupClient hasn't been run yet - run createClient here
// to get a reliable handle on the latest store
const { store } = await createClient({ servicePrincipal, connection })
await store.reset()
// set state back to defaults
setClient(undefined)
setEvents(undefined)
setAccounts([])
setSpaces([])
// set state up again
await setupClient()
}

useEffect(() => { void getClient() }, []) // load client - once.
useEffect(() => { void setupClient() }, []) // load client - once.

return (
<Context.Provider value={[{ client, accounts, spaces }, {}]}>
<Context.Provider value={[{ client, accounts, spaces }, { logout }]}>
{children}
</Context.Provider>
)
Expand Down
27 changes: 2 additions & 25 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0995fd5

Please sign in to comment.