mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
feat: backup and security settings pages, nostr restore onboarding, passkey unlock entry
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row items-center q-mb-md">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
color="primary"
|
||||
icon="arrow_back"
|
||||
aria-label="Back"
|
||||
@click="router.push('/settings')"
|
||||
/>
|
||||
<div class="text-h5 text-weight-bold text-primary q-ml-sm">Security</div>
|
||||
</div>
|
||||
|
||||
<!-- passkeys -->
|
||||
<q-list class="sattle-card q-mb-md" bordered separator>
|
||||
<q-item-label header class="text-primary text-weight-bold">Passkeys</q-item-label>
|
||||
|
||||
<q-item v-if="supported === null">
|
||||
<q-item-section class="text-grey-5">Checking passkey support…</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item v-else-if="supported === false">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey-3">Passkeys aren't available here</q-item-label>
|
||||
<q-item-label caption class="text-grey-5" style="white-space: normal">
|
||||
This browser has no passkey authenticator (like Touch ID, Windows Hello or Android
|
||||
biometrics) with the encryption support sattle needs. Your password unlock keeps working
|
||||
- nothing to do.
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<template v-else>
|
||||
<q-item v-if="wallet.state !== 'unlocked'">
|
||||
<q-item-section class="text-grey-5">
|
||||
Unlock your wallet first - adding a passkey needs the wallet's key in memory.
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<template v-else>
|
||||
<q-item v-if="!slots.length">
|
||||
<q-item-section class="text-grey-5">
|
||||
No passkeys yet. A passkey lets you unlock this wallet with your device's screen lock
|
||||
instead of typing the password.
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-for="slot in slots" :key="slot.credentialId">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey-3">{{ slot.name || 'Passkey' }}</q-item-label>
|
||||
<q-item-label caption class="text-grey-5">
|
||||
Added {{ new Date(slot.createdAt).toLocaleDateString() }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn flat dense no-caps color="negative" label="Remove" @click="askRemove(slot)" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<div class="q-pa-md">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
text-color="dark"
|
||||
icon="fingerprint"
|
||||
label="Add a passkey"
|
||||
class="full-width"
|
||||
:loading="registerBusy"
|
||||
@click="askRegister"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</q-list>
|
||||
|
||||
<!-- auto-lock -->
|
||||
<q-list class="sattle-card q-mb-md" bordered>
|
||||
<q-item-label header class="text-primary text-weight-bold">Auto-lock</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey-3"> Locks after 5 minutes without activity </q-item-label>
|
||||
<q-item-label caption class="text-grey-5" style="white-space: normal">
|
||||
Applies when your wallet is password-protected. You get a 30-second warning with a "stay
|
||||
unlocked" option before it locks. The duration isn't configurable yet.
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-banner v-if="banner" dense class="bg-negative text-white rounded-borders q-mb-md">
|
||||
{{ banner }}
|
||||
</q-banner>
|
||||
|
||||
<!-- add-passkey dialog: name it, then the authenticator ceremony runs -->
|
||||
<q-dialog v-model="registering">
|
||||
<q-card class="sattle-card q-pa-lg">
|
||||
<div class="text-h6 text-primary q-mb-sm">Add a passkey</div>
|
||||
<div class="text-body2 text-grey-4 q-mb-md">
|
||||
Your device will ask for its screen lock. The passkey only ever unlocks this wallet on
|
||||
this device - it never leaves it.
|
||||
</div>
|
||||
<q-input
|
||||
v-model="registerName"
|
||||
dark
|
||||
outlined
|
||||
color="primary"
|
||||
label="Name (optional)"
|
||||
placeholder="e.g. laptop"
|
||||
autocomplete="off"
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<div class="row q-gutter-sm justify-end">
|
||||
<q-btn v-close-popup flat no-caps color="grey-5" label="Cancel" />
|
||||
<q-btn
|
||||
unelevated
|
||||
no-caps
|
||||
color="primary"
|
||||
text-color="dark"
|
||||
label="Create passkey"
|
||||
:loading="registerBusy"
|
||||
@click="doRegister"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- remove confirmation -->
|
||||
<q-dialog v-model="confirmingRemove">
|
||||
<q-card class="sattle-card q-pa-lg">
|
||||
<div class="text-h6 text-primary q-mb-sm">Remove passkey</div>
|
||||
<div class="text-body2 text-grey-4 q-mb-md">
|
||||
Remove <strong>{{ removeTarget?.name || 'this passkey' }}</strong
|
||||
>? It will no longer unlock this wallet. You can remove it from your device's passkey list
|
||||
separately - the wallet can't do that for you.
|
||||
</div>
|
||||
<div class="row q-gutter-sm justify-end">
|
||||
<q-btn v-close-popup flat no-caps color="grey-5" label="Cancel" />
|
||||
<q-btn
|
||||
unelevated
|
||||
no-caps
|
||||
color="negative"
|
||||
text-color="white"
|
||||
label="Remove"
|
||||
@click="doRemove"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import type { PasskeySlot } from '@/lnurlcash/passkeys';
|
||||
import {
|
||||
passkeySupported,
|
||||
readPasskeySlots,
|
||||
registerPasskey,
|
||||
removePasskey,
|
||||
} from '@/lnurlcash/passkeys';
|
||||
import { useWalletStore } from '@/stores/wallet';
|
||||
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
const wallet = useWalletStore();
|
||||
|
||||
const toast = (type: 'positive' | 'negative', message: string): void => {
|
||||
if (typeof $q.notify === 'function') {
|
||||
$q.notify({ type, message, position: 'top', timeout: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
const banner = ref('');
|
||||
|
||||
// null while the async probe runs - the support answer decides which of
|
||||
// the three states (checking / unsupported / manage) renders
|
||||
const supported = ref<boolean | null>(null);
|
||||
const slots = ref<PasskeySlot[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
supported.value = await passkeySupported();
|
||||
slots.value = readPasskeySlots();
|
||||
});
|
||||
|
||||
// ---- register ----
|
||||
const registering = ref(false);
|
||||
const registerName = ref('');
|
||||
const registerBusy = ref(false);
|
||||
|
||||
const askRegister = () => {
|
||||
banner.value = '';
|
||||
registerName.value = '';
|
||||
registering.value = true;
|
||||
};
|
||||
|
||||
const doRegister = async () => {
|
||||
registerBusy.value = true;
|
||||
banner.value = '';
|
||||
try {
|
||||
const name = registerName.value.trim();
|
||||
await registerPasskey(wallet.requireLinkingKey(), name ? { name } : {});
|
||||
slots.value = readPasskeySlots();
|
||||
registering.value = false;
|
||||
toast('positive', 'Passkey added.');
|
||||
} catch (err) {
|
||||
banner.value = err instanceof Error ? err.message : 'Could not add that passkey.';
|
||||
registering.value = false;
|
||||
} finally {
|
||||
registerBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- remove ----
|
||||
const confirmingRemove = ref(false);
|
||||
const removeTarget = ref<PasskeySlot | null>(null);
|
||||
|
||||
const askRemove = (slot: PasskeySlot) => {
|
||||
banner.value = '';
|
||||
removeTarget.value = slot;
|
||||
confirmingRemove.value = true;
|
||||
};
|
||||
|
||||
const doRemove = async () => {
|
||||
confirmingRemove.value = false;
|
||||
if (!removeTarget.value) return;
|
||||
await removePasskey(removeTarget.value.credentialId);
|
||||
slots.value = readPasskeySlots();
|
||||
toast('positive', 'Passkey removed.');
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user