feat(viewerAccount): finish sign up, sign in and sign out

This commit is contained in:
Christopher Astfalk
2020-09-06 00:58:43 +02:00
parent d0ff7b5a59
commit 57396f51cb
6 changed files with 71 additions and 140 deletions
-1
View File
@@ -19,7 +19,6 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<script src="%PUBLIC_URL%/hoodie/client.js"></script>
<title>Andromeda Viewer</title>
</head>
<body>
+25 -57
View File
@@ -7,7 +7,6 @@ import {
signOut as accountDidSignOut,
unlocked,
displayResetKeys,
didUpdate as accountDidUpdate,
avatarSaved,
avatarsLoaded,
savedAvatarUpdated,
@@ -37,7 +36,7 @@ import {
import { IMChatType } from '../types/chat'
export function saveAvatar (name, agentId, grid) {
return (dispatch, getState, { hoodie }) => {
return (dispatch, getState, { cryptoStore }) => {
const gridName = typeof grid === 'string' ? grid : grid.name
const avatarIdentifier = `${agentId}@${gridName}`
@@ -48,7 +47,7 @@ export function saveAvatar (name, agentId, grid) {
return Promise.reject(new Error('Avatar already exist!'))
}
return hoodie.cryptoStore.withIdPrefix('avatars/').add({
return cryptoStore.withIdPrefix('avatars/').add({
dataSaveId: uuid(),
avatarIdentifier,
name: name.getFullName(),
@@ -58,7 +57,7 @@ export function saveAvatar (name, agentId, grid) {
}
export function loadSavedAvatars () {
return async (dispatch, getState, { hoodie }) => {
return async (dispatch, getState, { db, cryptoStore }) => {
const activeState = getState()
if (!selectIsSignedIn(activeState)) {
@@ -67,14 +66,14 @@ export function loadSavedAvatars () {
if (selectSavedAvatarsAreLoaded(activeState)) return
const avatarsStore = hoodie.cryptoStore.withIdPrefix('avatars/')
const avatarsStore = cryptoStore.withIdPrefix('avatars/')
const changeHandler = (eventName, doc) => {
dispatch(avatarsDidChange(eventName, doc))
}
avatarsStore.on('change', changeHandler)
hoodie.account.one('signout', () => {
db.on('destroyed', () => {
avatarsStore.off('change', changeHandler)
})
@@ -107,14 +106,14 @@ function avatarsDidChange (type, doc) {
}
export function saveGrid (newGrid) {
return (dispatch, getState, { hoodie }) => {
return (dispatch, getState, { cryptoStore }) => {
const name = newGrid.name.trim()
if (selectSavedGrids(getState()).some(grid => grid.name === name)) {
return Promise.reject(new Error('Grid already exist!'))
}
return hoodie.cryptoStore.withIdPrefix('grids/').add({
return cryptoStore.withIdPrefix('grids/').add({
name,
loginURL: newGrid.loginURL,
isLLSDLogin: false
@@ -123,7 +122,7 @@ export function saveGrid (newGrid) {
}
export function loadSavedGrids () {
return async (dispatch, getState, { hoodie }) => {
return async (dispatch, getState, { db, cryptoStore }) => {
const activeState = getState()
if (!selectIsSignedIn(activeState)) {
@@ -132,14 +131,14 @@ export function loadSavedGrids () {
if (selectSavedGridsAreLoaded(activeState)) return
const gridsStore = hoodie.cryptoStore.withIdPrefix('grids/')
const gridsStore = cryptoStore.withIdPrefix('grids/')
const changeHandler = (change, doc) => {
dispatch(gridsDidChange(change, doc))
}
gridsStore.on('change', changeHandler)
hoodie.account.one('signout', () => {
db.on('destroyed', () => {
gridsStore.off('change', changeHandler)
})
@@ -164,7 +163,7 @@ function gridsDidChange (type, grid) {
export function isSignedIn () {
return async (dispatch, getState, args) => {
const session = await args.remoteDB.getSession()
let username = session.userCtx.name
let username = session?.userCtx?.name
if (username == null) {
try {
@@ -184,12 +183,7 @@ export function isSignedIn () {
if (isLoggedIn) {
const userDbName = getUserDatabaseName(username)
args.remoteDB = createRemoteDB(userDbName)
startSyncing(args.db, args.remoteDB)
if (process.env.NODE_ENV !== 'production') {
window.remoteDB = args.remoteDB
}
args.cryptoStore = createCryptoStore(args.db, args.remoteDB)
}
dispatch(signInStatus(isLoggedIn, null, username))
@@ -198,17 +192,6 @@ export function isSignedIn () {
}
}
// eslint-disable-next-line
function listenToAccountChanges (account, dispatch) {
const handler = changes => {
dispatch(accountDidUpdate(changes))
}
account.on('update', handler)
account.one('signout', () => {
account.off('update', handler)
})
}
/**
* Generate/derive the login and encryption passwords from one password and username.
* Inspired by https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-client/lib/crypto.ts
@@ -285,10 +268,6 @@ export function unlock (password) {
startSyncing(args.db, args.remoteDB)
if (process.env.NODE_ENV !== 'production') {
window.remoteDB = args.remoteDB
}
await args.cryptoStore.unlock(cryptoPassword)
dispatch(unlocked())
@@ -305,13 +284,10 @@ function signInAndSync (accountProperties, password, cryptoPassword) {
const userDbName = getUserDatabaseName(accountProperties.data.id)
args.remoteDB.close()
args.remoteDB = createRemoteDB(userDbName, false)
args.cryptoStore = createCryptoStore(args.db, args.remoteDB)
startSyncing(args.db, args.remoteDB)
if (process.env.NODE_ENV !== 'production') {
window.remoteDB = args.remoteDB
}
await args.cryptoStore.unlock(cryptoPassword)
await args.db.put({
_id: '_local/account',
@@ -322,12 +298,14 @@ function signInAndSync (accountProperties, password, cryptoPassword) {
dispatch(signInStatus(true, true, accountProperties.data.attributes.username))
await dispatch(loadSavedGrids())
dispatch(loadSavedAvatars())
await dispatch(loadSavedAvatars())
}
}
export function signIn (username, password) {
return async (dispatch, getState, args) => {
return async (dispatch, getState) => {
if (selectIsSignedIn(getState())) return
const { loginPassword, cryptoPassword } = await derivePasswords(username, password)
const accountDataReq = await window.fetch('/session/account', {
method: 'GET',
@@ -346,7 +324,9 @@ export function signIn (username, password) {
}
export function signUp (username, password) {
return async (dispatch, getState, { cryptoStore, remoteDB }) => {
return async (dispatch, getState, { cryptoStore }) => {
if (selectIsSignedIn(getState())) return
const { loginPassword, cryptoPassword } = await derivePasswords(username, password)
const request = await window.fetch('/session/account', {
@@ -449,13 +429,7 @@ export function signOut () {
args.db = createLocalDB()
args.remoteDB = createRemoteDB('_users')
args.cryptoStore = createCryptoStore(args.db)
if (process.env.NODE_ENV !== 'production') {
window.localDB = args.db
window.remoteDB = args.remoteDB
window.cryptoStore = args.cryptoStore
}
args.cryptoStore = createCryptoStore(args.db, args.remoteDB)
} catch (err) {
console.error(err)
}
@@ -468,11 +442,11 @@ export function signOut () {
* @returns {object} JSON data and array of files (text).
*/
export function downloadAccountData () {
return async (dispatch, getState, { hoodie }) => {
const allAvatars = await hoodie.cryptoStore.withIdPrefix('avatars/').findAll()
return async (dispatch, getState, { cryptoStore }) => {
const allAvatars = await cryptoStore.withIdPrefix('avatars/').findAll()
const avatarData = await Promise.all(allAvatars.map(async avatar => {
const avatarStore = hoodie.cryptoStore.withIdPrefix(avatar.dataSaveId + '/')
const avatarStore = cryptoStore.withIdPrefix(avatar.dataSaveId + '/')
const [localChat, imChatsInfos] = await Promise.all([
avatarStore.withIdPrefix('localchat').findAll(),
@@ -494,15 +468,9 @@ export function downloadAccountData () {
}))
// Get other data
const [grids, account, profile] = await Promise.all([
hoodie.cryptoStore.withIdPrefix('grids/').findAll(),
hoodie.account.get(),
hoodie.account.profile.get()
])
const grids = await cryptoStore.withIdPrefix('grids/').findAll()
const raw = {
account,
profile,
grids,
avatars: avatarData
}
+6 -66
View File
@@ -16,31 +16,23 @@ export default function SignInPopup ({ isSignUp, dialog }) {
const password = useFormInput('')
const password2 = useFormInput('')
const cryptoPassword = useFormInput('')
const cryptoPassword2 = useFormInput('')
const [isSigningIn, setIsSigningIn] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
password.onChange('')
password2.onChange('')
cryptoPassword.onChange('')
cryptoPassword2.onChange('')
// Only reset passwords if the visibility did change
// eslint-disable-next-line
}, [dialog.visible])
const isValid = (() => {
if ([password.value, cryptoPassword.value].some((s, i) => s.length < 8)) {
if (password.value.length < 8) {
return false
}
// this also checks length of password2 and cryptoPassword2
if (isSignUp && (
password.value !== password2.value || cryptoPassword.value !== cryptoPassword2.value
)) {
// this also checks length of password2
if (isSignUp && password.value !== password2.value) {
return false
}
@@ -57,9 +49,9 @@ export default function SignInPopup ({ isSignUp, dialog }) {
try {
if (isSignUp) {
await dispatch(signUp(username, password.value, cryptoPassword.value))
await dispatch(signUp(username, password.value))
} else {
await dispatch(signIn(username, password.value, cryptoPassword.value))
await dispatch(signIn(username, password.value))
}
} catch (err) {
console.error(err)
@@ -115,6 +107,7 @@ export default function SignInPopup ({ isSignUp, dialog }) {
<small id='passwordHelp' className='leading-6 text-gray-600'>
Please use a strong and unique password!<br />
Minimal length: 8 characters!<br />
This password will <b>never</b> be saved or leave your machine!<br />
{'A '}
<a
href='https://en.wikipedia.org/wiki/List_of_password_managers'
@@ -154,59 +147,6 @@ export default function SignInPopup ({ isSignUp, dialog }) {
</label>
)}
<label className='flex flex-col m-1'>
<span>Encryption password</span>
<input
{...cryptoPassword}
id='cryptoPassword'
type='password'
className='block w-full mt-1 text-gray-900 form-input'
required
minLength='8'
aria-describedby={isSignUp && 'cryptoPwHelp'}
disabled={isSigningIn}
onFocus={onFocusScrollIntoView}
/>
{isSignUp && (
<small id='cryptoPwHelp' className='leading-5 text-gray-600'>
Minimal length: 8 characters!<br />
This password is used to encrypt your personal data.<br />
This includes: <i>Avatar login-info</i>, <i>grids</i>, and <i>chat-logs</i>.<br />
<b>Your personal data is encrypted on your machine.<br />
and will never leave it un-encrypted!
</b>
<br />
This password will <b>never</b> be saved or leave your machine!
</small>
)}
</label>
{isSignUp && (
<label className='flex flex-col m-1'>
<span>Repeat encryption password</span>
<input
{...cryptoPassword2}
id='cryptoPassword2'
type='password'
className='block w-full mt-1 text-gray-900 form-input'
required
minLength='8'
disabled={isSigningIn}
onFocus={onFocusScrollIntoView}
/>
{!(cryptoPassword2.value.length === 0 ||
cryptoPassword.value === cryptoPassword2.value
) && (
<small
className='px-4 py-2 mt-1 leading-6 text-red-800 bg-red-200 border border-red-500 rounded'
role='alert'
>
Encryption password doesn't match!
</small>
)}
</label>
)}
{error && (
<small
className='px-4 py-2 mt-1 leading-6 text-red-800 bg-red-200 border border-red-500 rounded'
+9 -5
View File
@@ -1,11 +1,12 @@
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { useSelector, useDispatch } from 'react-redux'
import { useDialogState, DialogDisclosure } from 'reakit'
import Modal from './modal'
import ResetPasswordDialog from './resetPasswordDialog'
import { signOut, unlock } from '../../actions/viewerAccount'
import { selectUserName } from '../../bundles/account'
import { useAutoFocus } from '../../hooks/utils'
@@ -14,6 +15,7 @@ import lockIcon from '../../icons/black_lock.svg'
export default function UnlockDialog () {
const dialog = useDialogState({ visible: process.env.NODE_ENV !== 'test' })
const dispatch = useDispatch()
const username = useSelector(selectUserName)
const [password, setPassword] = useState('')
const [isUnlocking, setIsUnlocking] = useState(false)
@@ -61,11 +63,13 @@ export default function UnlockDialog () {
return (
<Modal title='Unlock' icon={icon} dialog={dialog} notCloseable>
<form className='flex flex-col' onSubmit={doUnlock}>
<span className='m-1'>
Please enter your <em>Encryption-Password</em> to unlock this app!
</span>
<p className='m-1'>
You are logged in as <strong>{username}</strong>.
<br />
Please enter your <em>Password</em> to unlock this app!
</p>
<label className='flex flex-col m-1'>
<label className='flex flex-col m-1 mt-2'>
<span>Password</span>
<input
id='unlockPasswordIn'
+3 -3
View File
@@ -6,17 +6,17 @@ module.exports = proxySetup
function proxySetup (app) {
// Setup proxy for all http apis
app.use(createProxyMiddleware('http://localhost:8080/hoodie/andromeda-viewer/proxy/', {
app.use(createProxyMiddleware('http://localhost:3001/hoodie/andromeda-viewer/proxy/', {
changeOrigin: true,
toProxy: true
}))
// Setup all http apis
app.use(createProxyMiddleware('http://localhost:8080/hoodie'))
app.use(createProxyMiddleware('http://localhost:3001/session'))
// Setup proxy for web-socket
app.use(createProxyMiddleware('/andromeda-bridge', {
target: 'http://localhost:8080',
target: 'http://localhost:3001',
changeOrigin: true,
ws: true
}))
+28 -8
View File
@@ -2,32 +2,52 @@ import PouchDB from 'pouchdb-browser'
import memoryAdapter from 'pouchdb-adapter-memory'
import auth from 'pouchdb-authentication'
import hoodieApi from 'pouchdb-hoodie-api'
import hoodiePluginStoreCrypto from 'hoodie-plugin-store-crypto'
import CryptoStore from 'hoodie-plugin-store-crypto'
PouchDB.plugin(memoryAdapter)
PouchDB.plugin(auth)
PouchDB.plugin(hoodieApi)
export function createLocalDB () {
return new PouchDB('local')
const db = new PouchDB('local')
if (process.env.NODE_ENV !== 'production') {
window.localDB = db
}
return db
}
export function createCryptoStore (localDB) {
export function createCryptoStore (localDB, remoteDB) {
const api = localDB.hoodieApi()
const getter = { store: api }
hoodiePluginStoreCrypto(getter)
return getter.cryptoStore
const cryptoStore = new CryptoStore(api, {
remote: remoteDB
})
localDB.once('destroyed', () => {
cryptoStore.lock()
})
if (process.env.NODE_ENV !== 'production') {
window.cryptoStore = cryptoStore
}
return cryptoStore
}
export function createRemoteDB (dbName, skipSetup = true) {
const url = new URL(process.env.REACT_APP_DB_URL || 'http://127.0.0.1:5984')
url.pathname = '/' + dbName
console.log(url.href)
return new PouchDB(url.href, {
const db = new PouchDB(url.href, {
adapter: 'http',
skip_setup: skipSetup
})
if (process.env.NODE_ENV !== 'production') {
window.remoteDB = db
}
return db
}
export function startSyncing (localDB, remoteDB) {