Viewing linked passkeys/sessions (by clicking either one of them).
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 || '' })
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user