Overhaul settings and implement user preferences. Also implements dark theme toggle as part of the user settings.
This commit is contained in:
parent
b84f0907a8
commit
727746030c
17 changed files with 760 additions and 378 deletions
|
@ -26,7 +26,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passkey List Section -->
|
||||
<q-card-section>
|
||||
<h5>Your Registered Passkeys</h5>
|
||||
<q-list
|
||||
|
@ -61,14 +60,12 @@
|
|||
>
|
||||
Verified just now!
|
||||
</q-item-label>
|
||||
<!-- <q-item-label caption>Registered: {{ new Date(passkey.createdAt).toLocaleDateString() }}</q-item-label> -->
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
side
|
||||
class="row no-wrap items-center"
|
||||
>
|
||||
<!-- Delete Button -->
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
|
@ -125,9 +122,10 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
|
||||
import axios from 'boot/axios';
|
||||
import { useAuthStore } from 'stores/auth';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
const registerLoading = ref(false);
|
||||
const registerErrorMessage = ref('');
|
||||
|
@ -137,27 +135,27 @@ const fetchErrorMessage = ref('');
|
|||
const deleteLoading = ref(null);
|
||||
const deleteErrorMessage = ref('');
|
||||
const deleteSuccessMessage = ref('');
|
||||
const identifyLoading = ref(null); // Store the ID of the passkey being identified
|
||||
const identifyLoading = ref(null);
|
||||
const identifyErrorMessage = ref('');
|
||||
const identifiedPasskeyId = ref(null); // Store the ID of the successfully identified passkey
|
||||
const identifiedPasskeyId = ref(null);
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const passkeys = ref([]); // To store the list of passkeys
|
||||
const passkeys = ref([]);
|
||||
|
||||
// Computed properties to get state from the store
|
||||
const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||
const username = computed(() => authStore.user?.username);
|
||||
|
||||
// Fetch existing passkeys
|
||||
async function fetchPasskeys()
|
||||
{
|
||||
if (!isLoggedIn.value) return;
|
||||
fetchLoading.value = true;
|
||||
fetchErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = ''; // Clear delete messages on refresh
|
||||
deleteSuccessMessage.value = '';
|
||||
deleteErrorMessage.value = '';
|
||||
identifyErrorMessage.value = ''; // Clear identify message
|
||||
identifiedPasskeyId.value = null; // Clear identified key
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/auth/passkeys');
|
||||
|
@ -167,7 +165,7 @@ async function fetchPasskeys()
|
|||
{
|
||||
console.error('Error fetching passkeys:', error);
|
||||
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
||||
passkeys.value = []; // Clear passkeys on error
|
||||
passkeys.value = [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -175,7 +173,6 @@ async function fetchPasskeys()
|
|||
}
|
||||
}
|
||||
|
||||
// Check auth status and fetch passkeys on component mount
|
||||
onMounted(async() =>
|
||||
{
|
||||
let initialAuthError = '';
|
||||
|
@ -189,12 +186,11 @@ onMounted(async() =>
|
|||
}
|
||||
if (!isLoggedIn.value)
|
||||
{
|
||||
// Use register error message ref for consistency if login is required first
|
||||
registerErrorMessage.value = initialAuthError || 'You must be logged in to manage passkeys.';
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchPasskeys(); // Fetch passkeys if logged in
|
||||
fetchPasskeys();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -208,23 +204,20 @@ async function handleRegister()
|
|||
registerLoading.value = true;
|
||||
registerErrorMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
deleteSuccessMessage.value = ''; // Clear other messages
|
||||
deleteSuccessMessage.value = '';
|
||||
deleteErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Get options from server
|
||||
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
|
||||
username: username.value, // Use username from store
|
||||
username: username.value,
|
||||
});
|
||||
const options = optionsRes.data;
|
||||
|
||||
// 2. Start registration ceremony in browser
|
||||
const regResp = await startRegistration(options);
|
||||
|
||||
// 3. Send response to server for verification
|
||||
const verificationRes = await axios.post('/api/auth/verify-registration', {
|
||||
registrationResponse: regResp,
|
||||
});
|
||||
|
@ -232,7 +225,7 @@ async function handleRegister()
|
|||
if (verificationRes.data.verified)
|
||||
{
|
||||
registerSuccessMessage.value = 'New passkey registered successfully!';
|
||||
fetchPasskeys(); // Refresh the list of passkeys
|
||||
fetchPasskeys();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -243,7 +236,6 @@ async function handleRegister()
|
|||
{
|
||||
console.error('Registration error:', error);
|
||||
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
||||
// Handle specific simplewebauthn errors
|
||||
if (error.name === 'InvalidStateError')
|
||||
{
|
||||
registerErrorMessage.value = 'Authenticator may already be registered.';
|
||||
|
@ -268,42 +260,63 @@ async function handleRegister()
|
|||
}
|
||||
|
||||
|
||||
// Handle deleting a passkey
|
||||
async function handleDelete(credentialID)
|
||||
{
|
||||
if (!credentialID) return;
|
||||
|
||||
// Optional: Add a confirmation dialog here
|
||||
// if (!confirm('Are you sure you want to delete this passkey?')) {
|
||||
// return;
|
||||
// }
|
||||
if (passkeys.value.length <= 1)
|
||||
{
|
||||
deleteErrorMessage.value = 'You cannot delete your last passkey. Register another one first.';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
deleteLoading.value = credentialID; // Set loading state for the specific button
|
||||
deleteErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = ''; // Clear other messages
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
$q.dialog({
|
||||
title: 'Confirm Deletion',
|
||||
message: 'Are you sure you want to delete this passkey? This action cannot be undone.',
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
ok: {
|
||||
label: 'Delete',
|
||||
color: 'negative',
|
||||
flat: true,
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
flat: true,
|
||||
},
|
||||
}).onOk(async() =>
|
||||
{
|
||||
deleteLoading.value = credentialID;
|
||||
deleteErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try
|
||||
{
|
||||
await axios.delete(`/api/auth/passkeys/${credentialID}`);
|
||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||
fetchPasskeys(); // Refresh the list
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteLoading.value = null; // Clear loading state
|
||||
}
|
||||
try
|
||||
{
|
||||
await axios.delete(`/api/auth/passkeys/${credentialID}`);
|
||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||
fetchPasskeys();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteLoading.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle identifying a passkey
|
||||
async function handleIdentify()
|
||||
{
|
||||
if (!isLoggedIn.value)
|
||||
|
@ -314,8 +327,7 @@ async function handleIdentify()
|
|||
|
||||
identifyLoading.value = true;
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null; // Reset identified key
|
||||
// Clear other messages
|
||||
identifiedPasskeyId.value = null;
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
|
@ -323,30 +335,21 @@ async function handleIdentify()
|
|||
|
||||
try
|
||||
{
|
||||
// 1. Get authentication options from the server
|
||||
// We don't need to send username as the server should use the session
|
||||
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {}); // Send empty body
|
||||
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {});
|
||||
const options = optionsRes.data;
|
||||
|
||||
// Optionally filter options to only allow the specific key if needed, but usually not necessary for identification
|
||||
// options.allowCredentials = options.allowCredentials?.filter(cred => cred.id === credentialIDToIdentify);
|
||||
|
||||
// 2. Start authentication ceremony in the browser
|
||||
const authResp = await startAuthentication(options);
|
||||
|
||||
// 3. If successful, the response contains the ID of the key used
|
||||
identifiedPasskeyId.value = authResp.id;
|
||||
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
|
||||
|
||||
// Optional: Add a small delay before clearing the highlight
|
||||
setTimeout(() =>
|
||||
{
|
||||
// Only clear if it's still the same identified key
|
||||
if (identifiedPasskeyId.value === authResp.id)
|
||||
{
|
||||
identifiedPasskeyId.value = null;
|
||||
}
|
||||
}, 5000); // Clear highlight after 5 seconds
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
catch (error)
|
||||
|
@ -364,7 +367,7 @@ async function handleIdentify()
|
|||
}
|
||||
finally
|
||||
{
|
||||
identifyLoading.value = null; // Clear loading state
|
||||
identifyLoading.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue