Renaming of users in registration, profile and admin app.
This commit is contained in:
		| @@ -364,6 +364,32 @@ async function toggleRolePermission(role, permId, checked) { | |||||||
| function openDialog(type, data) { dialog.value = { type, data, busy: false, error: '' } } | function openDialog(type, data) { dialog.value = { type, data, busy: false, error: '' } } | ||||||
| function closeDialog() { dialog.value = { type: null, data: null, busy: false, error: '' } } | function closeDialog() { dialog.value = { type: null, data: null, busy: false, error: '' } } | ||||||
|  |  | ||||||
|  | // Admin user rename | ||||||
|  | const editingUserName = ref(false) | ||||||
|  | const editUserNameValue = ref('') | ||||||
|  | const editUserNameValid = computed(()=> editUserNameValue.value.trim().length > 0 && editUserNameValue.value.trim().length <= 64) | ||||||
|  | function beginEditUserName() { | ||||||
|  |   if (!selectedUser.value) return | ||||||
|  |   editingUserName.value = true | ||||||
|  |   editUserNameValue.value = userDetail.value?.display_name || selectedUser.value.display_name || '' | ||||||
|  | } | ||||||
|  | function cancelEditUserName() { editingUserName.value = false } | ||||||
|  | async function submitEditUserName() { | ||||||
|  |   if (!editingUserName.value || !editUserNameValid.value) return | ||||||
|  |   try { | ||||||
|  |     const res = await fetch(`/auth/admin/users/${selectedUser.value.uuid}/display-name`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ display_name: editUserNameValue.value.trim() }) }) | ||||||
|  |     const data = await res.json(); if (!res.ok || data.detail) throw new Error(data.detail || 'Rename failed') | ||||||
|  |     editingUserName.value = false | ||||||
|  |     await loadOrgs() | ||||||
|  |     const r = await fetch(`/auth/admin/users/${selectedUser.value.uuid}`) | ||||||
|  |     const jd = await r.json(); if (!r.ok || jd.detail) throw new Error(jd.detail || 'Reload failed') | ||||||
|  |     userDetail.value = jd | ||||||
|  |     authStore.showMessage('User renamed', 'success', 1500) | ||||||
|  |   } catch (e) { | ||||||
|  |     authStore.showMessage(e.message || 'Rename failed') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| async function submitDialog() { | async function submitDialog() { | ||||||
|   if (!dialog.value.type || dialog.value.busy) return |   if (!dialog.value.type || dialog.value.busy) return | ||||||
|   dialog.value.busy = true; dialog.value.error = '' |   dialog.value.busy = true; dialog.value.error = '' | ||||||
| @@ -458,7 +484,14 @@ async function submitDialog() { | |||||||
|  |  | ||||||
|         <!-- User Detail Page --> |         <!-- User Detail Page --> | ||||||
|         <div v-if="selectedUser" class="card user-detail"> |         <div v-if="selectedUser" class="card user-detail"> | ||||||
|           <h2 class="user-title"><span>{{ userDetail?.display_name || selectedUser.display_name }}</span></h2> |           <h2 class="user-title"> | ||||||
|  |             <span v-if="!editingUserName">{{ userDetail?.display_name || selectedUser.display_name }} <button class="icon-btn" @click="beginEditUserName" title="Rename user">✏️</button></span> | ||||||
|  |             <span v-else> | ||||||
|  |               <input v-model="editUserNameValue" maxlength="64" @keyup.enter="submitEditUserName" /> | ||||||
|  |               <button class="icon-btn" @click="submitEditUserName" :disabled="!editUserNameValid">💾</button> | ||||||
|  |               <button class="icon-btn" @click="cancelEditUserName">✖</button> | ||||||
|  |             </span> | ||||||
|  |           </h2> | ||||||
|           <div v-if="userDetail && !userDetail.error" class="user-meta"> |           <div v-if="userDetail && !userDetail.error" class="user-meta"> | ||||||
|             <p class="small">Organization: {{ userDetail.org.display_name }}</p> |             <p class="small">Organization: {{ userDetail.org.display_name }}</p> | ||||||
|             <p class="small">Role: {{ userDetail.role }}</p> |             <p class="small">Role: {{ userDetail.role }}</p> | ||||||
|   | |||||||
| @@ -3,7 +3,15 @@ | |||||||
|     <div class="view active"> |     <div class="view active"> | ||||||
|   <h1>👋 Welcome! <a v-if="isAdmin" href="/auth/admin/" class="admin-link" title="Admin Console">Admin</a></h1> |   <h1>👋 Welcome! <a v-if="isAdmin" href="/auth/admin/" class="admin-link" title="Admin Console">Admin</a></h1> | ||||||
|       <div v-if="authStore.userInfo?.user" class="user-info"> |       <div v-if="authStore.userInfo?.user" class="user-info"> | ||||||
|         <h3>👤 {{ authStore.userInfo.user.user_name }}</h3> |         <h3> | ||||||
|  |           👤 | ||||||
|  |           <template v-if="!editingName">{{ authStore.userInfo.user.user_name }} <button class="mini-btn" @click="startEdit" title="Edit name">✏️</button></template> | ||||||
|  |           <template v-else> | ||||||
|  |             <input v-model="newName" :disabled="authStore.isLoading" maxlength="64" @keyup.enter="saveName" /> | ||||||
|  |             <button class="mini-btn" @click="saveName" :disabled="!validName || authStore.isLoading">💾</button> | ||||||
|  |             <button class="mini-btn" @click="cancelEdit" :disabled="authStore.isLoading">✖</button> | ||||||
|  |           </template> | ||||||
|  |         </h3> | ||||||
|         <span><strong>Visits:</strong></span> |         <span><strong>Visits:</strong></span> | ||||||
|         <span>{{ authStore.userInfo.user.visits || 0 }}</span> |         <span>{{ authStore.userInfo.user.visits || 0 }}</span> | ||||||
|         <span><strong>Registered:</strong></span> |         <span><strong>Registered:</strong></span> | ||||||
| @@ -147,6 +155,25 @@ const logout = async () => { | |||||||
| } | } | ||||||
|  |  | ||||||
| const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin)) | const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin)) | ||||||
|  |  | ||||||
|  | // Name editing state & actions | ||||||
|  | const editingName = ref(false) | ||||||
|  | const newName = ref('') | ||||||
|  | const validName = computed(() => newName.value.trim().length > 0 && newName.value.trim().length <= 64) | ||||||
|  | function startEdit() { editingName.value = true; newName.value = authStore.userInfo?.user?.user_name || '' } | ||||||
|  | function cancelEdit() { editingName.value = false } | ||||||
|  | async function saveName() { | ||||||
|  |   if (!validName.value) return | ||||||
|  |   try { | ||||||
|  |     authStore.isLoading = true | ||||||
|  |     const res = await fetch('/auth/user/display-name', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ display_name: newName.value.trim() }) }) | ||||||
|  |     const data = await res.json(); if (!res.ok || data.detail) throw new Error(data.detail || 'Update failed') | ||||||
|  |     await authStore.loadUserInfo() | ||||||
|  |     editingName.value = false | ||||||
|  |     authStore.showMessage('Name updated', 'success', 1500) | ||||||
|  |   } catch (e) { authStore.showMessage(e.message || 'Failed to update name', 'error') } | ||||||
|  |   finally { authStore.isLoading = false } | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped> | <style scoped> | ||||||
| @@ -161,6 +188,7 @@ const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authSto | |||||||
| .user-info span { | .user-info span { | ||||||
|   text-align: left; |   text-align: left; | ||||||
| } | } | ||||||
|  | .mini-btn { font-size: 0.7em; margin-left: 0.3em; } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
| <style scoped> | <style scoped> | ||||||
|   | |||||||
| @@ -2,8 +2,17 @@ | |||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <div class="view active"> |     <div class="view active"> | ||||||
|       <h1>🔑 Add New Credential</h1> |       <h1>🔑 Add New Credential</h1> | ||||||
|       <h3>👤 {{ authStore.userInfo?.user?.user_name }}</h3> |       <label class="name-edit"> | ||||||
|       <!-- TODO: allow editing name <input type="text" v-model="user_name" required :disabled="authStore.isLoading"> --> |         <span>👤 Name:</span> | ||||||
|  |         <input | ||||||
|  |           type="text" | ||||||
|  |           v-model="user_name" | ||||||
|  |           :placeholder="authStore.userInfo?.user?.user_name || 'Your name'" | ||||||
|  |           :disabled="authStore.isLoading" | ||||||
|  |           maxlength="64" | ||||||
|  |           @keyup.enter="register" | ||||||
|  |         /> | ||||||
|  |       </label> | ||||||
|       <p>Proceed to complete {{authStore.userInfo?.session_type}}:</p> |       <p>Proceed to complete {{authStore.userInfo?.session_type}}:</p> | ||||||
|       <button |       <button | ||||||
|         class="btn-primary" |         class="btn-primary" | ||||||
| @@ -19,15 +28,26 @@ | |||||||
| <script setup> | <script setup> | ||||||
| import { useAuthStore } from '@/stores/auth' | import { useAuthStore } from '@/stores/auth' | ||||||
| import passkey from '@/utils/passkey' | import passkey from '@/utils/passkey' | ||||||
|  | import { ref, watchEffect } from 'vue' | ||||||
|  |  | ||||||
| const authStore = useAuthStore() | const authStore = useAuthStore() | ||||||
|  | const user_name = ref('') | ||||||
|  |  | ||||||
|  | // Initialize local name from store (once loaded) | ||||||
|  | watchEffect(() => { | ||||||
|  |   if (!user_name.value && authStore.userInfo?.user?.user_name) { | ||||||
|  |     user_name.value = authStore.userInfo.user.user_name | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
| async function register() { | async function register() { | ||||||
|   authStore.isLoading = true |   authStore.isLoading = true | ||||||
|   authStore.showMessage('Starting registration...', 'info') |   authStore.showMessage('Starting registration...', 'info') | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|   const result = await passkey.register(authStore.resetToken) |   const trimmed = (user_name.value || '').trim() | ||||||
|  |   const nameToSend = trimmed.length ? trimmed : null | ||||||
|  |   const result = await passkey.register(authStore.resetToken, nameToSend) | ||||||
|   console.log("Result", result) |   console.log("Result", result) | ||||||
|   await authStore.setSessionCookie(result.session_token) |   await authStore.setSessionCookie(result.session_token) | ||||||
|   // resetToken cleared by setSessionCookie; ensure again |   // resetToken cleared by setSessionCookie; ensure again | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| import { startRegistration, startAuthentication } from '@simplewebauthn/browser' | import { startRegistration, startAuthentication } from '@simplewebauthn/browser' | ||||||
| import aWebSocket from '@/utils/awaitable-websocket' | import aWebSocket from '@/utils/awaitable-websocket' | ||||||
|  |  | ||||||
| export async function register(resetToken = null) { | export async function register(resetToken = null, displayName = null) { | ||||||
|   const url = resetToken ? `/auth/ws/register?reset=${encodeURIComponent(resetToken)}` : "/auth/ws/register" |   let params = [] | ||||||
|  |   if (resetToken) params.push(`reset=${encodeURIComponent(resetToken)}`) | ||||||
|  |   if (displayName) params.push(`name=${encodeURIComponent(displayName)}`) | ||||||
|  |   const qs = params.length ? `?${params.join('&')}` : '' | ||||||
|  |   const url = `/auth/ws/register${qs}` | ||||||
|   const ws = await aWebSocket(url) |   const ws = await aWebSocket(url) | ||||||
|   try { |   try { | ||||||
|     const optionsJSON = await ws.receive_json() |     const optionsJSON = await ws.receive_json() | ||||||
|   | |||||||
| @@ -100,6 +100,10 @@ class DatabaseInterface(ABC): | |||||||
|     async def create_user(self, user: User) -> None: |     async def create_user(self, user: User) -> None: | ||||||
|         """Create a new user.""" |         """Create a new user.""" | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     async def update_user_display_name(self, user_uuid: UUID, display_name: str) -> None: | ||||||
|  |         """Update a user's display name.""" | ||||||
|  |  | ||||||
|     # Role operations |     # Role operations | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     async def create_role(self, role: Role) -> None: |     async def create_role(self, role: Role) -> None: | ||||||
| @@ -312,6 +316,27 @@ class DatabaseInterface(ABC): | |||||||
|     async def get_session_context(self, session_key: bytes) -> SessionContext | None: |     async def get_session_context(self, session_key: bytes) -> SessionContext | None: | ||||||
|         """Get complete session context including user, organization, role, and permissions.""" |         """Get complete session context including user, organization, role, and permissions.""" | ||||||
|  |  | ||||||
|  |         # Combined atomic operations | ||||||
|  |         @abstractmethod | ||||||
|  |         async def create_credential_session( | ||||||
|  |             self, | ||||||
|  |             user_uuid: UUID, | ||||||
|  |             credential: Credential, | ||||||
|  |             reset_key: bytes | None, | ||||||
|  |             session_key: bytes, | ||||||
|  |             session_expires: datetime, | ||||||
|  |             session_info: dict, | ||||||
|  |             display_name: str | None = None, | ||||||
|  |         ) -> None: | ||||||
|  |             """Atomically add a credential and create a session. | ||||||
|  |  | ||||||
|  |             Steps (single transaction): | ||||||
|  |                 1. Insert credential | ||||||
|  |                 2. Optionally delete old session (e.g. reset token) if provided | ||||||
|  |                 3. Optionally update user's display name | ||||||
|  |                 4. Insert new session referencing the credential | ||||||
|  |             """ | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     "User", |     "User", | ||||||
|   | |||||||
| @@ -271,6 +271,17 @@ class DB(DatabaseInterface): | |||||||
|         async with self.session() as session: |         async with self.session() as session: | ||||||
|             session.add(UserModel.from_dataclass(user)) |             session.add(UserModel.from_dataclass(user)) | ||||||
|  |  | ||||||
|  |     async def update_user_display_name(self, user_uuid: UUID, display_name: str) -> None: | ||||||
|  |         async with self.session() as session: | ||||||
|  |             stmt = ( | ||||||
|  |                 update(UserModel) | ||||||
|  |                 .where(UserModel.uuid == user_uuid.bytes) | ||||||
|  |                 .values(display_name=display_name) | ||||||
|  |             ) | ||||||
|  |             result = await session.execute(stmt) | ||||||
|  |             if result.rowcount == 0:  # type: ignore[attr-defined] | ||||||
|  |                 raise ValueError("User not found") | ||||||
|  |  | ||||||
|     async def create_role(self, role: Role) -> None: |     async def create_role(self, role: Role) -> None: | ||||||
|         async with self.session() as session: |         async with self.session() as session: | ||||||
|             # Create role record |             # Create role record | ||||||
| @@ -389,6 +400,55 @@ class DB(DatabaseInterface): | |||||||
|             ) |             ) | ||||||
|             session.add(credential_model) |             session.add(credential_model) | ||||||
|  |  | ||||||
|  |     async def create_credential_session( | ||||||
|  |         self, | ||||||
|  |         user_uuid: UUID, | ||||||
|  |         credential: Credential, | ||||||
|  |         reset_key: bytes | None, | ||||||
|  |         session_key: bytes, | ||||||
|  |         session_expires: datetime, | ||||||
|  |         session_info: dict, | ||||||
|  |         display_name: str | None = None, | ||||||
|  |     ) -> None: | ||||||
|  |         """Atomic credential + (optional old session delete) + (optional rename) + new session.""" | ||||||
|  |         async with self.session() as session: | ||||||
|  |             # Insert credential | ||||||
|  |             session.add( | ||||||
|  |                 CredentialModel( | ||||||
|  |                     uuid=credential.uuid.bytes, | ||||||
|  |                     credential_id=credential.credential_id, | ||||||
|  |                     user_uuid=credential.user_uuid.bytes, | ||||||
|  |                     aaguid=credential.aaguid.bytes, | ||||||
|  |                     public_key=credential.public_key, | ||||||
|  |                     sign_count=credential.sign_count, | ||||||
|  |                     created_at=credential.created_at, | ||||||
|  |                     last_used=credential.last_used, | ||||||
|  |                     last_verified=credential.last_verified, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             # Delete old session if provided | ||||||
|  |             if reset_key: | ||||||
|  |                 await session.execute( | ||||||
|  |                     delete(SessionModel).where(SessionModel.key == reset_key) | ||||||
|  |                 ) | ||||||
|  |             # Optional rename | ||||||
|  |             if display_name: | ||||||
|  |                 await session.execute( | ||||||
|  |                     update(UserModel) | ||||||
|  |                     .where(UserModel.uuid == user_uuid.bytes) | ||||||
|  |                     .values(display_name=display_name) | ||||||
|  |                 ) | ||||||
|  |             # New session | ||||||
|  |             session.add( | ||||||
|  |                 SessionModel( | ||||||
|  |                     key=session_key, | ||||||
|  |                     user_uuid=user_uuid.bytes, | ||||||
|  |                     credential_uuid=credential.uuid.bytes, | ||||||
|  |                     expires=session_expires, | ||||||
|  |                     info=session_info, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     async def delete_credential(self, uuid: UUID, user_uuid: UUID) -> None: |     async def delete_credential(self, uuid: UUID, user_uuid: UUID) -> None: | ||||||
|         async with self.session() as session: |         async with self.session() as session: | ||||||
|             stmt = ( |             stmt = ( | ||||||
|   | |||||||
| @@ -487,6 +487,40 @@ def register_api_routes(app: FastAPI): | |||||||
|             "aaguid_info": aaguid_info, |             "aaguid_info": aaguid_info, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |     @app.put("/auth/user/display-name") | ||||||
|  |     async def user_update_display_name(payload: dict = Body(...), auth=Cookie(None)): | ||||||
|  |         """Authenticated user updates their own display name.""" | ||||||
|  |         if not auth: | ||||||
|  |             raise HTTPException(status_code=401, detail="Authentication Required") | ||||||
|  |         s = await get_session(auth) | ||||||
|  |         new_name = (payload.get("display_name") or "").strip() | ||||||
|  |         if not new_name: | ||||||
|  |             raise HTTPException(status_code=400, detail="display_name required") | ||||||
|  |         if len(new_name) > 64: | ||||||
|  |             raise HTTPException(status_code=400, detail="display_name too long") | ||||||
|  |         await db.instance.update_user_display_name(s.user_uuid, new_name) | ||||||
|  |         return {"status": "ok"} | ||||||
|  |  | ||||||
|  |     @app.put("/auth/admin/users/{user_uuid}/display-name") | ||||||
|  |     async def admin_update_user_display_name( | ||||||
|  |         user_uuid: UUID, payload: dict = Body(...), auth=Cookie(None) | ||||||
|  |     ): | ||||||
|  |         """Admin updates a user's display name.""" | ||||||
|  |         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) | ||||||
|  |         try: | ||||||
|  |             user_org, _role_name = await db.instance.get_user_organization(user_uuid) | ||||||
|  |         except ValueError: | ||||||
|  |             raise HTTPException(status_code=404, detail="User not found") | ||||||
|  |         if not (is_global_admin or (is_org_admin and user_org.uuid == ctx.org.uuid)): | ||||||
|  |             raise HTTPException(status_code=403, detail="Insufficient permissions") | ||||||
|  |         new_name = (payload.get("display_name") or "").strip() | ||||||
|  |         if not new_name: | ||||||
|  |             raise HTTPException(status_code=400, detail="display_name required") | ||||||
|  |         if len(new_name) > 64: | ||||||
|  |             raise HTTPException(status_code=400, detail="display_name too long") | ||||||
|  |         await db.instance.update_user_display_name(user_uuid, new_name) | ||||||
|  |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     # Admin API: Permissions (global) |     # Admin API: Permissions (global) | ||||||
|  |  | ||||||
|     @app.get("/auth/admin/permissions") |     @app.get("/auth/admin/permissions") | ||||||
|   | |||||||
| @@ -5,9 +5,10 @@ from uuid import UUID | |||||||
| from fastapi import Cookie, FastAPI, WebSocket, WebSocketDisconnect | from fastapi import Cookie, FastAPI, WebSocket, WebSocketDisconnect | ||||||
| from webauthn.helpers.exceptions import InvalidAuthenticationResponse | from webauthn.helpers.exceptions import InvalidAuthenticationResponse | ||||||
|  |  | ||||||
| from ..authsession import create_session, get_reset, get_session | from ..authsession import create_session, expires, get_reset, get_session | ||||||
| from ..globals import db, passkey | from ..globals import db, passkey | ||||||
| from ..util import passphrase | from ..util import passphrase | ||||||
|  | from ..util.tokens import create_token, session_key | ||||||
| from .session import infodict | from .session import infodict | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -55,7 +56,7 @@ async def register_chat( | |||||||
| @app.websocket("/register") | @app.websocket("/register") | ||||||
| @websocket_error_handler | @websocket_error_handler | ||||||
| async def websocket_register_add( | async def websocket_register_add( | ||||||
|     ws: WebSocket, reset: str | None = None, auth=Cookie(None) |     ws: WebSocket, reset: str | None = None, name: str | None = None, auth=Cookie(None) | ||||||
| ): | ): | ||||||
|     """Register a new credential for an existing user. |     """Register a new credential for an existing user. | ||||||
|  |  | ||||||
| @@ -64,11 +65,9 @@ async def websocket_register_add( | |||||||
|     - Reset token supplied as ?reset=... (auth cookie ignored) |     - Reset token supplied as ?reset=... (auth cookie ignored) | ||||||
|     """ |     """ | ||||||
|     origin = ws.headers["origin"] |     origin = ws.headers["origin"] | ||||||
|     is_reset = False |  | ||||||
|     if reset is not None: |     if reset is not None: | ||||||
|         if not passphrase.is_well_formed(reset): |         if not passphrase.is_well_formed(reset): | ||||||
|             raise ValueError("Invalid reset token") |             raise ValueError("Invalid reset token") | ||||||
|         is_reset = True |  | ||||||
|         s = await get_reset(reset) |         s = await get_reset(reset) | ||||||
|     else: |     else: | ||||||
|         if not auth: |         if not auth: | ||||||
| @@ -76,23 +75,30 @@ async def websocket_register_add( | |||||||
|         s = await get_session(auth) |         s = await get_session(auth) | ||||||
|     user_uuid = s.user_uuid |     user_uuid = s.user_uuid | ||||||
|  |  | ||||||
|     # Get user information to get the user_name |     # Get user information and determine effective user_name for this registration | ||||||
|     user = await db.instance.get_user_by_uuid(user_uuid) |     user = await db.instance.get_user_by_uuid(user_uuid) | ||||||
|     user_name = user.display_name |     user_name = user.display_name | ||||||
|  |     if name is not None: | ||||||
|  |         stripped = name.strip() | ||||||
|  |         if stripped: | ||||||
|  |             user_name = stripped | ||||||
|     challenge_ids = await db.instance.get_credentials_by_user_uuid(user_uuid) |     challenge_ids = await db.instance.get_credentials_by_user_uuid(user_uuid) | ||||||
|  |  | ||||||
|     # WebAuthn registration |     # WebAuthn registration | ||||||
|     credential = await register_chat(ws, user_uuid, user_name, challenge_ids, origin) |     credential = await register_chat(ws, user_uuid, user_name, challenge_ids, origin) | ||||||
|     # IMPORTANT: Insert the credential before creating a session that references it |  | ||||||
|     # to satisfy the sessions.credential_uuid foreign key (now enforced). |  | ||||||
|     await db.instance.create_credential(credential) |  | ||||||
|  |  | ||||||
|     if is_reset: |     # Create a new session and store everything in database | ||||||
|         # Invalidate the one-time reset session only after credential persisted |     token = create_token() | ||||||
|         await db.instance.delete_session(s.key) |     await db.instance.create_credential_session(  # type: ignore[attr-defined] | ||||||
|         auth = await create_session( |         user_uuid=user_uuid, | ||||||
|             user_uuid, credential.uuid, infodict(ws, "authenticated") |         credential=credential, | ||||||
|         ) |         reset_key=(s.key if reset is not None else None), | ||||||
|  |         session_key=session_key(token), | ||||||
|  |         session_expires=expires(), | ||||||
|  |         session_info=infodict(ws, "authenticated"), | ||||||
|  |         display_name=user_name, | ||||||
|  |     ) | ||||||
|  |     auth = token | ||||||
|  |  | ||||||
|     assert isinstance(auth, str) and len(auth) == 16 |     assert isinstance(auth, str) and len(auth) == 16 | ||||||
|     await ws.send_json( |     await ws.send_json( | ||||||
|   | |||||||
| @@ -7,4 +7,5 @@ def assert_safe(value: str, *, field: str = "value") -> None: | |||||||
|     if not isinstance(value, str) or not value or not _SAFE_RE.match(value): |     if not isinstance(value, str) or not value or not _SAFE_RE.match(value): | ||||||
|         raise ValueError(f"{field} must match ^[A-Za-z0-9:._~-]+$") |         raise ValueError(f"{field} must match ^[A-Za-z0-9:._~-]+$") | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ["assert_safe"] | __all__ = ["assert_safe"] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko