(WIP) Add initial API client
parent
967f9fc474
commit
faddda83f0
|
@ -5,7 +5,7 @@ module.exports = {
|
||||||
// plugins: ['@typescript-eslint'],
|
// plugins: ['@typescript-eslint'],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.js', '*.mjs', '*.ts', '*.tsx'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-shadow': 'off',
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"ios": "react-native run-ios",
|
"ios": "react-native run-ios",
|
||||||
"web": "react-scripts start",
|
"web": "react-scripts start",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
|
"dev-backend": "node ./scripts/testing-server.mjs",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||||
},
|
},
|
||||||
|
@ -24,7 +25,8 @@
|
||||||
"react-native": "0.68.2",
|
"react-native": "0.68.2",
|
||||||
"react-native-safe-area-context": "^4.3.1",
|
"react-native-safe-area-context": "^4.3.1",
|
||||||
"react-native-screens": "^3.13.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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
|
|
|
@ -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()
|
|
@ -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 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 {Shell} from '../platform/shell'
|
||||||
import type {RootTabsScreenProps} from '../routes/types'
|
import type {RootTabsScreenProps} from '../routes/types'
|
||||||
import {useStores} from '../state'
|
import {useStores} from '../state'
|
||||||
|
|
||||||
export function Login({navigation}: RootTabsScreenProps<'Login'>) {
|
export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||||
<Button title="Login" onPress={() => store.session.setAuthed(true)} />
|
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
<Button title="Sign Up" onPress={() => navigation.navigate('Signup')} />
|
{store.session.uiState === 'idle' ? (
|
||||||
|
<>
|
||||||
|
{store.session.hasAccount ?? (
|
||||||
|
<Button
|
||||||
|
title="Login"
|
||||||
|
onPress={() => store.session.loadAccount()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
title="Sign Up"
|
||||||
|
onPress={() => navigation.navigate('Signup')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -1,24 +1,36 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {Text, Button, View, ActivityIndicator} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Shell} from '../platform/shell'
|
import {Shell} from '../platform/shell'
|
||||||
import {Text, Button, View} from 'react-native'
|
|
||||||
import type {RootTabsScreenProps} from '../routes/types'
|
import type {RootTabsScreenProps} from '../routes/types'
|
||||||
import {useStores} from '../state'
|
import {useStores} from '../state'
|
||||||
|
|
||||||
export function Signup({navigation}: RootTabsScreenProps<'Signup'>) {
|
export const Signup = observer(
|
||||||
const store = useStores()
|
({navigation}: RootTabsScreenProps<'Signup'>) => {
|
||||||
return (
|
const store = useStores()
|
||||||
<Shell>
|
return (
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<Shell>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Button
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||||
title="Create new account"
|
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
onPress={() => store.session.setAuthed(true)}
|
{store.session.uiState === 'idle' ? (
|
||||||
/>
|
<>
|
||||||
<Button
|
<Button
|
||||||
title="Log in to an existing account"
|
title="Create new account"
|
||||||
onPress={() => navigation.navigate('Login')}
|
onPress={() =>
|
||||||
/>
|
store.session.createTestAccount('http://localhost:1986')
|
||||||
</View>
|
}
|
||||||
</Shell>
|
/>
|
||||||
)
|
<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 {getEnv, IStateTreeNode} from 'mobx-state-tree'
|
||||||
|
import {API} from '../api'
|
||||||
|
|
||||||
export class Environment {
|
export class Environment {
|
||||||
|
api = new API()
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
async setup() {}
|
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
|
export const SessionModel = types
|
||||||
.model('Session')
|
.model('Session')
|
||||||
.props({
|
.props({
|
||||||
isAuthed: types.boolean,
|
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 => ({
|
.actions(self => ({
|
||||||
setAuthed: (v: boolean) => {
|
setAuthed: (v: boolean) => {
|
||||||
self.isAuthed = v
|
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> {}
|
export interface Session extends Instance<typeof SessionModel> {}
|
||||||
|
@ -17,5 +72,6 @@ export interface SessionSnapshot extends SnapshotOut<typeof SessionModel> {}
|
||||||
export function createDefaultSession() {
|
export function createDefaultSession() {
|
||||||
return {
|
return {
|
||||||
isAuthed: false,
|
isAuthed: false,
|
||||||
|
uiState: 'idle',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -8602,6 +8602,11 @@ multicast-dns@^7.2.5:
|
||||||
dns-packet "^5.2.2"
|
dns-packet "^5.2.2"
|
||||||
thunky "^1.0.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:
|
nanoid@^3.1.23, nanoid@^3.3.4:
|
||||||
version "3.3.4"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
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:
|
dependencies:
|
||||||
wrappy "1"
|
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:
|
onetime@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
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:
|
dependencies:
|
||||||
tslib "^1.8.1"
|
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:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
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"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
|
||||||
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
|
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:
|
uglify-es@^3.1.9:
|
||||||
version "3.3.9"
|
version "3.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
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"
|
commander "~2.13.0"
|
||||||
source-map "~0.6.1"
|
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:
|
unbox-primitive@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||||
|
|
Loading…
Reference in New Issue