Viewing linked passkeys/sessions (by clicking either one of them).

This commit is contained in:
Leo Vasanko
2025-12-03 13:21:52 -06:00
parent 4306323c44
commit 219dd70665
6 changed files with 80 additions and 11 deletions
+7
View File
@@ -19,6 +19,8 @@ const emit = defineEmits(['generateUserRegistrationLink', 'goOverview', 'openOrg
const authStore = useAuthStore()
const terminatingSessions = ref({})
const hoveredCredentialUuid = ref(null)
const hoveredSession = ref(null)
function onLinkCopied() {
authStore.showMessage('Link copied to clipboard!')
@@ -107,16 +109,21 @@ async function handleTerminateSession(session) {
:credentials="userDetail.credentials"
:aaguid-info="userDetail.aaguid_info"
:allow-delete="true"
:hovered-credential-uuid="hoveredCredentialUuid"
:hovered-session-credential-uuid="hoveredSession?.credential_uuid"
@delete="handleDelete"
@credential-hover="hoveredCredentialUuid = $event"
/>
</div>
</section>
<SessionList
:sessions="userDetail.sessions || []"
:terminating-sessions="terminatingSessions"
:hovered-credential-uuid="hoveredCredentialUuid"
:empty-message="'This user has no active sessions.'"
:section-description="'View and manage the active sessions for this user.'"
@terminate="handleTerminateSession"
@session-hover="hoveredSession = $event"
/>
</template>
<div class="actions ancillary-actions">
+7 -1
View File
@@ -469,6 +469,7 @@ th {
height: 100%;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
position: relative;
cursor: pointer;
}
.record-item:hover,
@@ -481,7 +482,12 @@ th {
.record-item.is-current,
.credential-item.current-session,
.session-item.is-current { border-color: var(--color-accent); }
.credential-item.is-hovered,
.session-item.is-current,
.session-item.is-hovered { border-color: var(--color-accent); }
.credential-item.is-linked-session,
.session-item.is-linked-credential { border-color: var(--color-accent); background-color: var(--color-surface-subtle); }
.item-top {
display: flex;
+27 -3
View File
@@ -6,7 +6,14 @@
<div
v-for="credential in credentials"
:key="credential.credential_uuid"
:class="['credential-item', { 'current-session': credential.is_current_session } ]"
:class="['credential-item', {
'current-session': credential.is_current_session && !hoveredCredentialUuid && !hoveredSessionCredentialUuid,
'is-hovered': hoveredCredentialUuid === credential.credential_uuid,
'is-linked-session': hoveredSessionCredentialUuid === credential.credential_uuid
}]"
tabindex="0"
@focusin="handleCredentialFocus(credential.credential_uuid)"
@focusout="handleCredentialBlur($event)"
>
<div class="item-top">
<div class="item-icon">
@@ -22,7 +29,9 @@
</div>
<h4 class="item-title">{{ getCredentialAuthName(credential) }}</h4>
<div class="item-actions">
<span v-if="credential.is_current_session" class="badge badge-current">Current</span>
<span v-if="credential.is_current_session && !hoveredCredentialUuid && !hoveredSessionCredentialUuid" class="badge badge-current">Current</span>
<span v-else-if="hoveredCredentialUuid === credential.credential_uuid" class="badge badge-current">Selected</span>
<span v-else-if="hoveredSessionCredentialUuid === credential.credential_uuid" class="badge badge-current">Linked</span>
<button
v-if="allowDelete"
@click="$emit('delete', credential)"
@@ -48,7 +57,7 @@
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { formatDate } from '@/utils/helpers'
const props = defineProps({
@@ -56,8 +65,23 @@ const props = defineProps({
aaguidInfo: { type: Object, default: () => ({}) },
loading: { type: Boolean, default: false },
allowDelete: { type: Boolean, default: false },
hoveredCredentialUuid: { type: String, default: null },
hoveredSessionCredentialUuid: { type: String, default: null },
})
const emit = defineEmits(['delete', 'credentialHover'])
const handleCredentialFocus = (uuid) => {
emit('credentialHover', uuid)
}
const handleCredentialBlur = (event) => {
// Only clear if focus moved outside this element
if (!event.currentTarget.contains(event.relatedTarget)) {
emit('credentialHover', null)
}
}
const getCredentialAuthName = (credential) => {
const info = props.aaguidInfo?.[credential.aaguid]
return info ? info.name : 'Unknown Authenticator'
+7
View File
@@ -30,8 +30,11 @@
:credentials="authStore.userInfo?.credentials || []"
:aaguid-info="authStore.userInfo?.aaguid_info || {}"
:loading="authStore.isLoading"
:hovered-credential-uuid="hoveredCredentialUuid"
:hovered-session-credential-uuid="hoveredSession?.credential_uuid"
allow-delete
@delete="handleDelete"
@credential-hover="hoveredCredentialUuid = $event"
/>
<div class="button-row">
<button @click="addNewCredential" class="btn-primary">Add New Passkey</button>
@@ -43,7 +46,9 @@
<SessionList
:sessions="sessions"
:terminating-sessions="terminatingSessions"
:hovered-credential-uuid="hoveredCredentialUuid"
@terminate="terminateSession"
@session-hover="hoveredSession = $event"
section-description="Review where you're signed in and end any sessions you no longer recognize."
/>
@@ -109,6 +114,8 @@ const showNameDialog = ref(false)
const showRegLink = ref(false)
const newName = ref('')
const saving = ref(false)
const hoveredCredentialUuid = ref(null)
const hoveredSession = ref(null)
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.user?.user_name || '' })
+31 -7
View File
@@ -16,15 +16,22 @@
<div
v-for="session in group.sessions"
:key="session.id"
:class="['session-item', { 'is-current': session.is_current }]"
@mouseenter="hoveredIp = session.ip"
@mouseleave="hoveredIp = null"
:class="['session-item', {
'is-current': session.is_current && !hoveredIp && !hoveredCredentialUuid,
'is-hovered': hoveredSession?.id === session.id,
'is-linked-credential': hoveredCredentialUuid === session.credential_uuid
}]"
tabindex="0"
@focusin="handleSessionFocus(session)"
@focusout="handleSessionBlur($event)"
>
<div class="item-top">
<h4 class="item-title">{{ session.user_agent }}</h4>
<div class="item-actions">
<span v-if="session.is_current" class="badge badge-current">Current</span>
<span v-else-if="isSameNetwork(session.ip)" class="badge">Same IP</span>
<span v-if="session.is_current && !hoveredIp && !hoveredCredentialUuid" class="badge badge-current">Current</span>
<span v-else-if="hoveredSession?.id === session.id" class="badge badge-current">Selected</span>
<span v-else-if="hoveredCredentialUuid === session.credential_uuid" class="badge badge-current">Linked</span>
<span v-else-if="!hoveredCredentialUuid && isSameNetwork(session.ip)" class="badge">Same IP</span>
<button
@click="$emit('terminate', session)"
class="btn-card-delete"
@@ -57,12 +64,29 @@ const props = defineProps({
sessions: { type: Array, default: () => [] },
emptyMessage: { type: String, default: 'You currently have no other active sessions.' },
sectionDescription: { type: String, default: "Review where you're signed in and end any sessions you no longer recognize." },
terminatingSessions: { type: Object, default: () => ({}) }
terminatingSessions: { type: Object, default: () => ({}) },
hoveredCredentialUuid: { type: String, default: null },
})
const emit = defineEmits(['terminate'])
const emit = defineEmits(['terminate', 'sessionHover'])
const hoveredIp = ref(null)
const hoveredSession = ref(null)
const handleSessionFocus = (session) => {
hoveredSession.value = session
hoveredIp.value = session.ip || null
emit('sessionHover', session)
}
const handleSessionBlur = (event) => {
// Only clear if focus moved outside this element
if (!event.currentTarget.contains(event.relatedTarget)) {
hoveredSession.value = null
hoveredIp.value = null
emit('sessionHover', null)
}
}
const isTerminating = (sessionId) => !!props.terminatingSessions[sessionId]