New user basic info card and improved layout, telephone number and other contact details shown.

This commit is contained in:
Leo Vasanko
2026-02-17 17:33:06 +00:00
parent a67fd77a32
commit 8128d40202
12 changed files with 274 additions and 188 deletions
+69 -113
View File
@@ -18,15 +18,16 @@
:name="authStore.userInfo.ctx.user.display_name"
:email="authStore.userInfo.ctx.user.email"
:preferred_username="authStore.userInfo.ctx.user.preferred_username"
:telephone="authStore.userInfo.ctx.user.telephone"
:visits="authStore.userInfo.visits"
:created-at="authStore.userInfo.created_at"
:last-seen="authStore.userInfo.last_seen"
:loading="authStore.isLoading"
:org-display-name="authStore.userInfo.ctx.org.display_name"
:role-name="authStore.userInfo.ctx.role.display_name"
update-endpoint="/auth/api/user/display-name"
@saved="authStore.loadUserInfo()"
@edit-name="openNameDialog"
@edit-email="openEmailDialog"
@edit-preferred-username="openPreferredUsernameDialog"
@edit="openEditDialog"
@keydown="handleUserInfoKeydown"
>
<div class="remote-auth-inline">
@@ -40,7 +41,7 @@
@device-info-visible="showDeviceInfo = $event"
/>
</div>
<p class="remote-auth-description">Provided by another device requesting remote auth.</p>
<p class="remote-auth-description">Login from another device</p>
</UserBasicInfo>
</section>
@@ -83,44 +84,28 @@
section-description="You are currently signed in to the following sessions. If you don't recognize something, consider deleting not only the session but the associated passkey you suspect is compromised, as only this terminates all linked sessions and prevents logging in again."
/>
<Modal v-if="showNameDialog" @close="showNameDialog = false">
<h3>Edit Display Name</h3>
<form @submit.prevent="saveName" class="modal-form">
<NameEditForm
label="Display Name"
v-model="newName"
:busy="saving"
@cancel="showNameDialog = false"
/>
</form>
</Modal>
<Modal v-if="showEmailDialog" @close="showEmailDialog = false">
<h3>Edit Email</h3>
<form @submit.prevent="saveEmail" class="modal-form">
<NameEditForm
label="Email"
placeholder="user@example.com"
input-type="email"
v-model="newEmail"
:busy="saving"
:error="emailError"
@cancel="showEmailDialog = false"
/>
</form>
</Modal>
<Modal v-if="showPreferredUsernameDialog" @close="showPreferredUsernameDialog = false">
<h3>Edit Preferred Username</h3>
<form @submit.prevent="savePreferredUsername" class="modal-form">
<NameEditForm
label="Preferred Username"
placeholder="username"
v-model="newPreferredUsername"
:busy="saving"
:error="preferredUsernameError"
@cancel="showPreferredUsernameDialog = false"
/>
<Modal v-if="showEditDialog" @close="showEditDialog = false">
<h3>Edit Profile</h3>
<form @submit.prevent="saveProfile" class="modal-form">
<div class="profile-edit-form">
<label for="edit-display-name">Display Name
<input id="edit-display-name" type="text" v-model="editName" :disabled="saving" required />
</label>
<label for="edit-email">Email
<input id="edit-email" type="email" v-model="editEmail" :disabled="saving" placeholder="user@example.com" />
</label>
<label for="edit-username">Preferred Username
<input id="edit-username" type="text" v-model="editUsername" :disabled="saving" placeholder="username" />
</label>
<label for="edit-telephone">Telephone
<input id="edit-telephone" type="tel" v-model="editTelephone" :disabled="saving" placeholder="+1-555-123-4567" />
</label>
<div v-if="editError" class="error small">{{ editError }}</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" @click="showEditDialog = false" :disabled="saving">Cancel</button>
<button type="submit" class="btn-primary" :disabled="saving" data-nav-primary>Save</button>
</div>
</div>
</form>
</Modal>
@@ -162,7 +147,6 @@ import CredentialList from '@/components/CredentialList.vue'
import ThemeSelector from '@/components/ThemeSelector.vue'
import UserBasicInfo from '@/components/UserBasicInfo.vue'
import Modal from '@/components/Modal.vue'
import NameEditForm from '@/components/NameEditForm.vue'
import SessionList from '@/components/SessionList.vue'
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
import RemoteAuthPermit from '@/components/RemoteAuthPermit.vue'
@@ -175,16 +159,14 @@ import { navigateButtonRow, focusPreferred, focusAtIndex, getDirection } from '@
const authStore = useAuthStore()
const updateInterval = ref(null)
const showNameDialog = ref(false)
const showEmailDialog = ref(false)
const showPreferredUsernameDialog = ref(false)
const showEditDialog = ref(false)
const showRegLink = ref(false)
const newName = ref('')
const newEmail = ref('')
const newPreferredUsername = ref('')
const editName = ref('')
const editEmail = ref('')
const editUsername = ref('')
const editTelephone = ref('')
const saving = ref(false)
const emailError = ref('')
const preferredUsernameError = ref('')
const editError = ref('')
const hoveredCredentialUuid = ref(null)
const hoveredSession = ref(null)
const showDeviceInfo = ref(false)
@@ -198,20 +180,16 @@ const userBasicInfo = ref(null)
const userInfoSection = ref(null)
// Check if any modal/dialog is open (blocks arrow key navigation)
const hasActiveModal = computed(() => showNameDialog.value || showEmailDialog.value || showPreferredUsernameDialog.value || showRegLink.value)
const hasActiveModal = computed(() => showEditDialog.value || showRegLink.value)
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.ctx.user.display_name ?? '' })
watch(showEmailDialog, (newVal) => {
if (newVal) {
newEmail.value = authStore.userInfo?.ctx.user.email ?? ''
emailError.value = ''
}
})
watch(showPreferredUsernameDialog, (newVal) => {
if (newVal) {
newPreferredUsername.value = authStore.userInfo?.ctx.user.preferred_username ?? ''
preferredUsernameError.value = ''
}
watch(showEditDialog, (open) => {
if (!open) return
const user = authStore.userInfo?.ctx?.user
editName.value = user?.display_name ?? ''
editEmail.value = user?.email ?? ''
editUsername.value = user?.preferred_username ?? ''
editTelephone.value = user?.telephone ?? ''
editError.value = ''
})
onMounted(() => {
@@ -385,7 +363,7 @@ const terminateSession = async (session) => {
const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
const logout = async () => { await authStore.logout() }
const openNameDialog = () => { newName.value = authStore.userInfo?.ctx.user.display_name ?? ''; showNameDialog.value = true }
const openEditDialog = () => { showEditDialog.value = true }
const isAdmin = computed(() => {
const perms = authStore.userInfo?.ctx.permissions
return perms.includes('auth:admin') || perms.includes('auth:org:admin')
@@ -409,57 +387,34 @@ const useWideLayout = computed(() => {
})
const breadcrumbEntries = computed(() => { const entries = [{ label: 'Auth', href: makeUiHref() }]; if (isAdmin.value) entries.push({ label: 'Admin', href: adminUiPath() }); return entries })
const saveName = async () => {
const name = newName.value.trim()
if (!name) { authStore.showMessage('Name cannot be empty', 'error'); return }
const saveProfile = async () => {
const name = editName.value.trim()
if (!name) { editError.value = 'Name cannot be empty'; return }
const user = authStore.userInfo.ctx.user
const emailVal = editEmail.value.trim() || null
const usernameVal = editUsername.value.trim() || null
const telephoneVal = editTelephone.value.trim() || null
try {
editError.value = ''
saving.value = true
await apiJson('/auth/api/user/display-name', { method: 'PATCH', body: { display_name: name } })
showNameDialog.value = false
await authStore.loadUserInfo()
authStore.showMessage('Name updated successfully!', 'success', 3000)
} catch (e) { authStore.showMessage(e.message || 'Failed to update name', 'error') }
finally { saving.value = false }
}
const openEmailDialog = () => {
showEmailDialog.value = true
}
const saveEmail = async () => {
try {
emailError.value = ''
saving.value = true
const email = newEmail.value.trim() || null
await apiJson('/auth/api/user/email', { method: 'PATCH', body: { email } })
showEmailDialog.value = false
await authStore.loadUserInfo()
authStore.showMessage('Email updated successfully!', 'success', 3000)
const tasks = []
if (name !== user.display_name)
tasks.push(apiJson('/auth/api/user/display-name', { method: 'PATCH', body: { display_name: name } }))
if (emailVal !== (user.email || null))
tasks.push(apiJson('/auth/api/user/email', { method: 'PATCH', body: { email: emailVal } }))
if (usernameVal !== (user.preferred_username || null))
tasks.push(apiJson('/auth/api/user/preferred-username', { method: 'PATCH', body: { preferred_username: usernameVal } }))
if (telephoneVal !== (user.telephone || null))
tasks.push(apiJson('/auth/api/user/telephone', { method: 'PATCH', body: { telephone: telephoneVal } }))
if (tasks.length) {
await Promise.all(tasks)
await authStore.loadUserInfo()
authStore.showMessage('Profile updated!', 'success', 3000)
}
showEditDialog.value = false
} catch (e) {
emailError.value = e.message || 'Failed to update email'
authStore.showMessage(emailError.value, 'error')
}
finally { saving.value = false }
}
const openPreferredUsernameDialog = () => {
showPreferredUsernameDialog.value = true
}
const savePreferredUsername = async () => {
try {
preferredUsernameError.value = ''
saving.value = true
const preferred_username = newPreferredUsername.value.trim() || null
await apiJson('/auth/api/user/preferred-username', { method: 'PATCH', body: { preferred_username } })
showPreferredUsernameDialog.value = false
await authStore.loadUserInfo()
authStore.showMessage('Preferred username updated successfully!', 'success', 3000)
} catch (e) {
preferredUsernameError.value = e.message || 'Failed to update preferred username'
authStore.showMessage(preferredUsernameError.value, 'error')
}
finally { saving.value = false }
editError.value = e.message || 'Failed to update profile'
} finally { saving.value = false }
}
</script>
@@ -474,4 +429,5 @@ const savePreferredUsername = async () => {
.remote-auth-label { display: block; margin: 0; font-size: 0.875rem; color: var(--color-text-muted); font-weight: 500; }
.remote-auth-description { font-size: 0.75rem; color: var(--color-text-muted); }
.theme-toggle { position: absolute; top: var(--layout-padding); right: var(--layout-padding); }
.profile-edit-form { display: flex; flex-direction: column; gap: var(--space-md); }
</style>