(WIP) Add initial API client

This commit is contained in:
Paul Frazee 2022-06-10 11:55:09 -05:00
parent 967f9fc474
commit faddda83f0
9 changed files with 260 additions and 27 deletions

97
src/api/index.ts Normal file
View 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])
}
}
}

View file

@ -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>
)
}
})

View file

@ -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>
)
},
)

View file

@ -4,8 +4,11 @@
*/
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
import {API} from '../api'
export class Environment {
api = new API()
constructor() {}
async setup() {}

View file

@ -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',
}
}