feat: improve account security
Support TOTP 2fa, allow for changing password, add session management and account removal.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import CodeInput from '~/components/CodeInput.vue';
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel']);
|
||||
|
||||
const { user, session } = useAuth();
|
||||
|
||||
const password = ref('');
|
||||
|
||||
// accounts with two-factor also have to prove the second factor
|
||||
const needsCode = ref(!!user.value?.twoFactorEnabled);
|
||||
const code = ref('');
|
||||
const useBackupCode = ref(false);
|
||||
|
||||
const codeReady = computed(() => {
|
||||
if (!needsCode.value) return true;
|
||||
|
||||
return useBackupCode.value ? code.value.trim() !== '' : code.value.length === 6;
|
||||
});
|
||||
|
||||
const toggleBackupCode = () => {
|
||||
useBackupCode.value = !useBackupCode.value;
|
||||
code.value = '';
|
||||
error.value = '';
|
||||
};
|
||||
const busy = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
const passwordInputEl = ref<HTMLInputElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
passwordInputEl.value?.focus();
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
if (busy.value || password.value === '' || !codeReady.value) return;
|
||||
|
||||
busy.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
await $fetch('/api/account', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
password: password.value,
|
||||
code: needsCode.value ? code.value.trim() : undefined,
|
||||
backupCode: needsCode.value ? useBackupCode.value : undefined,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
switch (err?.data?.data?.code as string | undefined) {
|
||||
case 'INVALID_PASSWORD':
|
||||
error.value = 'That password is not right. Nothing was deleted.';
|
||||
password.value = '';
|
||||
break;
|
||||
case 'INVALID_CODE':
|
||||
error.value = 'That code is not right. Check your authenticator app. Nothing was deleted.';
|
||||
code.value = '';
|
||||
break;
|
||||
case 'INVALID_BACKUP_CODE':
|
||||
error.value = 'That backup code is not valid, or it was already used. Nothing was deleted.';
|
||||
code.value = '';
|
||||
break;
|
||||
case 'TWO_FACTOR_REQUIRED':
|
||||
// two-factor was turned on somewhere else since this page loaded
|
||||
needsCode.value = true;
|
||||
error.value = 'This account has two-factor sign-in on. Enter a code to continue.';
|
||||
break;
|
||||
default:
|
||||
error.value = 'Your account could not be deleted. Nothing was deleted, try again.';
|
||||
break;
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// nothing of this account is left, so drop every local trace of it too
|
||||
localStorage.removeItem('encryptionKey');
|
||||
user.value = null;
|
||||
session.value = null;
|
||||
clearNuxtData();
|
||||
|
||||
emit('confirm', true);
|
||||
|
||||
await navigateTo('/auth/login');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-col gap-4.5 p-6 w-screen md:w-130 max-w-full h-full md:h-auto overflow-y-auto"
|
||||
aria-labelledby="delete-account-title" @submit.prevent="submit">
|
||||
<header class="flex flex-col gap-1">
|
||||
<h2 id="delete-account-title" class="text-lg font-semibold">Delete your account</h2>
|
||||
<p class="text-sm text-[var(--text-secondary)]">
|
||||
This can't be undone.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<p class="text-sm leading-normal">
|
||||
Deleting <strong class="font-medium">{{ user?.email }}</strong> removes your agents, topics and
|
||||
messages, your providers with their saved API keys, your file list, and your settings from this
|
||||
instance. You are signed out everywhere.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="delete-account-password"
|
||||
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
||||
Password
|
||||
</label>
|
||||
<input id="delete-account-password" ref="passwordInputEl" v-model="password" type="password" required
|
||||
:disabled="busy" autocomplete="current-password"
|
||||
class="w-full h-9 px-2.5 text-sm rounded-md bg-[var(--bg-container)]" />
|
||||
</div>
|
||||
|
||||
<div v-if="needsCode && useBackupCode" class="flex flex-col gap-1.5">
|
||||
<label for="delete-account-backup-code"
|
||||
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
||||
Backup code
|
||||
</label>
|
||||
<input id="delete-account-backup-code" v-model="code" type="text" required :disabled="busy"
|
||||
autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="xxxxx-xxxxx"
|
||||
class="w-full h-9 px-2.5 text-sm font-mono rounded-md bg-[var(--bg-container)] placeholder:text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<fieldset v-else-if="needsCode" class="flex flex-col">
|
||||
<legend class="mb-2 text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
||||
6-digit code from your authenticator app
|
||||
</legend>
|
||||
<CodeInput v-model="code" :disabled="busy" />
|
||||
</fieldset>
|
||||
|
||||
<button v-if="needsCode" type="button" :disabled="busy" @click="toggleBackupCode"
|
||||
class="self-start -mt-2 text-sm text-[var(--color-accent)] font-medium @hover:underline">
|
||||
{{ useBackupCode ? 'Use your authenticator app instead' : 'Use a backup code instead' }}
|
||||
</button>
|
||||
|
||||
<p v-if="error" role="alert" class="text-sm text-red-400">{{ error }}</p>
|
||||
|
||||
<footer class="flex justify-between gap-2 mt-auto">
|
||||
<button type="button" :disabled="busy"
|
||||
class="flex items-center h-10 px-4 rounded-md border border-[var(--color-border)] text-[var(--text-secondary)] @hover:bg-[var(--color-hover)] disabled:opacity-50 disabled:cursor-not-allowed transition-[background-color] duration-200"
|
||||
@click="emit('cancel')">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" :disabled="busy || password === '' || !codeReady"
|
||||
class="flex items-center gap-2 h-10 px-4 rounded-md font-semibold bg-red-600 text-white disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span v-if="busy" class="i-svg-spinners-180-ring-with-bg text-4"></span>
|
||||
Delete account
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
Reference in New Issue
Block a user