Add base auth & ucan request flow (web only)

zio/stable
Paul Frazee 2022-06-14 14:29:47 -05:00
parent 09b78a4634
commit cef133031e
14 changed files with 1555 additions and 290 deletions

1
.env 100644
View File

@ -0,0 +1 @@
REACT_APP_AUTH_LOBBY = 'http://localhost:3001'

View File

@ -14,20 +14,16 @@ Uses:
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
- After initial setup:
- `cd ios ; pod install` Installs the React Navigation deps ([info](https://reactnative.dev/docs/navigation#installation-and-setup)).
- To run the iOS simulator: `yarn ios`
- To run the Android simulator: `yarn android`
- To run the Web app: `yarn web`
- `cd ios ; pod install`
- Start the dev servers
- `yarn dev-pds`
- `yarn dev-wallet`
- Run the dev app
- iOS: `yarn ios`
- Android: `yarn android`
- Web: `yarn web`
- Tips
- `npx react-native info` Checks what has been installed.
- Android instructions are a *little* inaccurate but not as much as you might think. I had to manually create a virtual device, then run `yarn android` twice (once to start the emulator and the second time to connect to it).
## TODOs
- API
- Create mock api
- Tests
- Should just try to catch errors on basic load
## Various notes

View File

@ -1,5 +1,5 @@
{
"name": "app",
"name": "pubsq",
"version": "0.0.1",
"private": true,
"scripts": {
@ -7,11 +7,14 @@
"ios": "react-native run-ios",
"web": "react-scripts start",
"start": "react-native start",
"dev-backend": "node ./scripts/testing-server.mjs",
"dev-pds": "node ./scripts/testing-server.mjs",
"dev-wallet": "cd node_modules/\\@adxp/auth-lobby && npm run start:authed",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@adxp/auth": "*",
"@adxp/common": "*",
"@react-native-async-storage/async-storage": "^1.17.6",
"@react-navigation/bottom-tabs": "^6.3.1",
"@react-navigation/native": "^6.0.10",
@ -29,6 +32,9 @@
"ucans": "0.9.0-alpha3"
},
"devDependencies": {
"@adxp/auth-lobby": "*",
"@adxp/server": "*",
"@adxp/ws-relay": "*",
"@babel/core": "^7.12.9",
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^2.0.0",

View File

@ -1,16 +1,25 @@
import {IpldStore} from '@adx/common'
import server from '@adx/server/dist/server.js'
import Database from '@adx/server/dist/db/index.js'
import {IpldStore} from '@adxp/common'
import PDSServer from '@adxp/server/dist/server.js'
import PDSDatabase from '@adxp/server/dist/db/index.js'
import WSRelayServer from '@adxp/ws-relay/dist/index.js'
const PORT = 1986
const PDS_PORT = 2583
const WSR_PORT = 3005
async function start() {
console.log('Initializing...')
const db = Database.memory()
const db = PDSDatabase.memory()
const serverBlockstore = IpldStore.createInMemory()
await db.dropTables()
await db.createTables()
server(serverBlockstore, db, PORT)
PDSServer(serverBlockstore, db, PDS_PORT)
if (process.argv.includes('--relay')) {
WSRelayServer(WSR_PORT)
console.log(`🔁 Relay server running on port ${WSR_PORT}`)
} else {
console.log('Include --relay to start the WS Relay')
}
}
start()

48
src/api/auth.ts 100644
View File

@ -0,0 +1,48 @@
import * as auth from '@adxp/auth'
import {isWeb} from '../platform/detection'
import * as env from '../env'
const SCOPE = auth.writeCap(
'did:key:z6MkfRiFMLzCxxnw6VMrHK8pPFt4QAHS3jX3XM87y9rta6kP',
'did:example:microblog',
)
export async function isAuthed(authStore: auth.BrowserStore) {
return await authStore.hasUcan(SCOPE)
}
export async function logout(authStore: auth.BrowserStore) {
await authStore.reset()
}
export async function parseUrlForUcan() {
// @ts-ignore window is defined -prf
const fragment = window.location.hash
if (fragment.length < 1) {
return undefined
}
try {
const ucan = await auth.parseLobbyResponseHashFragment(fragment)
// @ts-ignore window is defined -prf
window.location.hash = ''
return ucan
} catch (err) {
return undefined
}
}
export async function requestAppUcan(authStore: auth.BrowserStore) {
const did = await authStore.getDid()
if (isWeb) {
// @ts-ignore window is defined -prf
const redirectTo = window.location.origin
const fragment = auth.requestAppUcanHashFragment(did, SCOPE, redirectTo)
// @ts-ignore window is defined -prf
window.location.href = `${env.AUTH_LOBBY}#${fragment}`
return false
} else {
// TODO
console.log('TODO')
}
return false
}

View File

@ -1,5 +1,26 @@
import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
import * as ucan from 'ucans'
// import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
// import * as ucan from 'ucans'
class MicroblogReader {
constructor(public url: string, public did: any) {}
}
class MicroblogDelegator {
constructor(
public url: string,
public did: any,
public keypair: any,
public ucanStore: any,
) {}
}
const auth = {
async claimFull(_one: any, _two: any) {
return {
encoded() {
return 'todo'
},
}
},
}
export class API {
userCfg?: UserConfig
@ -51,9 +72,9 @@ export interface SerializedUserConfig {
export class UserConfig {
serverUrl?: string
did?: string
keypair?: ucan.EdKeypair
keypair?: any //ucan.EdKeypair
rootAuthToken?: string
ucanStore?: ucan.Store
ucanStore?: any //ucan.Store
get hasWriteCaps() {
return Boolean(this.did && this.keypair && this.ucanStore)
@ -62,10 +83,10 @@ export class UserConfig {
static async createTest(serverUrl: string) {
const cfg = new UserConfig()
cfg.serverUrl = serverUrl
cfg.keypair = await ucan.EdKeypair.create()
cfg.keypair = true //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])
cfg.ucanStore = true // await ucan.Store.fromTokens([cfg.rootAuthToken])
return cfg
}
@ -88,10 +109,10 @@ export class UserConfig {
async hydrate(state: SerializedUserConfig) {
this.serverUrl = state.serverUrl
if (state.secretKeyStr && state.rootAuthToken) {
this.keypair = ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
this.keypair = true // ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
this.did = this.keypair.did()
this.rootAuthToken = state.rootAuthToken
this.ucanStore = await ucan.Store.fromTokens([this.rootAuthToken])
this.ucanStore = true // await ucan.Store.fromTokens([this.rootAuthToken])
}
}
}

5
src/env.ts 100644
View File

@ -0,0 +1,5 @@
if (typeof process.env.REACT_APP_AUTH_LOBBY !== 'string') {
throw new Error('ENV: No auth lobby provided')
}
export const AUTH_LOBBY = process.env.REACT_APP_AUTH_LOBBY

View File

@ -14,7 +14,7 @@ export function Home({navigation}: RootTabsScreenProps<'Home'>) {
title="Go to Jane's profile"
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
/>
<Button title="Logout" onPress={() => store.session.setAuthed(false)} />
<Button title="Logout" onPress={() => store.session.logout()} />
</View>
</Shell>
)

View File

@ -12,14 +12,9 @@ export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
<View style={{justifyContent: 'center', alignItems: 'center'}}>
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
{store.session.uiState === 'idle' ? (
{!store.session.uiIsProcessing ? (
<>
{store.session.hasAccount ?? (
<Button
title="Login"
onPress={() => store.session.loadAccount()}
/>
)}
<Button title="Login" onPress={() => store.session.login()} />
<Button
title="Sign Up"
onPress={() => navigation.navigate('Signup')}

View File

@ -13,13 +13,11 @@ export const Signup = observer(
<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' ? (
{!store.session.uiIsProcessing ? (
<>
<Button
title="Create new account"
onPress={() =>
store.session.createTestAccount('http://localhost:1986')
}
onPress={() => store.session.login()}
/>
<Button
title="Log in to an existing account"

View File

@ -4,14 +4,18 @@
*/
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
import * as auth from '@adxp/auth'
import {API} from '../api'
export class Environment {
api = new API()
authStore?: auth.BrowserStore
constructor() {}
async setup() {}
async setup() {
this.authStore = await auth.BrowserStore.load()
}
}
/**

View File

@ -6,6 +6,7 @@ import {
} from './models/root-store'
import {Environment} from './env'
import * as storage from './storage'
import * as auth from '../api/auth'
const ROOT_STATE_STORAGE_KEY = 'root'
@ -14,6 +15,7 @@ export async function setupState() {
let data: any
const env = new Environment()
await env.setup()
try {
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
rootStore = RootStoreModel.create(data, env)
@ -27,6 +29,16 @@ export async function setupState() {
storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
)
if (env.authStore) {
const isAuthed = await auth.isAuthed(env.authStore)
rootStore.session.setAuthed(isAuthed)
const ucan = await auth.parseUrlForUcan()
if (ucan) {
await env.authStore.addUcan(ucan)
rootStore.session.setAuthed(true)
}
}
return rootStore
}

View File

@ -1,12 +1,13 @@
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
import {UserConfig} from '../../api'
// import {UserConfig} from '../../api'
import * as auth from '../../api/auth'
import {withEnvironment} from '../env'
export const SessionModel = types
.model('Session')
.props({
isAuthed: types.boolean,
uiState: types.enumeration('idle', ['idle', 'working']),
uiIsProcessing: types.maybe(types.boolean),
uiError: types.maybe(types.string),
// TODO: these should be stored somewhere secret
@ -14,56 +15,84 @@ export const SessionModel = types
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'
login: flow(function* () {
self.uiIsProcessing = true
self.uiError = undefined
try {
const cfg = yield UserConfig.hydrate({
serverUrl: self.serverUrl,
secretKeyStr: self.secretKeyStr,
rootAuthToken: self.rootAuthToken,
})
self.environment.api.setUserCfg(cfg)
if (!self.environment.authStore) {
throw new Error('Auth store not initialized')
}
const res = yield auth.requestAppUcan(self.environment.authStore)
self.isAuthed = res
self.uiIsProcessing = false
return res
} catch (e: any) {
console.error('Failed to request app ucan', e)
self.uiError = e.toString()
self.uiIsProcessing = false
return false
}
}),
logout: flow(function* () {
self.uiIsProcessing = true
self.uiError = undefined
try {
if (!self.environment.authStore) {
throw new Error('Auth store not initialized')
}
const res = yield auth.logout(self.environment.authStore)
self.isAuthed = false
self.uiIsProcessing = false
return res
} catch (e: any) {
console.error('Failed to log out', e)
self.uiError = e.toString()
self.uiIsProcessing = false
return false
}
}),
/*loadAccount: flow(function* () {
self.uiIsProcessing = true
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'
self.uiIsProcessing = false
return true
} catch (e: any) {
console.error('Failed to create test account', e)
self.uiError = e.toString()
self.uiState = 'idle'
self.uiIsProcessing = false
return false
}
}),
createTestAccount: flow(function* (serverUrl: string) {
self.uiState = 'working'
createTestAccount: flow(function* (_serverUrl: string) {
self.uiIsProcessing = true
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
// 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)
// self.environment.api.setUserCfg(cfg)
} catch (e: any) {
console.error('Failed to create test account', e)
self.uiError = e.toString()
}
self.uiState = 'idle'
}),
self.uiIsProcessing = false
}),*/
}))
export interface Session extends Instance<typeof SessionModel> {}

1579
yarn.lock

File diff suppressed because it is too large Load Diff