fix: invalidate tokens on permission change (medium), optimize create_user query, fix ws reconnect race

- update_permissions now calls blacklist_active_refresh_token and
  rotate_user_secret to immediately invalidate stale tokens
- create_user uses returned id from tx.run_one instead of redundant SELECT
- websocket reconnect explicitly closes old connection after token refresh
  to prevent onclose handler race condition
This commit is contained in:
2026-07-28 18:02:14 +00:00
parent 8bb3619ddc
commit a82578f342
2 changed files with 10 additions and 4 deletions
+8 -4
View File
@@ -160,13 +160,13 @@ def create_user(
db = get_db()
with db.in_transaction() as tx:
tx.run_one(Q_INSERT_USER, (username, password_hash, jwt_secret))
user_id = tx.run_one(Q_INSERT_USER, (username, password_hash, jwt_secret))
if permissions:
for subsystem, level in permissions.items():
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
return {
"id": db.query(Q_SELECT_USER_BY_NAME, (username,))[0]["id"],
"id": user_id,
"username": username,
"permissions": _get_permissions(username),
}
@@ -202,9 +202,11 @@ def update_password(username: str, old_password: str, new_password: str) -> bool
def update_permissions(username: str, permissions: dict[str, str]) -> None:
"""Update a user's permissions.
"""Update a user's permissions and invalidate all existing tokens.
Replaces all existing permissions with the provided mapping.
Replaces all existing permissions with the provided mapping. Rotates the
JWT secret so that permission changes take effect immediately — existing
tokens with stale permissions are no longer valid.
Args:
username: The username.
@@ -219,6 +221,8 @@ def update_permissions(username: str, permissions: dict[str, str]) -> None:
tx.run(Q_DELETE_PERMISSIONS, (username,))
for subsystem, level in permissions.items():
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
blacklist_active_refresh_token(username)
rotate_user_secret(username)
def list_users() -> list[dict[str, Any]]:
+2
View File
@@ -59,12 +59,14 @@ function _wsConnect() {
if (_wsFailCount >= 3 && !_wsRefreshing) {
_wsRefreshing = true;
const oldConn = _wsConn;
tryRefreshToken().then(ok => {
_wsRefreshing = false;
if (ok) {
_wsFailCount = 0;
_wsReconnectMs = 0;
_wsConn = null;
if (oldConn) oldConn.close();
setTimeout(_wsConnect, 100);
} else {
redirectLogin();