about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-06-10 11:55:09 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-06-10 11:55:09 -0500
commitfaddda83f04b46bcdaab5c225cab696fc7a820cd (patch)
tree8a7cacb04f0a480d90a8e5dc9f9daae00f27c802
parent967f9fc474f2903dd2c12ef4f662ead1592ea26c (diff)
downloadvoidsky-faddda83f04b46bcdaab5c225cab696fc7a820cd.tar.zst
(WIP) Add initial API client
-rw-r--r--.eslintrc.js2
-rw-r--r--package.json4
-rw-r--r--scripts/testing-server.mjs16
-rw-r--r--src/api/index.ts97
-rw-r--r--src/screens/Login.tsx26
-rw-r--r--src/screens/Signup.tsx50
-rw-r--r--src/state/env.ts3
-rw-r--r--src/state/models/session.ts58
-rw-r--r--yarn.lock31
9 files changed, 260 insertions, 27 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index 898ffe6d7..ba805bc32 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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',
diff --git a/package.json b/package.json
index 763f6d89e..cfa1f892c 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/scripts/testing-server.mjs b/scripts/testing-server.mjs
new file mode 100644
index 000000000..3517acdf7
--- /dev/null
+++ b/scripts/testing-server.mjs
@@ -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()
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 000000000..f83e65411
--- /dev/null
+++ b/src/api/index.ts
@@ -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])
+    }
+  }
+}
diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx
index 0eea085d5..8451eb3c8 100644
--- a/src/screens/Login.tsx
+++ b/src/screens/Login.tsx
@@ -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>
   )
-}
+})
diff --git a/src/screens/Signup.tsx b/src/screens/Signup.tsx
index bab7abc1c..1d5915d65 100644
--- a/src/screens/Signup.tsx
+++ b/src/screens/Signup.tsx
@@ -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>
+    )
+  },
+)
diff --git a/src/state/env.ts b/src/state/env.ts
index 90a2cab5e..59c0554a2 100644
--- a/src/state/env.ts
+++ b/src/state/env.ts
@@ -4,8 +4,11 @@
  */
 
 import {getEnv, IStateTreeNode} from 'mobx-state-tree'
+import {API} from '../api'
 
 export class Environment {
+  api = new API()
+
   constructor() {}
 
   async setup() {}
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 675feb8bc..a550e2174 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -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',
   }
 }
diff --git a/yarn.lock b/yarn.lock
index 837633c54..dac3abca2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"