Renaming of users in registration, profile and admin app.
This commit is contained in:
parent
bc87f76d11
commit
37eaffff3f
@ -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"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user