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
+3 -1
View File
@@ -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"
>
<div class="admin-actions">
<button
+2
View File
@@ -325,6 +325,7 @@ input:not([type]),
input[type="text"],
input[type="search"],
input[type="email"],
input[type="tel"],
textarea,
select {
font: inherit;
@@ -341,6 +342,7 @@ input:not([type]):focus,
input[type="text"]:focus,
input[type="search"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
textarea:focus,
select:focus {
outline: none;
@@ -13,6 +13,8 @@
:visits="authStore.userInfo?.visits || 0"
:created-at="authStore.userInfo?.created_at"
:last-seen="authStore.userInfo?.last_seen"
:email="ctx.user.email"
:telephone="ctx.user.telephone"
:org-display-name="orgDisplayName"
:role-name="roleDisplayName"
:can-edit="false"
+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>
@@ -771,7 +771,6 @@ defineExpose({ reset, deny, code, handleInput, loading, error })
.input-wrapper {
position: relative;
display: flex;
width: 280px;
max-width: 100%;
}
+71 -73
View File
@@ -1,34 +1,37 @@
<template>
<div v-if="userLoaded" class="user-info" :class="{ 'has-extra': $slots.default }">
<h3 class="user-name-heading">
<span class="icon">👤</span>
<span class="user-name-row">
<span class="display-name" :title="name">{{ name }}</span>
<button v-if="canEdit && updateEndpoint" class="mini-btn" @click="emit('editName')" title="Edit name"></button>
</span>
</h3>
<div v-if="orgDisplayName || roleName" class="org-role-sub">
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
<div class="role-line" v-if="roleName">{{ roleName }}</div>
</div>
<div class="user-details">
<span class="date-label"><strong>Visits:</strong></span>
<span class="date-value">{{ visits || 0 }}</span>
<span class="date-label"><strong>Registered:</strong></span>
<span class="date-value">{{ formatDate(createdAt) }}</span>
<span class="date-label"><strong>Last seen:</strong></span>
<span class="date-value">{{ formatDate(lastSeen) }}</span>
</div>
<div class="oidc-info">
<div class="oidc-field">
<span class="field-label"><strong>Email:</strong></span>
<span class="field-value">{{ email || '—' }}</span>
<button v-if="canEdit" class="mini-btn" @click="emit('editEmail')" title="Edit email"></button>
<div class="user-info-content">
<div class="user-picture">
<span>👤</span>
</div>
<div class="oidc-field">
<span class="field-label"><strong>Preferred username:</strong></span>
<span class="field-value">{{ preferred_username || '—' }}</span>
<button v-if="canEdit" class="mini-btn" @click="emit('editPreferredUsername')" title="Edit preferred username"></button>
<h3 class="user-name-heading">
<span class="user-name-row">
<span class="display-name" :title="name">{{ name }}</span>
<button v-if="canEdit && updateEndpoint" class="mini-btn" @click="emit('edit')" title="Edit profile"></button>
</span>
</h3>
<div v-if="orgDisplayName || roleName" class="org-role-sub">
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
<div class="role-line" v-if="roleName">{{ roleName }}</div>
</div>
<div class="info-fields-block">
<div v-if="preferred_username" class="contact-item">🆔 {{ preferred_username }}</div>
<a v-if="email" :href="`mailto:${email}`" class="contact-link"> {{ email }}</a>
<a v-if="telephone" :href="`tel:${telephone}`" class="contact-link">📞 {{ telephone }}</a>
</div>
<div class="info-line">
<span v-if="visits">
<span class="info-date">{{ formatDate(createdAt) }}</span>
<span class="info-punct"> </span>
<span class="info-date">{{ formatDate(lastSeen) }}</span>
<span class="info-punct"> ×</span>
<span class="info-count">{{ visits }}</span>
</span>
<span v-else>
<span class="info-label">Created </span>
<span class="info-date">{{ formatDate(createdAt) }}</span>
<span class="info-punct"> Never signed in</span>
</span>
</div>
</div>
<div v-if="$slots.default" class="user-info-extra">
@@ -38,7 +41,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { formatDate } from '@/utils/helpers'
@@ -46,6 +49,7 @@ const props = defineProps({
name: { type: String, required: true },
email: { type: String, default: null },
preferred_username: { type: String, default: null },
telephone: { type: String, default: null },
visits: { type: [Number, String], default: 0 },
createdAt: { type: [String, Number, Date], default: null },
lastSeen: { type: [String, Number, Date], default: null },
@@ -56,7 +60,7 @@ const props = defineProps({
roleName: { type: String, default: '' }
})
const emit = defineEmits(['saved', 'editName', 'editEmail', 'editPreferredUsername'])
const emit = defineEmits(['saved', 'edit'])
const authStore = useAuthStore()
const userLoaded = computed(() => !!props.name)
@@ -64,62 +68,56 @@ const userLoaded = computed(() => !!props.name)
<style scoped>
.user-info.has-extra {
grid-template-columns: auto 1fr 2fr;
grid-template-columns: minmax(0, 1fr) 14rem;
grid-template-areas:
"heading heading extra"
"org org extra"
"label1 value1 extra"
"label2 value2 extra"
"label3 value3 extra"
"oidc oidc extra";
"content extra";
gap: 1.5rem;
}
.user-info:not(.has-extra) {
grid-template-columns: auto 1fr;
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"heading heading"
"org org"
"label1 value1"
"label2 value2"
"label3 value3"
"oidc oidc";
"content";
}
@media (max-width: 720px) {
.user-info.has-extra {
grid-template-columns: auto 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"heading heading"
"org org"
"label1 value1"
"label2 value2"
"label3 value3"
"extra extra";
"content"
"extra";
}
}
.user-name-heading { grid-area: heading; display: flex; align-items: center; flex-wrap: wrap; margin: 0 0 0.25rem 0; }
.org-role-sub { grid-area: org; display:flex; flex-direction:column; margin: -0.15rem 0 0.25rem; }
.org-line { font-size: .7rem; font-weight:600; line-height:1.1; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.role-line { font-size:.65rem; color: var(--color-text-muted); line-height:1.1; }
.info-label:nth-of-type(1) { grid-area: label1; }
.info-value:nth-of-type(2) { grid-area: value1; }
.info-label:nth-of-type(3) { grid-area: label2; }
.info-value:nth-of-type(4) { grid-area: value2; }
.info-label:nth-of-type(5) { grid-area: label3; }
.info-value:nth-of-type(6) { grid-area: value3; }
.oidc-info { grid-area: oidc; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border); display: flex; flex-direction: column; gap: 0.5rem; }
.oidc-field { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9em; }
.oidc-field .field-label { flex-shrink: 0; }
.oidc-field .field-value { flex: 1; word-break: break-all; color: var(--color-text-muted); }
.oidc-field .mini-btn { flex-shrink: 0; }
.user-info-extra { grid-area: extra; padding-left: 2rem; border-left: 1px solid var(--color-border); }
.user-name-row { display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; }
.user-name-row.editing { flex: 1 1 auto; }
.display-name { font-weight: 600; font-size: 1.05em; line-height: 1.2; max-width: 14ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.name-input { width: auto; flex: 1 1 140px; min-width: 120px; padding: 6px 8px; font-size: 0.9em; border: 1px solid var(--color-border-strong); border-radius: 6px; background: var(--color-surface); color: var(--color-text); }
.user-name-heading .name-input { width: auto; }
.name-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--focus-ring); }
.user-info-content {
grid-area: content;
display: grid;
grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas:
"picture heading fields"
"picture org fields"
". info info";
gap: 0 1rem;
min-width: 0;
}
.user-picture { grid-area: picture; display: flex; align-items: flex-start; font-size: 2em; line-height: 1; }
.user-name-heading { grid-area: heading; display: flex; align-items: center; flex-wrap: wrap; margin: 0 0 0.25rem 0; min-width: 0; }
.org-role-sub { grid-area: org; display: flex; flex-direction: column; min-width: 0; }
.org-line { font-size: .7rem; font-weight: 600; line-height: 1.1; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.role-line { font-size: .65rem; color: var(--color-text-muted); line-height: 1.1; }
.info-fields-block { grid-area: fields; display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.contact-item { display: block; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contact-link { color: var(--color-text); text-decoration: none; display: block; transition: transform 0.1s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contact-link:hover { transform: scale(1.05); }
.info-line { grid-area: info; line-height: 1.4; font-size: 0.9em; }
.info-date { color: var(--color-text) !important; }
.info-label { color: var(--color-text) !important; }
.info-punct { color: var(--color-text-muted) !important; }
.info-count { color: var(--color-text-muted) !important; }
.user-info-extra { grid-area: extra; padding-left: 1rem; border-left: 1px solid var(--color-border); flex-shrink: 0; }
.user-name-row { display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; min-width: 0; }
.display-name { font-weight: 600; font-size: 1.05em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.mini-btn { width: auto; padding: 4px 6px; margin: 0; font-size: 0.75em; line-height: 1; cursor: pointer; }
.mini-btn:hover:not(:disabled) { background: var(--color-accent-soft); color: var(--color-accent); }
.mini-btn:active:not(:disabled) { transform: translateY(1px); }
+2
View File
@@ -65,6 +65,7 @@ from paskia.db.operations import (
update_user_email,
update_user_preferred_username,
update_user_role,
update_user_telephone,
update_user_theme,
)
from paskia.db.structs import (
@@ -151,6 +152,7 @@ __all__ = [
"update_user_email",
"update_user_preferred_username",
"update_user_role",
"update_user_telephone",
"update_user_theme",
# OIDC
"create_oid_client",
+21
View File
@@ -329,6 +329,27 @@ def update_user_preferred_username(
_db.users[uuid].preferred_username = preferred_username
def update_user_telephone(
uuid: UUID,
telephone: str | None,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update user telephone number. Can be None to clear."""
if isinstance(uuid, str):
uuid = UUID(uuid)
if uuid not in _db.users:
raise ValueError(f"User {uuid} not found")
if telephone is not None:
telephone = (telephone or "").strip()
if not telephone:
telephone = None
elif len(telephone) > 32:
raise ValueError("telephone too long")
with _db.transaction("update_user_telephone", ctx):
_db.users[uuid].telephone = telephone
def update_user_role(
uuid: UUID,
role_uuid: UUID,
+1
View File
@@ -211,6 +211,7 @@ class User(msgspec.Struct, dict=True, omit_defaults=True):
theme: str = "" # "" or "auto" = OS default, "light", "dark"
email: str | None = None # OIDC email claim
preferred_username: str | None = None # OIDC preferred_username claim
telephone: str | None = None # Telephone number
def __post_init__(self):
if not hasattr(self, "uuid"):
+81
View File
@@ -579,6 +579,9 @@ async def admin_get_user_detail(
"visits": user.visits,
"created_at": user.created_at,
"last_seen": user.last_seen,
"email": user.email,
"preferred_username": user.preferred_username,
"telephone": user.telephone,
"credentials": [
{
"credential": c.uuid,
@@ -634,6 +637,84 @@ async def admin_update_user_display_name(
return {"status": "ok"}
@app.patch("/users/{user_uuid}/email")
async def admin_update_user_email(
user_uuid: UUID,
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
try:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
email = payload.get("email")
db.update_user_email(user_uuid, email, ctx=ctx)
return {"status": "ok"}
@app.patch("/users/{user_uuid}/preferred-username")
async def admin_update_user_preferred_username(
user_uuid: UUID,
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
try:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
preferred_username = payload.get("preferred_username")
db.update_user_preferred_username(user_uuid, preferred_username, ctx=ctx)
return {"status": "ok"}
@app.patch("/users/{user_uuid}/telephone")
async def admin_update_user_telephone(
user_uuid: UUID,
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
try:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
telephone = payload.get("telephone")
db.update_user_telephone(user_uuid, telephone, ctx=ctx)
return {"status": "ok"}
@app.delete("/users/{user_uuid}")
async def admin_delete_user(
user_uuid: UUID,
+20
View File
@@ -120,6 +120,26 @@ async def user_update_preferred_username(
return {"status": "ok"}
@app.patch("/telephone")
async def user_update_telephone(
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
if not auth:
raise authz.AuthException(
status_code=401, detail="Authentication Required", mode="login"
)
ctx = db.data().session_ctx(auth, request.headers.get("host"))
if not ctx:
raise authz.AuthException(
status_code=401, detail="Session expired", mode="login"
)
telephone = payload.get("telephone")
db.update_user_telephone(ctx.user.uuid, telephone, ctx=ctx)
return {"status": "ok"}
@app.post("/logout-all")
async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE):
if not auth:
+2
View File
@@ -21,6 +21,8 @@ def build_session_context(ctx: SessionContext) -> dict:
result["user"]["email"] = ctx.user.email
if ctx.user.preferred_username:
result["user"]["preferred_username"] = ctx.user.preferred_username
if ctx.user.telephone:
result["user"]["telephone"] = ctx.user.telephone
return result