Fixing cascade.
This commit is contained in:
		| @@ -70,6 +70,48 @@ function availableOrgsForPermission(pid) { | |||||||
|   return orgs.value.filter(o => !o.permissions.includes(pid)) |   return orgs.value.filter(o => !o.permissions.includes(pid)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function renamePermissionDisplay(p) { | ||||||
|  |   const newName = prompt('New display name', p.display_name) | ||||||
|  |   if (!newName || newName === p.display_name) return | ||||||
|  |   try { | ||||||
|  |     const body = { id: p.id, display_name: newName } | ||||||
|  |     const res = await fetch(`/auth/admin/permission?permission_id=${encodeURIComponent(p.id)}`, { | ||||||
|  |       method: 'PUT', | ||||||
|  |       headers: { 'Content-Type': 'application/json' }, | ||||||
|  |       body: JSON.stringify(body) | ||||||
|  |     }) | ||||||
|  |     const data = await res.json() | ||||||
|  |     if (data.detail) throw new Error(data.detail) | ||||||
|  |   await refreshPermissionsContext() | ||||||
|  |   } catch (e) { | ||||||
|  |     alert(e.message || 'Failed to rename display name') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function renamePermissionId(p) { | ||||||
|  |   const newId = prompt('New permission id', p.id) | ||||||
|  |   if (!newId || newId === p.id) return | ||||||
|  |   try { | ||||||
|  |     const body = { old_id: p.id, new_id: newId, display_name: p.display_name } | ||||||
|  |     const res = await fetch('/auth/admin/permission/rename', { | ||||||
|  |       method: 'POST', | ||||||
|  |       headers: { 'Content-Type': 'application/json' }, | ||||||
|  |       body: JSON.stringify(body) | ||||||
|  |     }) | ||||||
|  |     let data | ||||||
|  |     try { data = await res.json() } catch(_) { data = {} } | ||||||
|  |     if (!res.ok || data.detail) throw new Error(data.detail || data.error || `Failed (${res.status})`) | ||||||
|  |     await refreshPermissionsContext() | ||||||
|  |   } catch (e) { | ||||||
|  |     alert((e && e.message) ? e.message : 'Failed to rename permission id') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function refreshPermissionsContext() { | ||||||
|  |   // Reload both lists so All Permissions table shows new associations promptly. | ||||||
|  |   await Promise.all([loadPermissions(), loadOrgs()]) | ||||||
|  | } | ||||||
|  |  | ||||||
| async function attachPermissionToOrg(pid, orgUuid) { | async function attachPermissionToOrg(pid, orgUuid) { | ||||||
|   if (!orgUuid) return |   if (!orgUuid) return | ||||||
|   try { |   try { | ||||||
| @@ -184,6 +226,10 @@ async function updateOrg(org) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function deleteOrg(org) { | async function deleteOrg(org) { | ||||||
|  |   if (!info.value?.is_global_admin) { | ||||||
|  |     alert('Only global admins may delete organizations.') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|   if (!confirm(`Delete organization ${org.display_name}?`)) return |   if (!confirm(`Delete organization ${org.display_name}?`)) return | ||||||
|   const res = await fetch(`/auth/admin/orgs/${org.uuid}`, { method: 'DELETE' }) |   const res = await fetch(`/auth/admin/orgs/${org.uuid}`, { method: 'DELETE' }) | ||||||
|   const data = await res.json() |   const data = await res.json() | ||||||
| @@ -586,8 +632,8 @@ async function toggleRolePermission(role, permId, checked) { | |||||||
|             <div class="perm-grid-head center">Actions</div> |             <div class="perm-grid-head center">Actions</div> | ||||||
|             <template v-for="p in [...permissions].sort((a,b)=> a.id.localeCompare(b.id))" :key="p.id"> |             <template v-for="p in [...permissions].sort((a,b)=> a.id.localeCompare(b.id))" :key="p.id"> | ||||||
|               <div class="perm-cell perm-name" :title="p.id"> |               <div class="perm-cell perm-name" :title="p.id"> | ||||||
|                 <span class="perm-title">{{ p.display_name }}</span> |                 <div class="perm-title-line">{{ p.display_name }}</div> | ||||||
|                 <span class="perm-id muted">({{ p.id }})</span> |                 <div class="perm-id-line muted">{{ p.id }}</div> | ||||||
|               </div> |               </div> | ||||||
|               <div class="perm-cell perm-orgs" :title="permissionSummary[p.id]?.orgs?.map(o=>o.display_name).join(', ') || ''"> |               <div class="perm-cell perm-orgs" :title="permissionSummary[p.id]?.orgs?.map(o=>o.display_name).join(', ') || ''"> | ||||||
|                 <template v-if="permissionSummary[p.id]"> |                 <template v-if="permissionSummary[p.id]"> | ||||||
| @@ -626,7 +672,8 @@ async function toggleRolePermission(role, permId, checked) { | |||||||
|               </div> |               </div> | ||||||
|               <div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div> |               <div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div> | ||||||
|               <div class="perm-cell perm-actions center"> |               <div class="perm-cell perm-actions center"> | ||||||
|                 <button @click="updatePermission(p)" class="icon-btn" aria-label="Rename permission" title="Rename permission">✏️</button> |                 <button @click="renamePermissionDisplay(p)" class="icon-btn" aria-label="Change display name" title="Change display name">✏️</button> | ||||||
|  |                 <button @click="renamePermissionId(p)" class="icon-btn" aria-label="Change id" title="Change id">🆔</button> | ||||||
|                 <button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button> |                 <button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button> | ||||||
|               </div> |               </div> | ||||||
|             </template> |             </template> | ||||||
| @@ -727,8 +774,9 @@ button, .perm-actions button, .org-actions button, .role-actions button { width: | |||||||
| .permission-grid .perm-grid-head { font-size: .6rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; padding: .35rem .4rem; background: #f3f3f3; border: 1px solid #e1e1e1; } | .permission-grid .perm-grid-head { font-size: .6rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; padding: .35rem .4rem; background: #f3f3f3; border: 1px solid #e1e1e1; } | ||||||
| .permission-grid .perm-cell { background: #fff; border: 1px solid #eee; padding: .35rem .4rem; font-size: .7rem; display: flex; align-items: center; gap: .4rem; } | .permission-grid .perm-cell { background: #fff; border: 1px solid #eee; padding: .35rem .4rem; font-size: .7rem; display: flex; align-items: center; gap: .4rem; } | ||||||
| .permission-grid .perm-name { flex-direction: row; flex-wrap: wrap; } | .permission-grid .perm-name { flex-direction: row; flex-wrap: wrap; } | ||||||
| .permission-grid .perm-title { font-weight: 600; } | .permission-grid .perm-name { flex-direction: column; align-items: flex-start; gap:2px; } | ||||||
| .permission-grid .perm-id { font-size: .55rem; } | .permission-grid .perm-title-line { font-weight:600; line-height:1.1; } | ||||||
|  | .permission-grid .perm-id-line { font-size:.55rem; line-height:1.1; word-break:break-all; } | ||||||
| .permission-grid .center { justify-content: center; } | .permission-grid .center { justify-content: center; } | ||||||
| .permission-grid .perm-actions { gap: .25rem; } | .permission-grid .perm-actions { gap: .25rem; } | ||||||
| .permission-grid .perm-actions .icon-btn { font-size: .9rem; } | .permission-grid .perm-actions .icon-btn { font-size: .9rem; } | ||||||
|   | |||||||
| @@ -242,6 +242,18 @@ class DatabaseInterface(ABC): | |||||||
|     async def delete_permission(self, permission_id: str) -> None: |     async def delete_permission(self, permission_id: str) -> None: | ||||||
|         """Delete permission by ID.""" |         """Delete permission by ID.""" | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     async def rename_permission( | ||||||
|  |         self, old_id: str, new_id: str, display_name: str | ||||||
|  |     ) -> None: | ||||||
|  |         """Rename a permission's ID (and display name) updating all references. | ||||||
|  |  | ||||||
|  |         This must update: | ||||||
|  |             - permissions.id (primary key) | ||||||
|  |             - org_permissions.permission_id | ||||||
|  |             - role_permissions.permission_id | ||||||
|  |         """ | ||||||
|  |  | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     async def add_permission_to_organization( |     async def add_permission_to_organization( | ||||||
|         self, org_id: str, permission_id: str |         self, org_id: str, permission_id: str | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ from sqlalchemy import ( | |||||||
|     LargeBinary, |     LargeBinary, | ||||||
|     String, |     String, | ||||||
|     delete, |     delete, | ||||||
|  |     event, | ||||||
|     select, |     select, | ||||||
|     update, |     update, | ||||||
| ) | ) | ||||||
| @@ -226,6 +227,18 @@ class DB(DatabaseInterface): | |||||||
|     def __init__(self, db_path: str = DB_PATH): |     def __init__(self, db_path: str = DB_PATH): | ||||||
|         """Initialize with database path.""" |         """Initialize with database path.""" | ||||||
|         self.engine = create_async_engine(db_path, echo=False) |         self.engine = create_async_engine(db_path, echo=False) | ||||||
|  |         # Ensure SQLite foreign key enforcement is ON for every new connection | ||||||
|  |         if db_path.startswith("sqlite"): | ||||||
|  |  | ||||||
|  |             @event.listens_for(self.engine.sync_engine, "connect") | ||||||
|  |             def _fk_on(dbapi_connection, connection_record):  # type: ignore | ||||||
|  |                 try: | ||||||
|  |                     cursor = dbapi_connection.cursor() | ||||||
|  |                     cursor.execute("PRAGMA foreign_keys=ON;") | ||||||
|  |                     cursor.close() | ||||||
|  |                 except Exception: | ||||||
|  |                     pass | ||||||
|  |  | ||||||
|         self.async_session_factory = async_sessionmaker( |         self.async_session_factory = async_sessionmaker( | ||||||
|             self.engine, expire_on_commit=False |             self.engine, expire_on_commit=False | ||||||
|         ) |         ) | ||||||
| @@ -750,6 +763,63 @@ class DB(DatabaseInterface): | |||||||
|             ) |             ) | ||||||
|             await session.execute(stmt) |             await session.execute(stmt) | ||||||
|  |  | ||||||
|  |     async def rename_permission( | ||||||
|  |         self, old_id: str, new_id: str, display_name: str | ||||||
|  |     ) -> None: | ||||||
|  |         """Rename a permission's primary key and update referencing tables. | ||||||
|  |  | ||||||
|  |         Approach: insert new row (if id changes), update FKs, delete old row. | ||||||
|  |         Wrapped in a transaction; will raise on conflict. | ||||||
|  |         """ | ||||||
|  |         if old_id == new_id: | ||||||
|  |             # Just update display name | ||||||
|  |             async with self.session() as session: | ||||||
|  |                 stmt = ( | ||||||
|  |                     update(PermissionModel) | ||||||
|  |                     .where(PermissionModel.id == old_id) | ||||||
|  |                     .values(display_name=display_name) | ||||||
|  |                 ) | ||||||
|  |                 await session.execute(stmt) | ||||||
|  |             return | ||||||
|  |         async with self.session() as session: | ||||||
|  |             # Ensure old exists | ||||||
|  |             existing_old = await session.execute( | ||||||
|  |                 select(PermissionModel).where(PermissionModel.id == old_id) | ||||||
|  |             ) | ||||||
|  |             if not existing_old.scalar_one_or_none(): | ||||||
|  |                 raise ValueError("Original permission not found") | ||||||
|  |  | ||||||
|  |             # Check new not taken | ||||||
|  |             existing_new = await session.execute( | ||||||
|  |                 select(PermissionModel).where(PermissionModel.id == new_id) | ||||||
|  |             ) | ||||||
|  |             if existing_new.scalar_one_or_none(): | ||||||
|  |                 raise ValueError("New permission id already exists") | ||||||
|  |  | ||||||
|  |             # Create new permission row first | ||||||
|  |             session.add(PermissionModel(id=new_id, display_name=display_name)) | ||||||
|  |             await session.flush() | ||||||
|  |  | ||||||
|  |             # Update org_permissions | ||||||
|  |             await session.execute( | ||||||
|  |                 update(OrgPermission) | ||||||
|  |                 .where(OrgPermission.permission_id == old_id) | ||||||
|  |                 .values(permission_id=new_id) | ||||||
|  |             ) | ||||||
|  |             await session.flush() | ||||||
|  |             # Update role_permissions | ||||||
|  |             await session.execute( | ||||||
|  |                 update(RolePermission) | ||||||
|  |                 .where(RolePermission.permission_id == old_id) | ||||||
|  |                 .values(permission_id=new_id) | ||||||
|  |             ) | ||||||
|  |             await session.flush() | ||||||
|  |             # Delete old permission row | ||||||
|  |             await session.execute( | ||||||
|  |                 delete(PermissionModel).where(PermissionModel.id == old_id) | ||||||
|  |             ) | ||||||
|  |             await session.flush() | ||||||
|  |  | ||||||
|     async def delete_permission(self, permission_id: str) -> None: |     async def delete_permission(self, permission_id: str) -> None: | ||||||
|         async with self.session() as session: |         async with self.session() as session: | ||||||
|             stmt = delete(PermissionModel).where(PermissionModel.id == permission_id) |             stmt = delete(PermissionModel).where(PermissionModel.id == permission_id) | ||||||
|   | |||||||
| @@ -58,10 +58,17 @@ def register_api_routes(app: FastAPI): | |||||||
|         - For authenticated sessions: return full context (org/role/permissions/credentials) |         - For authenticated sessions: return full context (org/role/permissions/credentials) | ||||||
|         - For reset tokens: return only basic user information to drive reset flow |         - For reset tokens: return only basic user information to drive reset flow | ||||||
|         """ |         """ | ||||||
|  |         reset = False | ||||||
|         try: |         try: | ||||||
|             reset = auth and passphrase.is_well_formed(auth) |             if auth is None: | ||||||
|  |                 raise ValueError("Auth cookie missing") | ||||||
|  |             reset = passphrase.is_well_formed(auth) | ||||||
|             s = await (get_reset if reset else get_session)(auth) |             s = await (get_reset if reset else get_session)(auth) | ||||||
|         except ValueError: |         except ValueError as e: | ||||||
|  |             if reset: | ||||||
|  |                 print(e, reset, auth, tokens.reset_key(auth).hex()) | ||||||
|  |             else: | ||||||
|  |                 print(e, reset, auth) | ||||||
|             raise HTTPException( |             raise HTTPException( | ||||||
|                 status_code=401, |                 status_code=401, | ||||||
|                 detail="Authentication Required", |                 detail="Authentication Required", | ||||||
| @@ -235,15 +242,27 @@ def register_api_routes(app: FastAPI): | |||||||
|  |  | ||||||
|     @app.delete("/auth/admin/orgs/{org_uuid}") |     @app.delete("/auth/admin/orgs/{org_uuid}") | ||||||
|     async def admin_delete_org(org_uuid: UUID, auth=Cookie(None)): |     async def admin_delete_org(org_uuid: UUID, auth=Cookie(None)): | ||||||
|         _, is_global_admin, _ = await _get_ctx_and_admin_flags(auth) |         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) | ||||||
|         if not is_global_admin: |         if not is_global_admin: | ||||||
|  |             # Org admins cannot delete at all (avoid self-lockout) | ||||||
|             raise ValueError("Global admin required") |             raise ValueError("Global admin required") | ||||||
|  |         # Prevent deleting the organization that the acting global admin currently belongs to | ||||||
|  |         # if that deletion would remove their effective access (e.g., last org granting auth/admin) | ||||||
|  |         try: | ||||||
|  |             acting_org_uuid = ctx.org.uuid if ctx.org else None | ||||||
|  |         except Exception: | ||||||
|  |             acting_org_uuid = None | ||||||
|  |         if acting_org_uuid and acting_org_uuid == org_uuid: | ||||||
|  |             # Never allow deletion of the caller's own organization to avoid immediate account deletion. | ||||||
|  |             raise ValueError("Cannot delete the organization you belong to") | ||||||
|         await db.instance.delete_organization(org_uuid) |         await db.instance.delete_organization(org_uuid) | ||||||
|         return {"status": "ok"} |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     # Manage an org's grantable permissions (query param for permission_id) |     # Manage an org's grantable permissions (query param for permission_id) | ||||||
|     @app.post("/auth/admin/orgs/{org_uuid}/permission") |     @app.post("/auth/admin/orgs/{org_uuid}/permission") | ||||||
|     async def admin_add_org_permission(org_uuid: UUID, permission_id: str, auth=Cookie(None)): |     async def admin_add_org_permission( | ||||||
|  |         org_uuid: UUID, permission_id: str, auth=Cookie(None) | ||||||
|  |     ): | ||||||
|         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) |         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) | ||||||
|         if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)): |         if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)): | ||||||
|             raise ValueError("Insufficient permissions") |             raise ValueError("Insufficient permissions") | ||||||
| @@ -251,11 +270,15 @@ def register_api_routes(app: FastAPI): | |||||||
|         return {"status": "ok"} |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     @app.delete("/auth/admin/orgs/{org_uuid}/permission") |     @app.delete("/auth/admin/orgs/{org_uuid}/permission") | ||||||
|     async def admin_remove_org_permission(org_uuid: UUID, permission_id: str, auth=Cookie(None)): |     async def admin_remove_org_permission( | ||||||
|  |         org_uuid: UUID, permission_id: str, auth=Cookie(None) | ||||||
|  |     ): | ||||||
|         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) |         ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth) | ||||||
|         if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)): |         if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)): | ||||||
|             raise ValueError("Insufficient permissions") |             raise ValueError("Insufficient permissions") | ||||||
|         await db.instance.remove_permission_from_organization(str(org_uuid), permission_id) |         await db.instance.remove_permission_from_organization( | ||||||
|  |             str(org_uuid), permission_id | ||||||
|  |         ) | ||||||
|         return {"status": "ok"} |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     # -------------------- Admin API: Roles -------------------- |     # -------------------- Admin API: Roles -------------------- | ||||||
| @@ -336,6 +359,7 @@ def register_api_routes(app: FastAPI): | |||||||
|             raise ValueError("display_name and role are required") |             raise ValueError("display_name and role are required") | ||||||
|         # Validate role exists in org |         # Validate role exists in org | ||||||
|         from ..db import User as UserDC  # local import to avoid cycles |         from ..db import User as UserDC  # local import to avoid cycles | ||||||
|  |  | ||||||
|         roles = await db.instance.get_roles_by_organization(str(org_uuid)) |         roles = await db.instance.get_roles_by_organization(str(org_uuid)) | ||||||
|         role_obj = next((r for r in roles if r.display_name == role_name), None) |         role_obj = next((r for r in roles if r.display_name == role_name), None) | ||||||
|         if not role_obj: |         if not role_obj: | ||||||
| @@ -448,11 +472,14 @@ def register_api_routes(app: FastAPI): | |||||||
|                     "aaguid": aaguid_str, |                     "aaguid": aaguid_str, | ||||||
|                     "created_at": c.created_at.isoformat(), |                     "created_at": c.created_at.isoformat(), | ||||||
|                     "last_used": c.last_used.isoformat() if c.last_used else None, |                     "last_used": c.last_used.isoformat() if c.last_used else None, | ||||||
|                     "last_verified": c.last_verified.isoformat() if c.last_verified else None, |                     "last_verified": c.last_verified.isoformat() | ||||||
|  |                     if c.last_verified | ||||||
|  |                     else None, | ||||||
|                     "sign_count": c.sign_count, |                     "sign_count": c.sign_count, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         from .. import aaguid as aaguid_mod |         from .. import aaguid as aaguid_mod | ||||||
|  |  | ||||||
|         aaguid_info = aaguid_mod.filter(aaguids) |         aaguid_info = aaguid_mod.filter(aaguids) | ||||||
|         return { |         return { | ||||||
|             "display_name": user.display_name, |             "display_name": user.display_name, | ||||||
| @@ -492,14 +519,44 @@ def register_api_routes(app: FastAPI): | |||||||
|         return {"status": "ok"} |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     @app.put("/auth/admin/permission") |     @app.put("/auth/admin/permission") | ||||||
|     async def admin_update_permission(permission_id: str, display_name: str, auth=Cookie(None)): |     async def admin_update_permission( | ||||||
|  |         permission_id: str, display_name: str, auth=Cookie(None) | ||||||
|  |     ): | ||||||
|         _, is_global_admin, _ = await _get_ctx_and_admin_flags(auth) |         _, is_global_admin, _ = await _get_ctx_and_admin_flags(auth) | ||||||
|         if not is_global_admin: |         if not is_global_admin: | ||||||
|             raise ValueError("Global admin required") |             raise ValueError("Global admin required") | ||||||
|         from ..db import Permission as PermDC |         from ..db import Permission as PermDC | ||||||
|  |  | ||||||
|         if not display_name: |         if not display_name: | ||||||
|             raise ValueError("display_name is required") |             raise ValueError("display_name is required") | ||||||
|         await db.instance.update_permission(PermDC(id=permission_id, display_name=display_name)) |         await db.instance.update_permission( | ||||||
|  |             PermDC(id=permission_id, display_name=display_name) | ||||||
|  |         ) | ||||||
|  |         return {"status": "ok"} | ||||||
|  |  | ||||||
|  |     @app.post("/auth/admin/permission/rename") | ||||||
|  |     async def admin_rename_permission(payload: dict = Body(...), auth=Cookie(None)): | ||||||
|  |         """Rename a permission's id (and optionally display name) updating all references. | ||||||
|  |  | ||||||
|  |         Body: { "old_id": str, "new_id": str, "display_name": str|null } | ||||||
|  |         """ | ||||||
|  |         _, is_global_admin, _ = await _get_ctx_and_admin_flags(auth) | ||||||
|  |         if not is_global_admin: | ||||||
|  |             raise ValueError("Global admin required") | ||||||
|  |         old_id = payload.get("old_id") | ||||||
|  |         new_id = payload.get("new_id") | ||||||
|  |         display_name = payload.get("display_name") | ||||||
|  |         if not old_id or not new_id: | ||||||
|  |             raise ValueError("old_id and new_id required") | ||||||
|  |         if display_name is None: | ||||||
|  |             # Fetch old to retain display name | ||||||
|  |             perm = await db.instance.get_permission(old_id) | ||||||
|  |             display_name = perm.display_name | ||||||
|  |         # rename_permission added to interface; use getattr for forward compatibility | ||||||
|  |         rename_fn = getattr(db.instance, "rename_permission", None) | ||||||
|  |         if not rename_fn: | ||||||
|  |             raise ValueError("Permission renaming not supported by this backend") | ||||||
|  |         await rename_fn(old_id, new_id, display_name) | ||||||
|         return {"status": "ok"} |         return {"status": "ok"} | ||||||
|  |  | ||||||
|     @app.delete("/auth/admin/permission") |     @app.delete("/auth/admin/permission") | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ def register_reset_routes(app): | |||||||
|  |  | ||||||
|             response = RedirectResponse(url=f"{origin}/auth/", status_code=303) |             response = RedirectResponse(url=f"{origin}/auth/", status_code=303) | ||||||
|             session.set_session_cookie(response, reset_token) |             session.set_session_cookie(response, reset_token) | ||||||
|  |             print(response.headers) | ||||||
|             return response |             return response | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|   | |||||||
| @@ -69,17 +69,21 @@ async def websocket_register_add(ws: WebSocket, auth=Cookie(None)): | |||||||
|  |  | ||||||
|     # 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 reset: |     if reset: | ||||||
|         # Replace reset session with a new session |         # Invalidate the one-time reset session only after credential persisted | ||||||
|         await db.instance.delete_session(s.key) |         await db.instance.delete_session(s.key) | ||||||
|         token = await create_session( |         token = await create_session( | ||||||
|             user_uuid, credential.uuid, infodict(ws, "authenticated") |             user_uuid, credential.uuid, infodict(ws, "authenticated") | ||||||
|         ) |         ) | ||||||
|     else: |     else: | ||||||
|  |         # Existing session continues; we don't need to create a new one here. | ||||||
|         token = auth |         token = auth | ||||||
|  |  | ||||||
|     assert isinstance(token, str) and len(token) == 16 |     assert isinstance(token, str) and len(token) == 16 | ||||||
|     # Store the new credential in the database |  | ||||||
|     await db.instance.create_credential(credential) |  | ||||||
|  |  | ||||||
|     await ws.send_json( |     await ws.send_json( | ||||||
|         { |         { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko