diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-06-10 11:55:09 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-06-10 11:55:09 -0500 |
commit | faddda83f04b46bcdaab5c225cab696fc7a820cd (patch) | |
tree | 8a7cacb04f0a480d90a8e5dc9f9daae00f27c802 | |
parent | 967f9fc474f2903dd2c12ef4f662ead1592ea26c (diff) | |
download | voidsky-faddda83f04b46bcdaab5c225cab696fc7a820cd.tar.zst |
(WIP) Add initial API client
-rw-r--r-- | .eslintrc.js | 2 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | scripts/testing-server.mjs | 16 | ||||
-rw-r--r-- | src/api/index.ts | 97 | ||||
-rw-r--r-- | src/screens/Login.tsx | 26 | ||||
-rw-r--r-- | src/screens/Signup.tsx | 50 | ||||
-rw-r--r-- | src/state/env.ts | 3 | ||||
-rw-r--r-- | src/state/models/session.ts | 58 | ||||
-rw-r--r-- | yarn.lock | 31 |
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" |