mirror of
https://github.com/Terreii/andromeda-viewer.git
synced 2026-08-14 00:57:49 +00:00
feat(viewerAccount): finish sign up, sign in and sign out
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user