(WIP) Add initial API client

zio/stable
Paul Frazee 2022-06-10 11:55:09 -05:00
parent 967f9fc474
commit faddda83f0
9 changed files with 260 additions and 27 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
// plugins: ['@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx'],
files: ['*.js', '*.mjs', '*.ts', '*.tsx'],
rules: {
'@typescript-eslint/no-shadow': 'off',
'no-shadow': 'off',

View File

@ -7,6 +7,7 @@
"ios": "react-native run-ios",
"web": "react-scripts start",
"start": "react-native start",
"dev-backend": "node ./scripts/testing-server.mjs",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
@ -24,7 +25,8 @@
"react-native": "0.68.2",
"react-native-safe-area-context": "^4.3.1",
"react-native-screens": "^3.13.1",
"react-native-web": "^0.17.7"
"react-native-web": "^0.17.7",
"ucans": "0.9.0-alpha3"
},
"devDependencies": {
"@babel/core": "^7.12.9",

View File

@ -0,0 +1,16 @@
import {IpldStore} from '@adx/common'
import server from '@adx/server/dist/server.js'
import Database from '@adx/server/dist/db/index.js'
const PORT = 1986
async function start() {
console.log('Initializing...')
const db = Database.memory()
const serverBlockstore = IpldStore.createInMemory()
await db.dropTables()
await db.createTables()
server(serverBlockstore, db, PORT)
}
start()

97
src/api/index.ts 100644
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',
}
}

View File

@ -8602,6 +8602,11 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
multiformats@^9.4.2:
version "9.6.5"
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.5.tgz#f2d894a26664b454a90abf5a8911b7e39195db80"
integrity sha512-vMwf/FUO+qAPvl3vlSZEgEVFY/AxeZq5yg761ScF3CZsXgmTi/HGkicUiNN0CI4PW8FiY2P0OLklOcmQjdQJhw==
nanoid@^3.1.23, nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@ -8912,6 +8917,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
one-webcrypto@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/one-webcrypto/-/one-webcrypto-1.0.3.tgz#f951243cde29b79b6745ad14966fc598a609997c"
integrity sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@ -11624,6 +11634,11 @@ tsutils@^3.17.1, tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -11698,6 +11713,15 @@ ua-parser-js@^0.7.30:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
ucans@0.9.0-alpha3:
version "0.9.0-alpha3"
resolved "https://registry.yarnpkg.com/ucans/-/ucans-0.9.0-alpha3.tgz#1665b0ecf4f68ee77ba41dcb5f37f9064c9dbd4b"
integrity sha512-52eLo/YnrGf4o7T6Bv2vjPkvq0nSsvxZhkh8EOXwQKMC1hXvVrHArLPgDIyARPynoGw5ZAguzo4A/xibPdHM8Q==
dependencies:
one-webcrypto "^1.0.1"
tweetnacl "^1.0.3"
uint8arrays "^3.0.0"
uglify-es@^3.1.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
@ -11706,6 +11730,13 @@ uglify-es@^3.1.9:
commander "~2.13.0"
source-map "~0.6.1"
uint8arrays@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b"
integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==
dependencies:
multiformats "^9.4.2"
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"