Compare commits

..

No commits in common. "f96668b135fd3e63ee45f9e0abce469b610d0895" and "ba5f2d8bd9361b671e658b125668333014bace88" have entirely different histories.

6 changed files with 79 additions and 67 deletions

View File

@ -5,7 +5,7 @@
<RegisterView v-if="store.currentView === 'register'" />
<ProfileView v-if="store.currentView === 'profile'" />
<DeviceLinkView v-if="store.currentView === 'device-link'" />
<ResetView v-if="store.currentView === 'reset'" />
<AddCredentialView v-if="store.currentView === 'add-credential'" />
</div>
</template>
@ -17,7 +17,7 @@ import LoginView from '@/components/LoginView.vue'
import RegisterView from '@/components/RegisterView.vue'
import ProfileView from '@/components/ProfileView.vue'
import DeviceLinkView from '@/components/DeviceLinkView.vue'
import ResetView from '@/components/ResetView.vue'
import AddCredentialView from '@/components/AddCredentialView.vue'
const store = useAuthStore()
@ -34,6 +34,15 @@ onMounted(async () => {
console.log('Failed to load user info:', error)
store.currentView = 'login'
}
store.selectView()
if (store.currentCredentials.length) {
// User is logged in, go to profile
store.currentView = 'profile'
} else if (store.currentUser) {
// User is logged in via reset link, allow adding a credential
store.currentView = 'add-credential'
} else {
// User is not logged in, show login
store.currentView = 'login'
}
})
</script>

View File

@ -1,9 +1,8 @@
<template>
<div class="container">
<div class="view active">
<h1>🔑 Add New Credential</h1>
<h3>👤 {{ authStore.userInfo?.user?.user_name }}</h3>
<p>Proceed to complete {{authStore.userInfo?.session_type}}:</p>
<h1>🔑 Add Device Credential</h1>
<h3>👤 {{ authStore.currentUser.user_name }}</h3>
<button
class="btn-primary"
:disabled="authStore.isLoading"
@ -17,24 +16,34 @@
<script setup>
import { useAuthStore } from '@/stores/auth'
import { computed } from 'vue'
import { registerCredential } from '@/utils/passkey'
const authStore = useAuthStore()
const hasDeviceSession = computed(() => !!authStore.currentUser)
async function register() {
if (!hasDeviceSession.value) {
authStore.showMessage('No valid device addition session', 'error')
return
}
authStore.isLoading = true
authStore.showMessage('Starting registration...', 'info')
try {
// TODO: For reset sessions, might use registerWithToken() in the future
const result = await registerCredential()
console.log("Result", result)
await authStore.setSessionCookie(result.session_token)
authStore.showMessage('Passkey registered successfully!', 'success', 2000)
authStore.loadUserInfo().then(authStore.selectView)
authStore.currentView = 'profile'
} catch (error) {
authStore.showMessage(`Registration failed: ${error.message}`, 'error')
console.error('Registration error:', error)
if (error.name === "NotAllowedError") {
authStore.showMessage('Registration cancelled', 'error')
} else {
authStore.showMessage(`Registration failed: ${error.message}`, 'error')
}
} finally {
authStore.isLoading = false
}

View File

@ -37,7 +37,7 @@ const handleLogin = async () => {
location.reload()
}
} catch (error) {
authStore.showMessage(error.message, 'error')
authStore.showMessage(`Authentication failed: ${error.message}`, 'error')
}
}
</script>

View File

