(WIP) Add initial API client
This commit is contained in:
parent
967f9fc474
commit
faddda83f0
9 changed files with 260 additions and 27 deletions
97
src/api/index.ts
Normal file
97
src/api/index.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
|
||||
import * as ucan from 'ucans'
|
||||
|
||||
export class API {
|
||||
userCfg?: UserConfig
|
||||
reader?: MicroblogReader
|
||||
writer?: MicroblogDelegator
|
||||
|
||||
setUserCfg(cfg: UserConfig) {
|
||||
this.userCfg = cfg
|
||||
this.createReader()
|
||||
this.createWriter()
|
||||
}
|
||||
|
||||
private createReader() {
|
||||
if (!this.userCfg?.serverUrl) {
|
||||
this.reader = undefined
|
||||
} else {
|
||||
this.reader = new MicroblogReader(
|
||||
this.userCfg.serverUrl,
|
||||
this.userCfg.did,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private createWriter() {
|
||||
if (
|
||||
this.userCfg?.serverUrl &&
|
||||
this.userCfg?.did &&
|
||||
this.userCfg?.keypair &&
|
||||
this.userCfg?.ucanStore
|
||||
) {
|
||||
this.writer = new MicroblogDelegator(
|
||||
this.userCfg.serverUrl,
|
||||
this.userCfg.did,
|
||||
this.userCfg.keypair,
|
||||
this.userCfg.ucanStore,
|
||||
)
|
||||
} else {
|
||||
this.writer = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SerializedUserConfig {
|
||||
serverUrl?: string
|
||||
secretKeyStr?: string
|
||||
rootAuthToken?: string
|
||||
}
|
||||
|
||||
export class UserConfig {
|
||||
serverUrl?: string
|
||||
did?: string
|
||||
keypair?: ucan.EdKeypair
|
||||
rootAuthToken?: string
|
||||
ucanStore?: ucan.Store
|
||||
|
||||
get hasWriteCaps() {
|
||||
return Boolean(this.did && this.keypair && this.ucanStore)
|
||||
}
|
||||
|
||||
static async createTest(serverUrl: string) {
|
||||
const cfg = new UserConfig()
|
||||
cfg.serverUrl = serverUrl
|
||||
cfg.keypair = await ucan.EdKeypair.create()
|
||||
cfg.did = cfg.keypair.did()
|
||||
cfg.rootAuthToken = (await auth.claimFull(cfg.did, cfg.keypair)).encoded()
|
||||
cfg.ucanStore = await ucan.Store.fromTokens([cfg.rootAuthToken])
|
||||
return cfg
|
||||
}
|
||||
|
||||
static async hydrate(state: SerializedUserConfig) {
|
||||
const cfg = new UserConfig()
|
||||
await cfg.hydrate(state)
|
||||
return cfg
|
||||
}
|
||||
|
||||
async serialize(): Promise<SerializedUserConfig> {
|
||||
return {
|
||||
serverUrl: this.serverUrl,
|
||||
secretKeyStr: this.keypair
|
||||
? await this.keypair.export('base64')
|
||||
: undefined,
|
||||
rootAuthToken: this.rootAuthToken,
|
||||
}
|
||||
}
|
||||
|
||||
async hydrate(state: SerializedUserConfig) {
|
||||
this.serverUrl = state.serverUrl
|
||||
if (state.secretKeyStr && state.rootAuthToken) {
|
||||
this.keypair = ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
|
||||
this.did = this.keypair.did()
|
||||
this.rootAuthToken = state.rootAuthToken
|
||||
this.ucanStore = await ucan.Store.fromTokens([this.rootAuthToken])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,34 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import {Text, Button, View, ActivityIndicator} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Shell} from '../platform/shell'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
import {useStores} from '../state'
|
||||
|
||||
export function Login({navigation}: RootTabsScreenProps<'Login'>) {
|
||||
export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
|
||||
const store = useStores()
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||
<Button title="Login" onPress={() => store.session.setAuthed(true)} />
|
||||
<Button title="Sign Up" onPress={() => navigation.navigate('Signup')} />
|
||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||
{store.session.uiState === 'idle' ? (
|
||||
<>
|
||||
{store.session.hasAccount ?? (
|
||||
<Button
|
||||
title="Login"
|
||||
onPress={() => store.session.loadAccount()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
title="Sign Up"
|
||||
onPress={() => navigation.navigate('Signup')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,24 +1,36 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View, ActivityIndicator} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
import {useStores} from '../state'
|
||||
|
||||
export function Signup({navigation}: RootTabsScreenProps<'Signup'>) {
|
||||
const store = useStores()
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||
<Button
|
||||
title="Create new account"
|
||||
onPress={() => store.session.setAuthed(true)}
|
||||
/>
|
||||
<Button
|
||||
title="Log in to an existing account"
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
/>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
export const Signup = observer(
|
||||
({navigation}: RootTabsScreenProps<'Signup'>) => {
|
||||
const store = useStores()
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||
{store.session.uiState === 'idle' ? (
|
||||
<>
|
||||
<Button
|
||||
title="Create new account"
|
||||
onPress={() =>
|
||||
store.session.createTestAccount('http://localhost:1986')
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
title="Log in to an existing account"
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
*/
|
||||
|
||||
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
|
||||
import {API} from '../api'
|
||||
|
||||
export class Environment {
|
||||
api = new API()
|
||||
|
||||
constructor() {}
|
||||
|
||||
async setup() {}
|
||||
|
|
|
@ -1,14 +1,69 @@
|
|||
import {Instance, SnapshotOut, types} from 'mobx-state-tree'
|
||||
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
|
||||
import {UserConfig} from '../../api'
|
||||
import {withEnvironment} from '../env'
|
||||
|
||||
export const SessionModel = types
|
||||
.model('Session')
|
||||
.props({
|
||||
isAuthed: types.boolean,
|
||||
uiState: types.enumeration('idle', ['idle', 'working']),
|
||||
uiError: types.maybe(types.string),
|
||||
|
||||
// TODO: these should be stored somewhere secret
|
||||
serverUrl: types.maybe(types.string),
|
||||
secretKeyStr: types.maybe(types.string),
|
||||
rootAuthToken: types.maybe(types.string),
|
||||
})
|
||||
.views(self => ({
|
||||
get hasAccount() {
|
||||
return self.serverUrl && self.secretKeyStr && self.rootAuthToken
|
||||
},
|
||||
}))
|
||||
.extend(withEnvironment)
|
||||
.actions(self => ({
|
||||
setAuthed: (v: boolean) => {
|
||||
self.isAuthed = v
|
||||
},
|
||||
loadAccount: flow(function* () {
|
||||
if (!self.hasAccount) {
|
||||
return false
|
||||
}
|
||||
self.uiState = 'working'
|
||||
self.uiError = undefined
|
||||
try {
|
||||
const cfg = yield UserConfig.hydrate({
|
||||
serverUrl: self.serverUrl,
|
||||
secretKeyStr: self.secretKeyStr,
|
||||
rootAuthToken: self.rootAuthToken,
|
||||
})
|
||||
self.environment.api.setUserCfg(cfg)
|
||||
self.isAuthed = true
|
||||
self.uiState = 'idle'
|
||||
return true
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiState = 'idle'
|
||||
return false
|
||||
}
|
||||
}),
|
||||
createTestAccount: flow(function* (serverUrl: string) {
|
||||
self.uiState = 'working'
|
||||
self.uiError = undefined
|
||||
try {
|
||||
const cfg = yield UserConfig.createTest(serverUrl)
|
||||
const state = yield cfg.serialize()
|
||||
self.serverUrl = state.serverUrl
|
||||
self.secretKeyStr = state.secretKeyStr
|
||||
self.rootAuthToken = state.rootAuthToken
|
||||
self.isAuthed = true
|
||||
self.environment.api.setUserCfg(cfg)
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
}
|
||||
self.uiState = 'idle'
|
||||
}),
|
||||
}))
|
||||
|
||||
export interface Session extends Instance<typeof SessionModel> {}
|
||||
|
@ -17,5 +72,6 @@ export interface SessionSnapshot extends SnapshotOut<typeof SessionModel> {}
|
|||
export function createDefaultSession() {
|
||||
return {
|
||||
isAuthed: false,
|
||||
uiState: 'idle',
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue