Added linting and enforced code styling.
This commit is contained in:
parent
8655eae39c
commit
86967b26cd
37 changed files with 3356 additions and 1875 deletions
|
|
@ -1,9 +1,11 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div class="q-mb-md row justify-between items-center">
|
||||
<div class="text-h4">Passkey Management</div>
|
||||
<div class="text-h4">
|
||||
Passkey Management
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
<q-btn
|
||||
label="Identify Passkey"
|
||||
color="secondary"
|
||||
class="q-mx-md q-mt-md"
|
||||
|
|
@ -11,8 +13,8 @@
|
|||
:loading="identifyLoading"
|
||||
:disable="identifyLoading || !isLoggedIn"
|
||||
outline
|
||||
/>
|
||||
<q-btn
|
||||
/>
|
||||
<q-btn
|
||||
label="Register New Passkey"
|
||||
color="primary"
|
||||
class="q-mx-md q-mt-md"
|
||||
|
|
@ -20,17 +22,31 @@
|
|||
:loading="registerLoading"
|
||||
:disable="registerLoading || !isLoggedIn"
|
||||
outline
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passkey List Section -->
|
||||
<q-card-section>
|
||||
<h5>Your Registered Passkeys</h5>
|
||||
<q-list bordered separator v-if="passkeys.length > 0 && !fetchLoading">
|
||||
<q-list
|
||||
bordered
|
||||
separator
|
||||
v-if="passkeys.length > 0 && !fetchLoading"
|
||||
>
|
||||
<q-item v-if="registerSuccessMessage || registerErrorMessage">
|
||||
<div v-if="registerSuccessMessage" class="text-positive q-mt-md">{{ registerSuccessMessage }}</div>
|
||||
<div v-if="registerErrorMessage" class="text-negative q-mt-md">{{ registerErrorMessage }}</div>
|
||||
<div
|
||||
v-if="registerSuccessMessage"
|
||||
class="text-positive q-mt-md"
|
||||
>
|
||||
{{ registerSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="registerErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ registerErrorMessage }}
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="passkey in passkeys"
|
||||
|
|
@ -39,14 +55,19 @@
|
|||
>
|
||||
<q-item-section>
|
||||
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label>
|
||||
<q-item-label caption v-if="identifiedPasskeyId === passkey.credentialID">
|
||||
<q-item-label
|
||||
caption
|
||||
v-if="identifiedPasskeyId === passkey.credentialID"
|
||||
>
|
||||
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">
|
||||
|
||||
<q-item-section
|
||||
side
|
||||
class="row no-wrap items-center"
|
||||
>
|
||||
<!-- Delete Button -->
|
||||
<q-btn
|
||||
flat
|
||||
|
|
@ -61,16 +82,44 @@
|
|||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div v-else-if="fetchLoading" class="q-mt-md">Loading passkeys...</div>
|
||||
<div v-else class="q-mt-md">You have no passkeys registered yet.</div>
|
||||
|
||||
<div v-if="fetchErrorMessage" class="text-negative q-mt-md">{{ fetchErrorMessage }}</div>
|
||||
<div v-if="deleteSuccessMessage" class="text-positive q-mt-md">{{ deleteSuccessMessage }}</div>
|
||||
<div v-if="deleteErrorMessage" class="text-negative q-mt-md">{{ deleteErrorMessage }}</div>
|
||||
<div v-if="identifyErrorMessage" class="text-negative q-mt-md">{{ identifyErrorMessage }}</div>
|
||||
|
||||
</q-card-section>
|
||||
<div
|
||||
v-else-if="fetchLoading"
|
||||
class="q-mt-md"
|
||||
>
|
||||
Loading passkeys...
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="q-mt-md"
|
||||
>
|
||||
You have no passkeys registered yet.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="fetchErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ fetchErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteSuccessMessage"
|
||||
class="text-positive q-mt-md"
|
||||
>
|
||||
{{ deleteSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ deleteErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="identifyErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ identifyErrorMessage }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
|
@ -100,7 +149,8 @@ const isLoggedIn = computed(() => authStore.isAuthenticated);
|
|||
const username = computed(() => authStore.user?.username);
|
||||
|
||||
// Fetch existing passkeys
|
||||
async function fetchPasskeys() {
|
||||
async function fetchPasskeys()
|
||||
{
|
||||
if (!isLoggedIn.value) return;
|
||||
fetchLoading.value = true;
|
||||
fetchErrorMessage.value = '';
|
||||
|
|
@ -108,37 +158,50 @@ async function fetchPasskeys() {
|
|||
deleteErrorMessage.value = '';
|
||||
identifyErrorMessage.value = ''; // Clear identify message
|
||||
identifiedPasskeyId.value = null; // Clear identified key
|
||||
try {
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/auth/passkeys');
|
||||
passkeys.value = response.data || [];
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching passkeys:', error);
|
||||
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
||||
passkeys.value = []; // Clear passkeys on error
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
fetchLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check auth status and fetch passkeys on component mount
|
||||
onMounted(async () => {
|
||||
onMounted(async() =>
|
||||
{
|
||||
let initialAuthError = '';
|
||||
if (!authStore.isAuthenticated) {
|
||||
if (!authStore.isAuthenticated)
|
||||
{
|
||||
await authStore.checkAuthStatus();
|
||||
if (authStore.error) {
|
||||
initialAuthError = `Authentication error: ${authStore.error}`;
|
||||
if (authStore.error)
|
||||
{
|
||||
initialAuthError = `Authentication error: ${authStore.error}`;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
async function handleRegister() {
|
||||
if (!isLoggedIn.value || !username.value) {
|
||||
async function handleRegister()
|
||||
{
|
||||
if (!isLoggedIn.value || !username.value)
|
||||
{
|
||||
registerErrorMessage.value = 'User not authenticated.';
|
||||
return;
|
||||
}
|
||||
|
|
@ -150,7 +213,8 @@ async function handleRegister() {
|
|||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// 1. Get options from server
|
||||
const optionsRes = await axios.post('/auth/generate-registration-options', {
|
||||
username: username.value, // Use username from store
|
||||
|
|
@ -165,33 +229,48 @@ async function handleRegister() {
|
|||
registrationResponse: regResp,
|
||||
});
|
||||
|
||||
if (verificationRes.data.verified) {
|
||||
if (verificationRes.data.verified)
|
||||
{
|
||||
registerSuccessMessage.value = 'New passkey registered successfully!';
|
||||
fetchPasskeys(); // Refresh the list of passkeys
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
registerErrorMessage.value = 'Passkey verification failed.';
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
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') {
|
||||
if (error.name === 'InvalidStateError')
|
||||
{
|
||||
registerErrorMessage.value = 'Authenticator may already be registered.';
|
||||
} else if (error.name === 'NotAllowedError') {
|
||||
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||
} else if (error.response?.status === 409) {
|
||||
registerErrorMessage.value = 'This passkey seems to be registered already.';
|
||||
} else {
|
||||
}
|
||||
else if (error.name === 'NotAllowedError')
|
||||
{
|
||||
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||
}
|
||||
else if (error.response?.status === 409)
|
||||
{
|
||||
registerErrorMessage.value = 'This passkey seems to be registered already.';
|
||||
}
|
||||
else
|
||||
{
|
||||
registerErrorMessage.value = `Registration failed: ${message}`;
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
registerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle deleting a passkey
|
||||
async function handleDelete(credentialID) {
|
||||
async function handleDelete(credentialID)
|
||||
{
|
||||
if (!credentialID) return;
|
||||
|
||||
// Optional: Add a confirmation dialog here
|
||||
|
|
@ -207,21 +286,28 @@ async function handleDelete(credentialID) {
|
|||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
await axios.delete(`/auth/passkeys/${credentialID}`);
|
||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||
fetchPasskeys(); // Refresh the list
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteLoading.value = null; // Clear loading state
|
||||
}
|
||||
}
|
||||
|
||||
// Handle identifying a passkey
|
||||
async function handleIdentify() {
|
||||
if (!isLoggedIn.value) {
|
||||
async function handleIdentify()
|
||||
{
|
||||
if (!isLoggedIn.value)
|
||||
{
|
||||
identifyErrorMessage.value = 'You must be logged in.';
|
||||
return;
|
||||
}
|
||||
|
|
@ -235,7 +321,8 @@ async function handleIdentify() {
|
|||
deleteSuccessMessage.value = '';
|
||||
deleteErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
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('/auth/generate-authentication-options', {}); // Send empty body
|
||||
|
|
@ -252,22 +339,31 @@ async function handleIdentify() {
|
|||
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;
|
||||
}
|
||||
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
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Identification error:', error);
|
||||
identifiedPasskeyId.value = null;
|
||||
if (error.name === 'NotAllowedError') {
|
||||
if (error.name === 'NotAllowedError')
|
||||
{
|
||||
identifyErrorMessage.value = 'Identification ceremony was cancelled or timed out.';
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
identifyErrorMessage.value = error.response?.data?.error || error.message || 'Failed to identify passkey.';
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
identifyLoading.value = null; // Clear loading state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue