diff --git a/frontend/src/admin/AdminUserDetail.vue b/frontend/src/admin/AdminUserDetail.vue index da9ad62..9cd528a 100644 --- a/frontend/src/admin/AdminUserDetail.vue +++ b/frontend/src/admin/AdminUserDetail.vue @@ -184,12 +184,14 @@ defineExpose({ focusFirstElement }) :visits="userDetail.visits" :created-at="userDetail.created_at" :last-seen="userDetail.last_seen" + :email="userDetail.email" + :telephone="userDetail.telephone" :loading="loading" :org-display-name="userDetail.org.display_name" :role-name="userDetail.role" :update-endpoint="`/auth/api/admin/users/${selectedUser.uuid}/display-name`" @saved="$emit('onUserNameSaved')" - @edit-name="handleEditName" + @edit="handleEditName" >
+ +
+ @@ -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 } } @@ -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); } diff --git a/frontend/src/components/RemoteAuthPermit.vue b/frontend/src/components/RemoteAuthPermit.vue index 642a6e1..b867aaa 100644 --- a/frontend/src/components/RemoteAuthPermit.vue +++ b/frontend/src/components/RemoteAuthPermit.vue @@ -771,7 +771,6 @@ defineExpose({ reset, deny, code, handleInput, loading, error }) .input-wrapper { position: relative; display: flex; - width: 280px; max-width: 100%; } diff --git a/frontend/src/components/UserBasicInfo.vue b/frontend/src/components/UserBasicInfo.vue index c8ac327..4f27af0 100644 --- a/frontend/src/components/UserBasicInfo.vue +++ b/frontend/src/components/UserBasicInfo.vue @@ -1,34 +1,37 @@