@ -2,14 +2,14 @@
<div class="container">
<div class="view active">
<h1>👋 Welcome!</h1>
<div v-if="authStore.userInfo?.user" class="user-info">
<h3>👤 {{ authStore.userInfo.user.user_name }}</h3>
<div v-if="authStore.currentUser" class="user-info">
<h3>👤 {{ authStore.currentUser.user_name }}</h3>
<span><strong>Visits:</strong></span>
<span>{{ authStore.userInfo.user.visits || 0 }}</span>
<span>{{ authStore.currentUser.visits || 0 }}</span>
<span><strong>Registered:</strong></span>
<span>{{ formatDate(authStore.userInfo.user.created_at) }}</span>
<span>{{ formatDate(authStore.currentUser.created_at) }}</span>
<span><strong>Last seen:</strong></span>
<span>{{ formatDate(authStore.userInfo.user.last_seen) }}</span>
<span>{{ formatDate(authStore.currentUser.last_seen) }}</span>
</div>
<h2>Your Passkeys</h2>
@ -17,12 +17,12 @@
<div v-if="authStore.isLoading">
<p>Loading credentials...</p>
</div>
<div v-else-if="authStore.userInfo?.credentials?.length === 0">
<div v-else-if="authStore.currentCredentials.length === 0">
<p>No passkeys found.</p>
</div>
<div v-else>
<div
v-for="credential in authStore.userInfo?.credentials || []"
v-for="credential in authStore.currentCredentials"
:key="credential.credential_uuid"
:class="['credential-item', { 'current-session': credential.is_current_session }]"
>
@ -86,12 +86,19 @@ import { registerCredential } from '@/utils/passkey'
const authStore = useAuthStore()
const updateInterval = ref(null)
onMounted(() => {
onMounted(async () => {
try {
await authStore.loadUserInfo()
} catch (error) {
authStore.showMessage(`Failed to load user info: ${error.message}`, 'error')
authStore.currentView = 'login'
return
}
updateInterval.value = setInterval(() => {
// Trigger Vue reactivity to update formatDate fields
if (authStore.userInfo) {
authStore.userInfo = { ...authStore.userInfo }
}
authStore.currentUser = { ...authStore.currentUser }
authStore.currentCredentials = [...authStore.currentCredentials]
}, 60000) // Update every minute
})
@ -102,12 +109,12 @@ onUnmounted(() => {
})
const getCredentialAuthName = (credential) => {
const authInfo = authStore.userInfo?.aaguid_info?.[credential.aaguid]
const authInfo = authStore.aaguidInfo[credential.aaguid]
return authInfo ? authInfo.name : 'Unknown Authenticator'
}
const getCredentialAuthIcon = (credential) => {
const authInfo = authStore.userInfo?.aaguid_info?.[credential.aaguid]
const authInfo = authStore.aaguidInfo[credential.aaguid]
if (!authInfo) return null
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
@ -124,7 +131,7 @@ const addNewCredential = async () => {
authStore.showMessage('New passkey added successfully!', 'success', 3000)
} catch (error) {
console.error('Failed to add new passkey:', error)
authStore.showMessage(error.message, 'error')
authStore.showMessage(`Failed to add passkey: ${error.message}`, 'error')
} finally {
authStore.isLoading = false
}

View File

@ -4,11 +4,13 @@ import { registerUser, authenticateUser, registerWithToken } from '@/utils/passk
export const useAuthStore = defineStore('auth', {
state: () => ({
// Auth State
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info, session_type, authenticated}
currentUser: null,
currentCredentials: [],
aaguidInfo: {},
isLoading: false,
// UI State
currentView: 'login', // 'login', 'register', 'profile', 'reset'
currentView: 'login', // 'login', 'register', 'profile', 'device-link'
status: {
message: '',
type: 'info',
@ -44,15 +46,9 @@ export const useAuthStore = defineStore('auth', {
try {
const result = await registerUser(user_name)
this.userInfo = {
user: {
user_id: result.user_id,
user_name: user_name,
},
credentials: [],
aaguid_info: {},
session_type: null,
authenticated: false
this.currentUser = {
user_id: result.user_id,
user_name: user_name,
}
await this.setSessionCookie(result.session_token)
@ -74,16 +70,15 @@ export const useAuthStore = defineStore('auth', {
this.isLoading = false
}
},
selectView() {
if (!this.userInfo) this.currentView = 'login'
else if (this.userInfo.authenticated) this.currentView = 'profile'
else this.currentView = 'reset'
},
async loadUserInfo() {
const response = await fetch('/auth/user-info', {method: 'POST'})
const result = await response.json()
if (result.detail) throw new Error(`Server: ${result.detail}`)
this.userInfo = result
this.currentUser = result.user
this.currentCredentials = result.credentials || []
this.aaguidInfo = result.aaguid_info || {}
if (result.session_type === 'device addition') this.currentView = 'add-credential'
console.log('User info loaded:', result)
},
async deleteCredential(uuid) {
@ -100,7 +95,9 @@ export const useAuthStore = defineStore('auth', {
console.error('Logout error:', error)
}
this.userInfo = null
this.currentUser = null
this.currentCredentials = []
this.aaguidInfo = {}
},
}
})

View File

@ -4,18 +4,13 @@ import aWebSocket from '@/utils/awaitable-websocket'
export async function register(url, options) {
if (options) url += `?${new URLSearchParams(options).toString()}`
const ws = await aWebSocket(url)
try {
const optionsJSON = await ws.receive_json()
const registrationResponse = await startRegistration({ optionsJSON })
ws.send_json(registrationResponse)
const result = await ws.receive_json()
} catch (error) {
console.error('Registration error:', error)
// Replace useless and ugly error message from startRegistration
throw Error(error.name === "NotAllowedError" ? 'Passkey registration cancelled' : error.message)
} finally {
ws.close()
}
const optionsJSON = await ws.receive_json()
const registrationResponse = await startRegistration({ optionsJSON })
ws.send_json(registrationResponse)
const result = await ws.receive_json()
ws.close()
return result;
}
export async function registerUser(user_name) {
@ -31,16 +26,11 @@ export async function registerWithToken(token) {
export async function authenticateUser() {
const ws = await aWebSocket('/auth/ws/authenticate')
try {
const optionsJSON = await ws.receive_json()
const authResponse = await startAuthentication({ optionsJSON })
ws.send_json(authResponse)
const result = await ws.receive_json()
return result
} catch (error) {
console.error('Authentication error:', error)
throw Error(error.name === "NotAllowedError" ? 'Passkey authentication cancelled' : error.message)
} finally {
ws.close()
}
const optionsJSON = await ws.receive_json()
const authResponse = await startAuthentication({ optionsJSON })
ws.send_json(authResponse)
const result = await ws.receive_json()
ws.close()
return result
}