final touches
This commit is contained in:
parent
3cfbaf4638
commit
b8216740d4
|
@ -1,238 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useMe } from './me'
|
||||
import { useShowModal } from './modal'
|
||||
import { useVaultConfigurator } from './vault/use-vault-configurator'
|
||||
import { Button, InputGroup } from 'react-bootstrap'
|
||||
import { Form, Input, PasswordInput, SubmitButton } from './form'
|
||||
import bip39Words from '@/lib/bip39-words'
|
||||
import Info from './info'
|
||||
import CancelButton from './cancel-button'
|
||||
import * as yup from 'yup'
|
||||
import { deviceSyncSchema } from '@/lib/validate'
|
||||
import RefreshIcon from '@/svgs/refresh-line.svg'
|
||||
import { useApolloClient } from '@apollo/client'
|
||||
|
||||
export default function DeviceSync () {
|
||||
const { me } = useMe()
|
||||
const apollo = useApolloClient()
|
||||
const { key, setVaultKey, clearVault } = useVaultConfigurator()
|
||||
const showModal = useShowModal()
|
||||
|
||||
const enabled = !!me?.privates?.vaultKeyHash
|
||||
const connected = !!key
|
||||
|
||||
const manage = useCallback(async () => {
|
||||
if (enabled && connected) {
|
||||
showModal((onClose) => (
|
||||
<div>
|
||||
<h2>Device sync is enabled!</h2>
|
||||
<p>
|
||||
Sensitive data (like wallet credentials) are now securely synced between all connected devices.
|
||||
</p>
|
||||
<p className='text-muted text-sm'>
|
||||
Disconnect to prevent this device from syncing data or to reset your passphrase.
|
||||
</p>
|
||||
<div className='d-flex justify-content-between'>
|
||||
<div className='d-flex align-items-center ms-auto gap-2'>
|
||||
<Button className='me-2 text-muted nav-link fw-bold' variant='link' onClick={onClose}>close</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
clearVault()
|
||||
onClose()
|
||||
}}
|
||||
>disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
} else {
|
||||
showModal((onClose) => (
|
||||
<ConnectForm onClose={onClose} onConnect={onConnect} enabled={enabled} />
|
||||
))
|
||||
}
|
||||
}, [enabled, connected, key])
|
||||
|
||||
const reset = useCallback(async () => {
|
||||
const schema = yup.object().shape({
|
||||
confirm: yup.string()
|
||||
.oneOf(['yes'], 'you must confirm by typing "yes"')
|
||||
.required('required')
|
||||
})
|
||||
showModal((onClose) => (
|
||||
<div>
|
||||
<h2>Reset device sync</h2>
|
||||
<p>
|
||||
This will delete all encrypted data on the server and disconnect all devices.
|
||||
</p>
|
||||
<p>
|
||||
You will need to enter a new passphrase on this and all other devices to sync data again.
|
||||
</p>
|
||||
<Form
|
||||
className='mt-3'
|
||||
initial={{ confirm: '' }}
|
||||
schema={schema}
|
||||
onSubmit={async values => {
|
||||
await clearVault()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
label='This action cannot be undone. Type `yes` to confirm.'
|
||||
name='confirm'
|
||||
placeholder=''
|
||||
required
|
||||
autoFocus
|
||||
autoComplete='off'
|
||||
/>
|
||||
<div className='d-flex justify-content-between'>
|
||||
<div className='d-flex align-items-center ms-auto'>
|
||||
<CancelButton onClick={onClose} />
|
||||
<SubmitButton variant='danger'>
|
||||
continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
))
|
||||
}, [])
|
||||
|
||||
const onConnect = useCallback(async (values, formik) => {
|
||||
if (values.passphrase) {
|
||||
try {
|
||||
await setVaultKey(values.passphrase)
|
||||
apollo.cache.evict({ fieldName: 'BestWallets' })
|
||||
apollo.cache.gc()
|
||||
await apollo.refetchQueries({ include: ['BestWallets'] })
|
||||
} catch (e) {
|
||||
formik?.setErrors({ passphrase: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}, [setVaultKey])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='form-label mt-3'>device sync</div>
|
||||
<div className='mt-2 d-flex align-items-center'>
|
||||
<div>
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={manage}
|
||||
>
|
||||
{enabled ? (connected ? 'Manage ' : 'Connect to ') : 'Enable '}
|
||||
device sync
|
||||
</Button>
|
||||
</div>
|
||||
<Info>
|
||||
<p>
|
||||
Device sync uses end-to-end encryption to securely synchronize your data across devices.
|
||||
</p>
|
||||
<p className='text-muted text-sm'>
|
||||
Your sensitive data remains private and inaccessible to our servers while being synced across all your connected devices using only a passphrase.
|
||||
</p>
|
||||
</Info>
|
||||
</div>
|
||||
{enabled && !connected && (
|
||||
<div className='mt-2 d-flex align-items-center'>
|
||||
<div>
|
||||
<Button
|
||||
variant='danger'
|
||||
onClick={reset}
|
||||
>
|
||||
Reset device sync data
|
||||
</Button>
|
||||
</div>
|
||||
<Info>
|
||||
<p>
|
||||
If you have lost your passphrase or wish to erase all encrypted data from the server, you can reset the device sync data and start over.
|
||||
</p>
|
||||
<p className='text-muted text-sm'>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</Info>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const generatePassphrase = (n = 12) => {
|
||||
const rand = new Uint32Array(n)
|
||||
window.crypto.getRandomValues(rand)
|
||||
return Array.from(rand).map(i => bip39Words[i % bip39Words.length]).join(' ')
|
||||
}
|
||||
|
||||
function ConnectForm ({ onClose, onConnect, enabled }) {
|
||||
const [passphrase, setPassphrase] = useState(!enabled ? generatePassphrase : '')
|
||||
|
||||
useEffect(() => {
|
||||
const scannedPassphrase = window.localStorage.getItem('qr:passphrase')
|
||||
if (scannedPassphrase) {
|
||||
setPassphrase(scannedPassphrase)
|
||||
window.localStorage.removeItem('qr:passphrase')
|
||||
}
|
||||
})
|
||||
|
||||
const newPassphrase = useCallback(() => {
|
||||
setPassphrase(() => generatePassphrase(12))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{!enabled ? 'Enable device sync' : 'Input your passphrase'}</h2>
|
||||
<p>
|
||||
{!enabled
|
||||
? 'Enable secure sync of sensitive data (like wallet credentials) between your devices. You’ll need to enter this passphrase on each device you want to connect.'
|
||||
: 'Enter the passphrase from device sync to access your encrypted sensitive data (like wallet credentials) on the server.'}
|
||||
</p>
|
||||
<Form
|
||||
schema={enabled ? undefined : deviceSyncSchema}
|
||||
initial={{ passphrase }}
|
||||
enableReinitialize
|
||||
onSubmit={async (values, formik) => {
|
||||
try {
|
||||
await onConnect(values, formik)
|
||||
onClose()
|
||||
} catch {}
|
||||
}}
|
||||
>
|
||||
<PasswordInput
|
||||
label='passphrase'
|
||||
name='passphrase'
|
||||
placeholder=''
|
||||
required
|
||||
autoFocus
|
||||
as='textarea'
|
||||
rows={3}
|
||||
readOnly={!enabled}
|
||||
copy={!enabled}
|
||||
append={
|
||||
!enabled && (
|
||||
<InputGroup.Text style={{ cursor: 'pointer', userSelect: 'none' }} onClick={newPassphrase}>
|
||||
<RefreshIcon width={16} height={16} />
|
||||
</InputGroup.Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<p className='text-muted text-sm'>
|
||||
{
|
||||
!enabled
|
||||
? 'This passphrase is stored only on your device and cannot be shown again.'
|
||||
: 'If you have forgotten your passphrase, you can reset and start over.'
|
||||
}
|
||||
</p>
|
||||
<div className='mt-3'>
|
||||
<div className='d-flex justify-content-between'>
|
||||
<div className='d-flex align-items-center ms-auto gap-2'>
|
||||
<CancelButton onClick={onClose} />
|
||||
<SubmitButton variant='primary'>{enabled ? 'connect' : 'enable'}</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -31,7 +31,6 @@ import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
|||
import { useField } from 'formik'
|
||||
import styles from './settings.module.css'
|
||||
import { AuthBanner } from '@/components/banners'
|
||||
import DeviceSync from '@/components/device-sync'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
|
||||
|
||||
|
@ -612,7 +611,6 @@ export default function Settings ({ ssrData }) {
|
|||
<div className='form-label'>saturday newsletter</div>
|
||||
<Button href='https://mail.stacker.news/subscription/form' target='_blank'>(re)subscribe</Button>
|
||||
{settings?.authMethods && <AuthMethods methods={settings.authMethods} apiKeyEnabled={settings.apiKeyEnabled} />}
|
||||
<DeviceSync />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
|
@ -33,7 +33,14 @@ export default function DeviceSync ({ ssrData }) {
|
|||
<Layout>
|
||||
<div className='pb-3 w-100 mt-2'>
|
||||
<SettingsHeader />
|
||||
<div className='mt-3' style={{ maxWidth: '600px' }}>
|
||||
<small className='line-height-md d-block mt-3' style={{ maxWidth: '600px' }}>
|
||||
<p>
|
||||
Device sync uses end-to-end encryption to securely synchronize your data across devices.
|
||||
|
||||
Your sensitive data remains private and inaccessible to our servers while being synced across all your connected devices using only a passphrase.
|
||||
</p>
|
||||
</small>
|
||||
<div className='mt-4' style={{ maxWidth: '600px' }}>
|
||||
{
|
||||
(connected && passphrase && <Connect passphrase={passphrase} />) ||
|
||||
(connected && <Connected disconnectVault={disconnectVault} />) ||
|
||||
|
|
|
@ -41,9 +41,11 @@ export default function WalletSettings () {
|
|||
[field.name]: wallet?.config?.[field.name] || ''
|
||||
}
|
||||
}, wallet?.config)
|
||||
if (wallet?.def.clientOnly) {
|
||||
|
||||
if (wallet?.def.fields.every(f => f.clientOnly)) {
|
||||
return initial
|
||||
}
|
||||
|
||||
return {
|
||||
...initial,
|
||||
...autowithdrawInitial({ me })
|
||||
|
|
|
@ -43,17 +43,23 @@ ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_userId_fkey" FOREIGN KEY ("u
|
|||
-- AddForeignKey
|
||||
ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE FUNCTION wallet_updated_at_trigger() RETURNS TRIGGER AS $$
|
||||
CREATE OR REPLACE FUNCTION wallet_updated_at_trigger() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE "users" SET "walletsUpdatedAt" = NOW() WHERE "id" = NEW."userId";
|
||||
RETURN NEW;
|
||||
UPDATE "users"
|
||||
SET "walletsUpdatedAt" = NOW()
|
||||
WHERE "id" = CASE
|
||||
WHEN TG_OP = 'DELETE'
|
||||
THEN OLD."userId"
|
||||
ELSE NEW."userId"
|
||||
END;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER wallet_updated_at_trigger
|
||||
AFTER INSERT OR UPDATE ON "Wallet"
|
||||
CREATE OR REPLACE TRIGGER wallet_updated_at_trigger
|
||||
AFTER INSERT OR UPDATE OR DELETE ON "Wallet"
|
||||
FOR EACH ROW EXECUTE PROCEDURE wallet_updated_at_trigger();
|
||||
|
||||
CREATE TRIGGER vault_entry_updated_at_trigger
|
||||
AFTER INSERT OR UPDATE ON "VaultEntry"
|
||||
CREATE OR REPLACE TRIGGER vault_entry_updated_at_trigger
|
||||
AFTER INSERT OR UPDATE OR DELETE ON "VaultEntry"
|
||||
FOR EACH ROW EXECUTE PROCEDURE wallet_updated_at_trigger();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { blinkSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||
|
@ -7,7 +6,6 @@ export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
|||
export const name = 'blink'
|
||||
export const walletType = 'BLINK'
|
||||
export const walletField = 'walletBlink'
|
||||
export const fieldValidation = blinkSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { decodeRune } from '@/lib/cln'
|
||||
import { B64_URL_REGEX } from '@/lib/format'
|
||||
import { CLNAutowithdrawSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'cln'
|
||||
export const walletType = 'CLN'
|
||||
export const walletField = 'walletCLN'
|
||||
export const fieldValidation = CLNAutowithdrawSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
|
|
@ -127,7 +127,7 @@ export function WalletsProvider ({ children }) {
|
|||
}
|
||||
}, [me?.privates?.autoWithdrawMaxFeePercent, me?.privates?.autoWithdrawThreshold, me?.privates?.autoWithdrawMaxFeeTotal])
|
||||
|
||||
// if the vault key is set, and we have local wallets,
|
||||
// whenever the vault key is set, and we have local wallets,
|
||||
// we'll send any merged local wallets to the server, and delete them from local storage
|
||||
const syncLocalWallets = useCallback(async encrypt => {
|
||||
const walletsToSync = wallets.filter(w =>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { string } from '@/lib/yup'
|
|||
export const name = 'lnc'
|
||||
export const walletType = 'LNC'
|
||||
export const walletField = 'walletLNC'
|
||||
export const clientOnly = true
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
|
|
@ -76,7 +76,7 @@ function composeWalletSchema (walletDef, serverSide, skipGenerated) {
|
|||
acc[name] = createFieldSchema(name, validate)
|
||||
|
||||
if (!optional) {
|
||||
acc[name] = acc[name].required('Required')
|
||||
acc[name] = acc[name].required('required')
|
||||
} else if (requiredWithout) {
|
||||
// if we are the server, the pairSetting will be in the vaultEntries array
|
||||
acc[name] = acc[name].when([serverSide ? 'vaultEntries' : requiredWithout], ([pairSetting], schema) => {
|
||||
|
|
Loading…
Reference in New Issue