Skip to content

authentication refactoring#831

Merged
PythonFZ merged 9 commits intomainfrom
feat/auth-refactoring
Jan 8, 2026
Merged

authentication refactoring#831
PythonFZ merged 9 commits intomainfrom
feat/auth-refactoring

Conversation

@PythonFZ
Copy link
Copy Markdown
Member

@PythonFZ PythonFZ commented Jan 7, 2026

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Explicit POST /api/rooms to create rooms; frontend supports join-or-create with optional template/copy-from.
    • New user registration and upgrade endpoints; guest registration flow supported.
  • Improvements

    • Authentication now JWT-based (includes iat/exp, ~7-day expiry) carrying username/role.
    • Stronger password policy and Argon2 hashing; username validation added.
    • Centralized UTC time utilities used across services.
  • Tests

    • Tests updated to register users and create rooms before join/login flows.

✏️ Tip: You can customize this high-level summary in your review settings.

PythonFZ and others added 4 commits January 7, 2026 19:29
BREAKING CHANGES:
- Room creation is now separate from joining:
  - POST /api/rooms creates a new room (returns 201)
  - POST /api/rooms/{id}/join joins existing rooms only (returns 404 if not exists)
- Password hashing upgraded from SHA-256 to Argon2id (existing passwords invalidated)
- JWT tokens now have 7-day expiration

Key changes:
- Add ensure_user_exists() to UserService for idempotent user creation
- Users are created on socket connect from JWT claims, not during login
- JWT is now the single source of truth for identity
- Simplified login endpoint - guests get JWT without Redis entry until socket connect
- Added argon2-cffi dependency for secure password hashing
- Frontend: Removed redundant role state, use getRoleFromToken() only
- Updated all tests to use create-then-join pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This refactoring enforces strict single responsibility per endpoint:
- /api/user/register: ONLY place for user creation (guest or registered)
- /api/login: ONLY authenticates users (never creates)
- Socket connect and room join validate user exists

Key changes:
- Add password validation (minimum 8 characters)
- Add get_authoritative_role() for Redis-based role checks
- Add is_admin_username() to AdminService for proper error messages
- Replace deprecated datetime.utcnow() with time utilities
- Remove redundant USERNAME_KEY from frontend localStorage
- Update Python client to use register-then-login flow

All users must now register before login. Guest flow:
1. POST /api/user/register (creates user)
2. POST /api/login (authenticates, returns JWT)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security fixes:
- Unify all auth error messages to "Authentication failed" to prevent
  username enumeration attacks
- Add username validation (1-64 chars, alphanumeric + underscore/hyphen)
  to prevent path injection, XSS, and DoS attacks

Bug fixes:
- Fix 409 handling in frontend auth.ts - now properly handles case where
  registerData.userName is undefined on conflict response

Code cleanup:
- Remove unused get_authoritative_role() function (YAGNI)
- Update get_current_user_role() docstring to reference AdminService

Test updates:
- Update tests to expect generic "Authentication failed" message
- Fix test usernames to comply with new validation (John Doe → john-doe)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Refactors auth to use JWT claims (iat/exp) as source-of-truth, adds explicit POST /api/rooms create endpoint and client join-or-create flow, migrates password hashing to Argon2id with username/password validation, and centralizes UTC time utilities across backend modules.

Changes

Cohort / File(s) Summary
Frontend auth & rest
app/src/utils/auth.ts, app/src/hooks/useRestManager.ts
Auth now uses JWT claims (getUserRole(), getUsernameFromToken(), ensureAuthenticated()); login() supports guest register-then-login; useRestManager switched to joinOrCreateRoom() and reads role from token
Frontend API client
app/src/myapi/client.ts
Added createRoom() and joinOrCreateRoom() (fallback: join → on 404 create → join); removed allowCreate/created join flags and simplified join request/response shapes
Backend auth & tokens / routes
src/zndraw/auth.py, src/zndraw/app/utility_routes.py
JWTs now include iat/exp and jti; added JWT_EXPIRY_SECONDS; new endpoints POST /api/user/register and POST /api/user/upgrade; login flow refactored to require/validate registration; removed /api/user/role
Backend user service (security & validation)
src/zndraw/services/user_service.py, pyproject.toml
Switched to Argon2id hashing (added argon2-cffi); added validate_password(), validate_username(), PasswordValidationError, UsernameValidationError, ensure_user_exists(); removed legacy salt/SHA-256 helpers
Backend room management & python client
src/zndraw/app/room_routes.py, src/zndraw/api_manager.py
Added POST /api/rooms (create_room); join now validates existence/user and returns minimal join response; python client implements create-on-404 fallback and create_room()
Time utilities & consolidation
src/zndraw/utils/time.py, multiple backend files*
New utc_now(), utc_now_iso(), utc_now_timestamp(); replaced ad-hoc datetime usages across events, job manager/dispatcher, lock/worker/extension routes
Admin service
src/zndraw/services/admin_service.py
Constant-time credential comparison via secrets.compare_digest and added is_admin_username()
Job executor / extension loading
src/zndraw/job_executor.py
Removed public param from _get_extension_class signature; sanitize worker identity (colons→dashes)
Tests & fixtures
tests/**/* (conftest.py and many tests)
Tests updated to pre-create rooms via POST /api/rooms, register users where required, add JWT expiry tests, adapt to Argon2 and username validation, add _create_and_join_room() helpers and adjust fixtures (e.g., authenticated_session)

*Examples of files updated to use time helpers: src/zndraw/app/events.py, extension_routes.py, job_dispatcher.py, job_manager.py, lock_routes.py, worker_routes.py.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthAPI as Auth Routes
    participant UserSvc as User Service
    participant JWTAuth as JWT Creator
    participant Redis

    Note over Client,Redis: Guest register-then-login flow
    Client->>AuthAPI: POST /api/user/register {userName?}
    AuthAPI->>UserSvc: ensure_user_exists(userName)
    UserSvc->>Redis: create or return user entry
    UserSvc-->>AuthAPI: userName
    AuthAPI->>JWTAuth: create_jwt_token(userName, role)
    JWTAuth-->>AuthAPI: token (with iat, exp, jti)
    AuthAPI-->>Client: 201 {token, userName, role}
Loading
sequenceDiagram
    participant Client
    participant RoomAPI as Room Routes
    participant RoomSvc as Room Service
    participant UserSvc as User Service
    participant Redis

    Note over Client,Redis: Create → Join-or-Create flow
    alt Client explicit create
        Client->>RoomAPI: POST /api/rooms {roomId, copyFrom?}
        RoomAPI->>UserSvc: user_exists(currentUser)
        UserSvc-->>RoomAPI: true
        RoomAPI->>RoomSvc: create_room(roomId, copyFrom)
        RoomSvc->>Redis: store room (+copy frames)
        RoomSvc-->>RoomAPI: room created
        RoomAPI-->>Client: 201 {status, roomId}
    else Client join (join-or-create client)
        Client->>RoomAPI: POST /api/rooms/{roomId}/join
        RoomAPI->>RoomSvc: validate room exists
        RoomSvc->>Redis: fetch room
        alt room exists
            RoomAPI->>Redis: create session (connectedAt via utc_now_iso)
            RoomAPI-->>Client: 200 {status, sessionId, roomId}
        else missing
            RoomAPI-->>Client: 404 Room not found
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 Hops and keys, a token bright,

Rooms now bloom by morning light,
Argon2 guards every den,
UTC time keeps track of when,
Rabbit cheers — hop on, my friend!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "authentication refactoring" accurately summarizes the primary change: a comprehensive refactor of the authentication system and related user/room management. It directly reflects the main focus of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 96.05% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/zndraw/services/admin_service.py (1)

61-79: Use constant-time comparison for credential validation.

The method uses standard == operators to compare both username and password against configured values. Standard string equality comparison short-circuits on the first character difference, which makes it vulnerable to timing attacks. An attacker could measure response times to gradually learn the correct credentials character by character.

Use secrets.compare_digest() for constant-time comparison of security-sensitive strings.

🔒 Proposed fix using secrets.compare_digest
+import secrets
+
 """Admin access control service.

 Manages admin user privileges and deployment mode detection.
     def validate_admin_credentials(self, username: str, password: str) -> bool:
         """Validate admin credentials against configured values.
 
         Parameters
         ----------
         username : str
             Username to validate
         password : str
             Password to validate
 
         Returns
         -------
         bool
             True if credentials match admin configuration
         """
         if not self._deployment_mode:
             return False
 
-        return username == self._admin_username and password == self._admin_password
+        username_match = secrets.compare_digest(
+            username.encode('utf-8'),
+            (self._admin_username or '').encode('utf-8')
+        )
+        password_match = secrets.compare_digest(
+            password.encode('utf-8'),
+            (self._admin_password or '').encode('utf-8')
+        )
+        return username_match and password_match
src/zndraw/services/user_service.py (1)

492-518: Missing cleanup of visited_rooms key leaves orphaned data.

The UserKeys class defines a visited_rooms() key (per src/zndraw/app/redis_keys.py), but delete_user doesn't delete it. This leaves orphaned data in Redis after user deletion.

🐛 Proposed fix
     def delete_user(self, user_name: str) -> bool:
         keys = UserKeys(user_name)

         pipe = self.r.pipeline()
         pipe.delete(keys.hash_key())
         pipe.delete(keys.admin_key())
+        pipe.delete(keys.visited_rooms())
         # Remove from indices
         pipe.srem(GlobalIndexKeys.users_index(), user_name)
         pipe.srem(GlobalIndexKeys.admins_index(), user_name)
         pipe.execute()
🤖 Fix all issues with AI agents
In @pyproject.toml:
- Line 14: Update the argon2-cffi dependency version in pyproject.toml by
replacing the existing requirement string "argon2-cffi>=23.1.0" with
"argon2-cffi>=25.1.0"; edit the dependency line so the project will install the
newer 25.1.0+ release and then run your dependency install/update (e.g., poetry
install or pip-tools refresh) and tests to verify compatibility.

In @tests/test_auth.py:
- Around line 130-150: The test test_expired_jwt_token_raises_error uses
deprecated datetime.datetime.utcnow(); import utc_now from zndraw.utils.time and
replace both datetime.datetime.utcnow() calls in that test with utc_now() so the
iat and exp fields are set using utc_now() - ensure you add the import for
utc_now at the top of tests/test_auth.py and remove or keep the datetime import
only if still needed elsewhere.
🧹 Nitpick comments (16)
tests/test_zndraw_admin.py (1)

161-161: Consider using a raw string for the regex pattern.

While the current pattern works, best practice for regex patterns in pytest.raises is to use raw strings to avoid potential issues with escape sequences.

♻️ Proposed fix
-    with pytest.raises(RuntimeError, match="Login failed.*Authentication failed"):
+    with pytest.raises(RuntimeError, match=r"Login failed.*Authentication failed"):
tests/test_partial_frame_update.py (1)

466-469: Consider adding timeout parameter for production-grade tests.

While this is test code, adding timeout=5 to the requests.post() call would prevent tests from hanging indefinitely if the server becomes unresponsive.

♻️ Proposed refinement
     create_response = requests.post(
-        f"{server}/api/rooms", json={"roomId": room}, headers=auth_headers
+        f"{server}/api/rooms", json={"roomId": room}, headers=auth_headers, timeout=5
     )
tests/test_chat.py (1)

14-17: Consider adding timeout parameter for production-grade tests.

While this is test code, adding timeout=5 to both requests.post() calls would prevent tests from hanging indefinitely if the server becomes unresponsive.

♻️ Proposed refinement
     create_response = requests.post(
-        f"{server}/api/rooms", json={"roomId": room}, headers=headers
+        f"{server}/api/rooms", json={"roomId": room}, headers=headers, timeout=5
     )
     assert create_response.status_code == 201

     # Join room
     response = requests.post(
-        f"{server}/api/rooms/{room}/join", json={}, headers=headers
+        f"{server}/api/rooms/{room}/join", json={}, headers=headers, timeout=5
     )

Also applies to: 20-21

tests/test_auth.py (1)

104-127: Move datetime import to module level.

The datetime import on line 106 should be at the top of the file per coding guidelines. Consider importing it alongside other imports at the module level.

♻️ Proposed refactor

Move the import to the top of the file (after line 4):

 import jwt as pyjwt
 import pytest
+import datetime

 from zndraw.auth import (

And remove the local import:

 def test_jwt_token_has_expiration(app):
     """Test that JWT tokens have a 7-day expiration claim."""
-    import datetime
-
     with app.app_context():
tests/conftest.py (2)

57-89: LGTM! Register-then-login flow correctly implemented.

The updated authentication flow properly registers the user before logging in, aligning with the new API design. The 409 handling for existing users enables test reusability.

Minor: Consider adding timeout parameter to requests.post calls to prevent tests from hanging indefinitely if the server becomes unresponsive.


92-151: Room create-then-join logic is sound.

The helper correctly handles the 404 → create → join flow. However, consider adding timeouts to prevent test hangs.

♻️ Optional: Add timeouts to requests calls
 def _join_room_and_get_headers(
     server: str, room_id: str, user: str = "test-user"
 ) -> dict:
     ...
     # Try to join existing room first
     response = requests.post(
         f"{server}/api/rooms/{room_id}/join",
         json={},
         headers=auth_headers,
+        timeout=10,
     )

     # If room doesn't exist (404), create it then join
     if response.status_code == 404:
         create_response = requests.post(
             f"{server}/api/rooms",
             json={"roomId": room_id},
             headers=auth_headers,
+            timeout=10,
         )
         ...
         # Now join the newly created room
         response = requests.post(
             f"{server}/api/rooms/{room_id}/join",
             json={},
             headers=auth_headers,
+            timeout=10,
         )
tests/test_step_endpoints.py (2)

8-24: Consolidate duplicated _create_and_join_room helper into conftest.py.

This exact helper function is duplicated in test_lock_socket_events.py and test_requires_lock_decorator.py. Consider moving it to conftest.py and exposing it as a fixture or importable function to follow DRY principles.

Based on the relevant code snippets, conftest.py already has _join_room_and_get_headers which handles create-on-404. You could either:

  1. Reuse _join_room_and_get_headers in tests, or
  2. Add a simpler _create_and_join_room to conftest.py and remove the duplicates

51-67: Test name is misleading.

The function is named test_get_step_without_auth but it actually tests with authentication (auth_headers). Consider renaming to test_get_step_returns_data_for_existing_room to match the docstring.

tests/test_user_service.py (1)

531-561: Consider using pytest.mark.parametrize to reduce test setup duplication.

Per coding guidelines, parametrize can help avoid code duplication. The password validation tests follow a similar pattern.

♻️ Refactor suggestion using parametrize
@pytest.mark.parametrize(
    "password,should_fail,match_pattern",
    [
        ("1", True, "at least 8 characters"),
        ("   ", True, "at least 8 characters"),
        ("validpass123", False, None),
        ("x" * 1000, False, None),
    ],
)
def test_password_requirements_enforced(user_service, password, should_fail, match_pattern):
    """Test that password requirements are enforced."""
    from zndraw.services.user_service import PasswordValidationError
    
    user_name = f"test-user-{uuid.uuid4().hex[:8]}"
    user_service.create_user(user_name)
    
    if should_fail:
        with pytest.raises(PasswordValidationError, match=match_pattern):
            user_service.register_user(user_name, user_name, password)
    else:
        user_service.register_user(user_name, user_name, password)
        assert user_service.verify_password(user_name, password)
src/zndraw/app/room_routes.py (1)

135-208: LGTM! Clean implementation of explicit room creation endpoint.

Good separation of concerns:

  • Validates roomId presence and format
  • Checks for duplicates (409)
  • Supports optional copyFrom for cloning rooms
  • Broadcasts creation via socket
  • Returns 201 on success

One minor concern: Line 187 returns 404 for ValueError, but this could mask errors beyond "source room not found" (e.g., validation errors from copy_from). Consider more specific error handling.

♻️ Optional: More specific error handling
     try:
         result = room_service.create_room(room_id, user_name, description, copy_from)
     except ValueError as e:
-        return {"error": str(e)}, 404
+        error_msg = str(e)
+        # 404 only for "not found" errors, 400 for other validation errors
+        if "not found" in error_msg.lower() or "does not exist" in error_msg.lower():
+            return {"error": error_msg}, 404
+        return {"error": error_msg}, 400
tests/test_auth_integration.py (1)

10-34: Add timeout parameter to requests calls for test reliability.

Multiple requests.post and requests.get calls throughout this file lack the timeout parameter. This could cause tests to hang indefinitely if the server becomes unresponsive. Static analysis (Ruff S113) flagged this across ~19 locations.

Consider adding a consistent timeout (e.g., timeout=10) to all requests calls in this file for consistency with test_lazy_room_loading.py.

Example fix for the first few occurrences
     # Step 1: Register user
     register_response = requests.post(
-        f"{server}/api/user/register", json={"userName": "test-user"}
+        f"{server}/api/user/register", json={"userName": "test-user"}, timeout=10
     )
     ...
     # Step 2: Login to get JWT
-    response = requests.post(f"{server}/api/login", json={"userName": "test-user"})
+    response = requests.post(f"{server}/api/login", json={"userName": "test-user"}, timeout=10)
src/zndraw/api_manager.py (2)

233-240: Add timeout to requests.post call.

The create_room method's POST request lacks a timeout, which could cause the client to hang indefinitely.

Proposed fix
-        response = requests.post(f"{self.url}/api/rooms", json=payload, headers=headers)
+        response = requests.post(f"{self.url}/api/rooms", json=payload, headers=headers, timeout=10.0)

265-278: Add timeout to requests.post calls in join_room.

Both POST requests in join_room (lines 266 and 276) lack timeouts.

Proposed fix
         # Try to join existing room first
         response = requests.post(
-            f"{self.url}/api/rooms/{self.room}/join", json={}, headers=headers
+            f"{self.url}/api/rooms/{self.room}/join", json={}, headers=headers, timeout=10.0
         )

         # If room doesn't exist (404), create it then join
         if response.status_code == 404:
             log.info(f"Room '{self.room}' doesn't exist, creating it...")
             self.create_room(description=description, copy_from=copy_from)

             # Now join the newly created room
             response = requests.post(
-                f"{self.url}/api/rooms/{self.room}/join", json={}, headers=headers
+                f"{self.url}/api/rooms/{self.room}/join", json={}, headers=headers, timeout=10.0
             )
src/zndraw/app/utility_routes.py (2)

324-326: Consider using logging.exception for full traceback.

When catching the generic Exception, using log.exception() instead of log.error() will include the stack trace, which is helpful for debugging unexpected errors.

Proposed fix
     except Exception as e:
-        log.error(f"Error registering user: {e}")
+        log.exception(f"Error registering user: {e}")
         return {"error": "Registration failed", "type": "ServerError"}, 500

390-392: Same suggestion: use logging.exception for upgrade_user.

Proposed fix
     except Exception as e:
-        log.error(f"Error upgrading user: {e}")
+        log.exception(f"Error upgrading user: {e}")
         return {"error": "Upgrade failed", "type": "ServerError"}, 500
src/zndraw/services/user_service.py (1)

232-268: Potential TOCTOU race condition in user creation.

The check-then-act pattern between username_exists() and the pipeline execution isn't atomic. Two concurrent calls with the same username could both pass the existence check, resulting in the second call silently overwriting the first user's timestamps.

For auto-generated guest usernames with UUIDs, this is extremely unlikely. However, if callers provide explicit usernames, this could cause subtle data corruption.

Consider using HSETNX for the initial field or a Lua script for true atomicity if explicit usernames are supported:

♻️ Atomic alternative using HSETNX
     def ensure_user_exists(self, user_name: str) -> bool:
-        if self.username_exists(user_name):
-            return False
-
         keys = UserKeys(user_name)
         current_time = utc_now_iso()
 
-        pipe = self.r.pipeline()
-        pipe.hset(
-            keys.hash_key(),
-            mapping={
-                "userName": user_name,
-                "createdAt": current_time,
-                "lastLogin": current_time,
-            },
-        )
-        pipe.sadd(GlobalIndexKeys.users_index(), user_name)
-        pipe.execute()
+        # HSETNX returns 1 if field was set, 0 if it existed
+        created = self.r.hsetnx(keys.hash_key(), "userName", user_name)
+        if not created:
+            return False
+
+        # User was just created, set remaining fields
+        pipe = self.r.pipeline()
+        pipe.hset(keys.hash_key(), mapping={
+            "createdAt": current_time,
+            "lastLogin": current_time,
+        })
+        pipe.sadd(GlobalIndexKeys.users_index(), user_name)
+        pipe.execute()
 
         log.info(f"Created guest user {user_name}")
         return True
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8782043 and 99633ba.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • app/src/hooks/useRestManager.ts
  • app/src/myapi/client.ts
  • app/src/utils/auth.ts
  • pyproject.toml
  • src/zndraw/api_manager.py
  • src/zndraw/app/events.py
  • src/zndraw/app/extension_routes.py
  • src/zndraw/app/job_dispatcher.py
  • src/zndraw/app/job_manager.py
  • src/zndraw/app/lock_routes.py
  • src/zndraw/app/room_routes.py
  • src/zndraw/app/utility_routes.py
  • src/zndraw/app/worker_routes.py
  • src/zndraw/auth.py
  • src/zndraw/services/admin_service.py
  • src/zndraw/services/user_service.py
  • src/zndraw/utils/__init__.py
  • src/zndraw/utils/time.py
  • tests/conftest.py
  • tests/test_admin_auth.py
  • tests/test_auth.py
  • tests/test_auth_integration.py
  • tests/test_chat.py
  • tests/test_lazy_room_loading.py
  • tests/test_lock_socket_events.py
  • tests/test_partial_frame_update.py
  • tests/test_requires_lock_decorator.py
  • tests/test_room_lock_enforcement.py
  • tests/test_step_endpoints.py
  • tests/test_user_service.py
  • tests/test_vis_room.py
  • tests/test_zndraw_admin.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: If sensible, implement collections.abc interfaces for classes, such as MutableMapping or MutableSequence
Use numpy style docstrings
Docstrings must be concise and to the point
Use type hints wherever possible. Import typing as t if necessary, but use list[int|float] | None instead of t.Optional[t.List[int|float]]
Imports should always be at the top of the file

Files:

  • tests/test_room_lock_enforcement.py
  • src/zndraw/utils/__init__.py
  • src/zndraw/app/events.py
  • src/zndraw/utils/time.py
  • src/zndraw/app/job_manager.py
  • src/zndraw/app/worker_routes.py
  • src/zndraw/auth.py
  • src/zndraw/app/lock_routes.py
  • src/zndraw/services/admin_service.py
  • src/zndraw/app/job_dispatcher.py
  • tests/test_lazy_room_loading.py
  • src/zndraw/app/extension_routes.py
  • tests/test_auth_integration.py
  • tests/test_auth.py
  • tests/test_chat.py
  • src/zndraw/services/user_service.py
  • tests/test_partial_frame_update.py
  • src/zndraw/app/utility_routes.py
  • tests/test_step_endpoints.py
  • tests/test_admin_auth.py
  • tests/conftest.py
  • src/zndraw/app/room_routes.py
  • tests/test_user_service.py
  • tests/test_zndraw_admin.py
  • tests/test_vis_room.py
  • tests/test_requires_lock_decorator.py
  • src/zndraw/api_manager.py
  • tests/test_lock_socket_events.py
**/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/test_*.py: Use pytest.mark.parametrize to avoid code duplication in tests
Tests should be very specific and test only one thing
Avoid complex test setups
Each test must be a function, not a method of a class

Files:

  • tests/test_room_lock_enforcement.py
  • tests/test_lazy_room_loading.py
  • tests/test_auth_integration.py
  • tests/test_auth.py
  • tests/test_chat.py
  • tests/test_partial_frame_update.py
  • tests/test_step_endpoints.py
  • tests/test_admin_auth.py
  • tests/test_user_service.py
  • tests/test_zndraw_admin.py
  • tests/test_vis_room.py
  • tests/test_requires_lock_decorator.py
  • tests/test_lock_socket_events.py
🧬 Code graph analysis (20)
src/zndraw/app/events.py (4)
src/zndraw/utils/time.py (1)
  • utc_now_iso (22-30)
tests/test_user_service.py (1)
  • user_service (17-19)
src/zndraw/services/user_service.py (1)
  • username_exists (131-145)
src/zndraw/app/redis_keys.py (3)
  • UserKeys (518-564)
  • hash_key (534-536)
  • hash_key (642-644)
src/zndraw/app/job_manager.py (2)
src/zndraw/utils/time.py (2)
  • utc_now_iso (22-30)
  • utc_now_timestamp (33-41)
src/zndraw/app/redis_keys.py (1)
  • jobs_by_time (397-399)
src/zndraw/app/worker_routes.py (1)
src/zndraw/utils/time.py (1)
  • utc_now_timestamp (33-41)
src/zndraw/auth.py (1)
src/zndraw/utils/time.py (1)
  • utc_now (9-19)
src/zndraw/app/lock_routes.py (1)
src/zndraw/utils/time.py (1)
  • utc_now_iso (22-30)
src/zndraw/app/job_dispatcher.py (1)
src/zndraw/utils/time.py (1)
  • utc_now_timestamp (33-41)
tests/test_lazy_room_loading.py (1)
tests/conftest.py (1)
  • server (300-306)
src/zndraw/app/extension_routes.py (1)
src/zndraw/utils/time.py (1)
  • utc_now_timestamp (33-41)
tests/test_auth_integration.py (2)
src/zndraw/app/redis_keys.py (4)
  • username (573-575)
  • UserKeys (518-564)
  • hash_key (534-536)
  • hash_key (642-644)
src/zndraw/app/metadata_manager.py (1)
  • exists (127-135)
tests/test_auth.py (1)
src/zndraw/auth.py (3)
  • create_jwt_token (54-86)
  • decode_jwt_token (89-116)
  • AuthError (20-34)
tests/test_chat.py (1)
tests/conftest.py (2)
  • get_jwt_auth_headers (167-169)
  • server (300-306)
src/zndraw/services/user_service.py (2)
src/zndraw/app/redis_keys.py (5)
  • GlobalIndexKeys (658-701)
  • UserKeys (518-564)
  • hash_key (534-536)
  • hash_key (642-644)
  • users_index (682-690)
src/zndraw/utils/time.py (1)
  • utc_now_iso (22-30)
tests/test_partial_frame_update.py (2)
tests/conftest.py (1)
  • server (300-306)
tests/test_room_lock_enforcement.py (1)
  • auth_headers (44-46)
tests/conftest.py (1)
tests/test_room_lock_enforcement.py (1)
  • auth_headers (44-46)
tests/test_user_service.py (5)
src/zndraw/app/utility_routes.py (1)
  • change_password (397-434)
src/zndraw/services/user_service.py (4)
  • change_password (402-443)
  • reset_password (445-479)
  • verify_password (369-400)
  • PasswordValidationError (27-30)
src/zndraw/app/redis_keys.py (3)
  • UserKeys (518-564)
  • hash_key (534-536)
  • hash_key (642-644)
src/zndraw/frame_cache.py (1)
  • keys (10-11)
src/zndraw/app/metadata_manager.py (1)
  • exists (127-135)
app/src/hooks/useRestManager.ts (2)
app/src/utils/auth.ts (1)
  • getUserRole (240-243)
app/src/myapi/client.ts (1)
  • joinOrCreateRoom (471-495)
tests/test_requires_lock_decorator.py (3)
tests/test_lock_socket_events.py (1)
  • _create_and_join_room (10-25)
tests/test_step_endpoints.py (1)
  • _create_and_join_room (8-23)
tests/test_room_lock_enforcement.py (1)
  • auth_headers (44-46)
src/zndraw/api_manager.py (2)
src/zndraw/app/room_routes.py (2)
  • create_room (137-207)
  • join_room (257-335)
src/zndraw/services/room_service.py (1)
  • create_room (70-112)
app/src/utils/auth.ts (3)
src/zndraw/app/utility_routes.py (1)
  • login (156-201)
src/zndraw/api_manager.py (1)
  • login (22-132)
src/zndraw/services/user_service.py (1)
  • UserRole (99-104)
tests/test_lock_socket_events.py (1)
tests/conftest.py (2)
  • server (300-306)
  • get_jwt_auth_headers (167-169)
🪛 Ruff (0.14.10)
src/zndraw/app/events.py

118-118: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_auth_integration.py

13-13: Probable use of requests call without timeout

(S113)


40-40: Probable use of requests call without timeout

(S113)


48-48: Probable use of requests call without timeout

(S113)


62-62: Probable use of requests call without timeout

(S113)


72-72: Probable use of requests call without timeout

(S113)


89-89: Probable use of requests call without timeout

(S113)


97-97: Probable use of requests call without timeout

(S113)


182-182: Probable use of requests call without timeout

(S113)


209-209: Probable use of requests call without timeout

(S113)


259-259: Probable use of requests call without timeout

(S113)


279-279: Probable use of requests call without timeout

(S113)


302-302: Probable use of requests call without timeout

(S113)


310-310: Probable use of requests call without timeout

(S113)


332-332: Probable use of requests call without timeout

(S113)


349-349: Probable use of requests call without timeout

(S113)


369-369: Probable use of requests call without timeout

(S113)


377-377: Probable use of requests call without timeout

(S113)


394-394: Probable use of requests call without timeout

(S113)


402-402: Probable use of requests call without timeout

(S113)

tests/test_chat.py

14-14: Probable use of requests call without timeout

(S113)


20-20: Probable use of requests call without timeout

(S113)

src/zndraw/services/user_service.py

50-52: Avoid specifying long messages outside the exception class

(TRY003)


85-85: Avoid specifying long messages outside the exception class

(TRY003)


88-90: Avoid specifying long messages outside the exception class

(TRY003)


93-96: Avoid specifying long messages outside the exception class

(TRY003)


398-398: Consider moving this statement to an else block

(TRY300)


432-432: Avoid specifying long messages outside the exception class

(TRY003)


435-435: Avoid specifying long messages outside the exception class

(TRY003)


471-471: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_partial_frame_update.py

466-466: Probable use of requests call without timeout

(S113)

src/zndraw/app/utility_routes.py

318-318: Consider moving this statement to an else block

(TRY300)


324-324: Do not catch blind exception: Exception

(BLE001)


325-325: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


390-390: Do not catch blind exception: Exception

(BLE001)


391-391: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

tests/test_step_endpoints.py

11-11: Probable use of requests call without timeout

(S113)


19-19: Probable use of requests call without timeout

(S113)

tests/conftest.py

75-75: Probable use of requests call without timeout

(S113)


80-80: Avoid specifying long messages outside the exception class

(TRY003)


123-123: Probable use of requests call without timeout

(S113)


131-131: Probable use of requests call without timeout

(S113)


137-139: Avoid specifying long messages outside the exception class

(TRY003)


142-142: Probable use of requests call without timeout

(S113)


416-416: Probable use of requests call without timeout

(S113)


426-426: Probable use of requests call without timeout

(S113)

tests/test_zndraw_admin.py

161-161: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

tests/test_vis_room.py

20-20: Probable use of requests call without timeout

(S113)


32-32: Probable use of requests call without timeout

(S113)


64-64: Probable use of requests call without timeout

(S113)


79-79: Probable use of requests call without timeout

(S113)


87-87: Probable use of requests call without timeout

(S113)


99-99: Probable use of requests call without timeout

(S113)


118-118: Probable use of requests call without timeout

(S113)


132-132: Probable use of requests call without timeout

(S113)

tests/test_requires_lock_decorator.py

11-11: Probable use of requests call without timeout

(S113)


19-19: Probable use of requests call without timeout

(S113)

src/zndraw/api_manager.py

79-81: Avoid specifying long messages outside the exception class

(TRY003)


233-233: Probable use of requests call without timeout

(S113)


236-238: Avoid specifying long messages outside the exception class

(TRY003)


266-266: Probable use of requests call without timeout

(S113)


276-276: Probable use of requests call without timeout

(S113)

tests/test_lock_socket_events.py

13-13: Probable use of requests call without timeout

(S113)


21-21: Probable use of requests call without timeout

(S113)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pytest (3.13, ubuntu-latest)
  • GitHub Check: pytest (3.12, ubuntu-latest)
  • GitHub Check: pytest (3.11, ubuntu-latest)
🔇 Additional comments (64)
src/zndraw/services/admin_service.py (1)

81-96: No action needed—code correctly prevents username enumeration.

The is_admin_username method is used internally for flow control only (line 215 in utility_routes.py), and both the admin-wrong-password path (line 216) and regular-user-wrong-password path (line 220) return the identical AUTH_FAILED_MSG response. The unified error message goal is already maintained. Timing attacks on usernames are not a practical security concern since usernames are non-secret.

src/zndraw/utils/__init__.py (1)

1-5: LGTM! Clear module documentation.

The module-level docstring provides a concise overview of the utility functions available. This aligns well with the new time utilities introduced in this PR.

tests/test_admin_auth.py (3)

35-48: LGTM! Proper registration flow implemented.

The test now correctly follows the register-then-login pattern introduced in this PR. The username format change to "john-doe" (lowercase with hyphen) aligns with the new validation rules (1-64 chars, alphanumeric with underscore/hyphen).


72-80: LGTM! Security improvement through unified error messaging.

The updated error message "Authentication failed" instead of "Invalid username or password" is a security best practice that prevents username enumeration attacks.


168-186: LGTM! Comprehensive test for anonymous guest flow.

The test effectively validates:

  • Anonymous guest registration (no username provided)
  • Auto-generated username format (starts with "user-")
  • Subsequent login with the generated username
  • Admin role assignment in local mode

This thoroughly exercises the new registration behavior.

src/zndraw/app/extension_routes.py (1)

17-17: LGTM! Good refactoring for consistency.

Migrating to the centralized utc_now_timestamp() utility improves maintainability and ensures consistent UTC time handling across the codebase. The functionality is equivalent to the previous datetime.utcnow().timestamp() implementation.

Also applies to: 149-149

tests/test_partial_frame_update.py (1)

464-469: LGTM! Aligns with the new create-then-join pattern.

The explicit room creation via POST /api/rooms before joining follows the authentication refactoring's new flow and matches the pattern adopted across the test suite.

tests/test_room_lock_enforcement.py (1)

33-36: LGTM! Follows the new register-then-login pattern.

The preliminary user registration step via POST /api/user/register correctly implements the authentication flow introduced by this PR, ensuring the user exists before attempting login.

src/zndraw/app/job_dispatcher.py (1)

8-9: LGTM! Centralized UTC timestamp generation.

The migration from datetime.utcnow().timestamp() to utc_now_timestamp() utility aligns with the PR's effort to standardize UTC time handling across the codebase.

Also applies to: 199-199

src/zndraw/app/lock_routes.py (1)

10-10: LGTM! Standardized ISO timestamp generation.

The replacement of datetime.datetime.utcnow().isoformat() with utc_now_iso() utility in both lock acquisition and refresh paths ensures consistent UTC time handling for lock metadata.

Also applies to: 105-105, 170-170

tests/test_chat.py (1)

11-22: LGTM! Adopts the new create-then-join flow.

The test correctly obtains JWT headers upfront and explicitly creates the room via POST /api/rooms before joining, aligning with the authentication refactoring's new pattern. Header reuse across both requests is efficient and appropriate.

src/zndraw/app/job_manager.py (1)

12-12: LGTM! Consistent use of centralized UTC time utilities.

The refactoring to use utc_now_iso() and utc_now_timestamp() from the centralized time utilities module improves consistency and maintainability across the codebase.

Also applies to: 168-168, 201-201, 313-313, 352-352, 406-406

src/zndraw/app/events.py (1)

10-10: LGTM! Improved separation of concerns with user service.

The refactoring to use user_service.username_exists() for user validation provides better separation of concerns and encapsulation. The use of utc_now_iso() for activity timestamps is consistent with the broader PR changes.

Also applies to: 113-126

src/zndraw/app/worker_routes.py (1)

18-18: LGTM! Consistent timestamp handling for worker registration.

The refactoring to use utc_now_timestamp() for recording worker registration times is consistent with the broader UTC time utility migration across the codebase.

Also applies to: 206-206, 256-256

src/zndraw/auth.py (2)

3-3: LGTM! Well-structured JWT token implementation.

The JWT token creation follows RFC 7519 best practices with standard claims (sub, iat, exp, jti) and uses the centralized utc_now() utility for consistent UTC timestamp handling. The 7-day expiration is reasonable for this authentication use case.

Also applies to: 12-12, 16-17, 54-86


156-177: LGTM! Clear documentation of JWT role vs. authorization.

The updated docstring correctly clarifies that JWT roles are informational and that AdminService.is_admin() should be used for authorization decisions. This helps prevent confusion between authentication (JWT) and authorization (Redis).

tests/conftest.py (1)

414-431: Fixture correctly pre-creates room before joining.

The joined_room fixture now aligns with the new API contract. The assertion on status codes (200, 201, 409) properly handles both new room creation and re-runs where the room may already exist.

src/zndraw/utils/time.py (2)

22-41: LGTM!

Clean utility functions that properly derive from utc_now() as the single source of truth. The docstrings follow NumPy style as required.


9-19: The project requires Python 3.11+ (requires-python = ">=3.11" in pyproject.toml), so using datetime.UTC is correct and appropriate. No change needed.

Likely an incorrect or invalid review comment.

tests/test_step_endpoints.py (2)

26-48: LGTM!

The fixture correctly uses the helper to set up a room with a step lock for testing.


380-436: LGTM!

Good integration test verifying socket events are emitted on step updates. The setup correctly creates and joins the room before testing socket communication.

tests/test_lock_socket_events.py (2)

37-52: LGTM!

The fixture is well-structured, returning all necessary values for socket event testing. The tuple now includes user_name which is correctly unpacked in all test functions.


315-363: LGTM!

Good test for verifying multi-client lock event broadcasting. User 1 creates and joins, user 2 joins existing room and listens for events - correctly tests the cross-client notification flow.

tests/test_requires_lock_decorator.py (2)

26-48: LGTM!

The room_with_lock fixture correctly uses the new helper for room setup before acquiring the lock.


298-336: LGTM!

Tests for missing/invalid JWT now properly create rooms first, ensuring the test isolates JWT validation from room existence errors. The updated fixture signatures correctly add get_jwt_auth_headers.

tests/test_user_service.py (2)

495-528: LGTM!

Excellent test coverage for Argon2 hashing. Verifies that:

  1. Same password produces different hashes (random salt)
  2. Password verification works correctly
  3. Hashes have the correct $argon2 prefix

580-640: LGTM!

Comprehensive tests for the new ensure_user_exists method:

  • Creates new user when missing
  • Returns False when user already exists (idempotent)
  • Preserves existing user data including password hash

This aligns well with the PR's goal of idempotent user creation.

src/zndraw/app/room_routes.py (1)

256-335: LGTM! Join endpoint properly enforces room and user existence.

Key improvements:

  • Returns 404 if room doesn't exist (line 302) - enforces explicit room creation
  • Returns 401 if user doesn't exist (line 306) - enforces registration-first flow
  • Uses utc_now_iso() for timestamps (line 314)
  • Clean response without frameCount or created flags

The error message "User not found. Please register first." provides clear guidance.

tests/test_lazy_room_loading.py (3)

19-34: LGTM! Correctly implements the create-then-join pattern.

The test properly separates room creation (POST /api/rooms) from joining (POST /api/rooms/{id}/join), which aligns with the new API semantics where room creation and joining are distinct operations.


106-120: Good test structure for ZnDraw auto-creation scenario.

The comment correctly notes that ZnDraw auto-creates the room, so a different user joining an existing room doesn't need explicit creation. The assertions for the minimal join response (status, roomId) are appropriate.


167-176: LGTM! Auth-required test correctly pre-creates the room.

The test now properly creates the room before testing auth requirements on lazy loading endpoints, ensuring the 401 responses are genuinely about missing authentication rather than missing rooms.

tests/test_auth_integration.py (4)

37-79: LGTM! Guest registration tests cover both empty and whitespace username scenarios.

The tests properly verify that:

  1. Missing username generates a user-* prefixed guest name
  2. Whitespace-only username is treated the same as missing
  3. Login works with the server-generated username

Good coverage for edge cases.


244-268: Good test for verifying Redis user creation on register.

This test correctly validates that /api/user/register creates the Redis entry, which is the new canonical place for user creation. The test properly checks both non-existence before registration and existence after.


271-285: LGTM! Login without registration test validates security.

The test confirms that login fails with a generic "Authentication failed" message when the user hasn't registered, which prevents username enumeration attacks as documented in the PR objectives.


322-341: LGTM! Tests correctly validate room not found behavior.

The 404 response for joining a non-existent room properly separates room creation from room joining, matching the new API semantics.

src/zndraw/api_manager.py (3)

65-101: LGTM! Register-then-login flow correctly handles guest users.

The implementation properly:

  1. Registers users first via /api/user/register
  2. Handles 409 (user exists) as acceptable
  3. Falls back to provided username when registration returns 409
  4. Performs login with the resolved username

Minor observation: The and register_response.status_code != 401 check on line 77 is technically redundant since 401 isn't in (200, 201, 409), but it doesn't cause issues.


198-240: LGTM! create_room method properly encapsulates room creation.

The method correctly:

  1. Builds the payload with roomId, optional description, and copyFrom
  2. Uses JWT authorization when available
  3. Returns the response for 200/201 status codes
  4. Raises RuntimeError with context on failure

242-288: LGTM! join_room implements create-on-demand pattern correctly.

The method properly:

  1. Attempts to join existing room first
  2. Creates room on 404 using the provided description/copy_from
  3. Retries joining after creation
  4. Stores session ID from response

This aligns with the PR's separation of room creation and joining.

tests/test_vis_room.py (4)

7-48: LGTM! Test correctly implements create-then-join pattern.

The test properly:

  1. Creates room via POST /api/rooms (expects 201)
  2. Joins via POST /api/rooms/{roomId}/join (expects 200)
  3. Validates response structure for both operations
  4. Verifies room appears in room list afterward

Note: Consider adding timeout=10 to requests calls for consistency (as flagged in other test files).


61-70: LGTM! Non-existent room test validates 404 behavior.

Test correctly verifies that joining a non-existent room returns 404 with an error message containing the room name.


73-106: LGTM! Test validates multiple users can join existing room.

Good coverage of the scenario where:

  1. First user creates and joins the room
  2. Second user (different auth) successfully joins the same room

108-140: LGTM! Copy-from test validates room duplication.

Test correctly verifies:

  1. Source room is created with frames via ZnDraw
  2. New room copies frames from source (frameCount == 3)
  3. New room without copyFrom starts empty (frameCount == 0)
src/zndraw/app/utility_routes.py (4)

31-32: Good security practice: Generic authentication error message.

Using a constant AUTH_FAILED_MSG prevents username enumeration attacks by returning the same error whether the username doesn't exist or the password is wrong.


155-201: LGTM! Login endpoint properly enforces registration requirement.

The refactored login:

  1. Requires userName (no longer generates guests here)
  2. Validates username format before proceeding
  3. Delegates to appropriate handler based on password presence
  4. Uses consistent error messages to prevent enumeration

204-261: LGTM! Helper functions cleanly separate authentication paths.

The helpers provide good separation of concerns:

  • _handle_password_login: Admin and registered user authentication
  • _handle_guest_login: Guest authentication with registered user check
  • _determine_role: Centralized role determination logic
  • _issue_token: Single point for token generation response

264-327: LGTM! Register endpoint is the canonical user creation point.

The endpoint correctly:

  1. Generates username for anonymous guests (user-{hex})
  2. Validates username format
  3. Returns 409 for duplicate usernames
  4. Handles both guest (no password) and registered (with password) creation
  5. Returns 201 on successful creation

Minor: The static analysis suggestion about moving the return to an else block (TRY300) is a stylistic preference and doesn't affect correctness.

app/src/hooks/useRestManager.ts (3)

4-7: LGTM! Imports updated to reflect new API surface.

  • throttle import narrowed (no longer importing set)
  • joinOrCreateRoom replaces direct joinRoom usage
  • getUserRole from auth utilities for JWT-based role retrieval

66-70: LGTM! User role sourced from JWT claims.

Using getUserRole() as the single source of truth for user role aligns with the PR objective of treating JWT as authoritative for identity. The conditional setUserRole only updates when a role is present.


88-96: LGTM! joinOrCreateRoom handles on-demand room creation.

The call correctly passes:

  • template from URL query params
  • copyFrom: template - uses template as source for copying when creating a new room

This aligns with the client.ts implementation where copyFrom ?? template is used.

app/src/myapi/client.ts (3)

424-443: LGTM! createRoom API function properly defined.

The function:

  • Takes a typed request with required roomId and optional description/copyFrom
  • Supports AbortSignal for cancellation
  • Returns typed response with status, roomId, frameCount, and created flag

467-495: LGTM! joinOrCreateRoom implements the recommended join-then-create pattern.

The implementation correctly:

  1. Attempts to join existing room first (optimistic path)
  2. On 404, creates the room with provided options
  3. Uses copyFrom ?? template to support template-based room creation
  4. Joins the newly created room
  5. Re-throws non-404 errors

This is the recommended frontend entry point for room joining as noted in the JSDoc.


445-454: LGTM! JoinRoomRequest/Response types simplified.

The types correctly reflect the new API:

  • JoinRoomRequest only contains optional template
  • JoinRoomResponse no longer includes created (room creation is now explicit)
src/zndraw/services/user_service.py (7)

7-31: LGTM! Good security choice with Argon2id.

Proper import organization and using PasswordHasher() with secure defaults is the recommended approach.


33-53: LGTM!

Password validation is straightforward and the descriptive error message is appropriate for validation errors.


55-97: LGTM!

Username validation is comprehensive. The explicit length check before regex provides a clearer error message for users who exceed the limit.


369-401: LGTM!

The Argon2 verification pattern is correct. The VerifyMismatchError exception handling clearly distinguishes between "wrong password" (return False) and other issues (propagate exception).


269-367: LGTM!

The register_user method correctly handles both same-username registration and username change scenarios, with proper admin status transfer and atomic operations via pipeline.


402-479: LGTM!

Both change_password and reset_password validate the new password upfront and use Argon2 consistently.


520-563: LGTM!

Good defensive handling of stale index entries and proper bytes decoding from Redis.

app/src/utils/auth.ts (5)

1-45: LGTM!

Types and interfaces are well-defined and consistent with the backend's UserRole enum and JWT structure.


56-137: LGTM!

The register-then-login flow for guests correctly handles the 409 (already exists) response as a valid path to proceed with login. The defensive check for "409 without username" is a good safeguard against unexpected server behavior.


139-243: LGTM!

Good implementation of JWT-centric identity. The deprecation notice on getUsername() guides consumers to the clearer getUsernameFromToken(), and token validation correctly handles expiry by comparing milliseconds.


245-288: LGTM!

The upgrade flow correctly requires prior authentication and updates the stored token to reflect the new role and username from the server's response.


290-318: LGTM!

Standard authenticated request pattern with consistent error handling.

Comment thread pyproject.toml Outdated
Comment thread tests/test_auth.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the authentication system to improve security and clarify the separation between user registration and room management. Key improvements include switching from SHA-256 to Argon2 password hashing, adding JWT token expiration (7 days), and enforcing username validation.

  • Introduces register-then-login flow where /api/user/register is the single point for user creation
  • Separates room creation (POST /api/rooms) from joining (POST /api/rooms/{id}/join)
  • Replaces SHA-256 password hashing with Argon2id for improved security
  • Adds password requirements (minimum 8 characters) and username validation (alphanumeric with underscore/hyphen)
  • Implements JWT token expiration (7 days) with proper iat and exp claims

Reviewed changes

Copilot reviewed 32 out of 33 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uv.lock Adds argon2-cffi dependency and updates Python version markers
pyproject.toml Adds argon2-cffi>=23.1.0 to dependencies
src/zndraw/services/user_service.py Replaces SHA-256 with Argon2, adds password/username validation, removes separate salt storage
src/zndraw/services/admin_service.py Adds is_admin_username() helper for wrong password detection
src/zndraw/auth.py Adds JWT expiration (7 days) with iat and exp claims
src/zndraw/app/utility_routes.py Splits registration into /api/user/register (create) and /api/user/upgrade (guest→registered), updates login to never create users
src/zndraw/app/room_routes.py Adds explicit POST /api/rooms endpoint, removes implicit room creation from join endpoint
src/zndraw/utils/time.py New utility module with utc_now() helpers to replace deprecated datetime.utcnow()
src/zndraw/api_manager.py Updates Python client to use register-then-login flow and create_room/join_room separation
app/src/utils/auth.ts Updates frontend auth to use register-then-login flow, removes deprecated localStorage fields
app/src/myapi/client.ts Adds createRoom() and joinOrCreateRoom() helper functions
tests/* Updates all tests for new authentication and room creation flows

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/zndraw/utils/time.py Outdated
Comment thread tests/test_auth.py Outdated
Comment thread src/zndraw/auth.py
Comment thread src/zndraw/services/user_service.py
- Upgrade argon2-cffi from >=23.1.0 to >=25.1.0 (current stable)
- Use datetime.timezone.utc instead of datetime.UTC for Python 3.11.x compatibility
- Replace deprecated datetime.utcnow() with utc_now() utility in tests
- Add clarifying comment for username regex pattern (1 + 0-63 = 1-64 chars)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 83.46304% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.92%. Comparing base (8782043) to head (6aa5269).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/zndraw/app/utility_routes.py 60.49% 32 Missing ⚠️
src/zndraw/app/room_routes.py 12.50% 28 Missing ⚠️
src/zndraw/app/events.py 16.66% 5 Missing ⚠️
src/zndraw/app/job_manager.py 16.66% 5 Missing ⚠️
src/zndraw/api_manager.py 90.62% 3 Missing ⚠️
src/zndraw/services/user_service.py 94.00% 3 Missing ⚠️
src/zndraw/app/lock_routes.py 33.33% 2 Missing ⚠️
src/zndraw/app/worker_routes.py 33.33% 2 Missing ⚠️
tests/conftest.py 81.81% 2 Missing ⚠️
src/zndraw/app/extension_routes.py 50.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #831      +/-   ##
==========================================
+ Coverage   78.69%   78.92%   +0.23%     
==========================================
  Files         153      154       +1     
  Lines       18579    18818     +239     
==========================================
+ Hits        14620    14852     +232     
- Misses       3959     3966       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

PythonFZ and others added 3 commits January 7, 2026 22:05
- Sanitize worker_id to valid username by replacing ':' with '-'
  (celery:{uuid} format violates username pattern)
- Remove unused 'public' parameter from _get_extension_class (YAGNI)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
src/zndraw/job_executor.py (1)

106-109: Consider more robust worker ID sanitization with validation.

The current sanitization only replaces colons with dashes. Given the PR introduces strict username validation (1–64 chars, alphanumeric with underscore/hyphen), consider:

  1. Validating the sanitized username meets all requirements
  2. Handling edge cases (e.g., very long task IDs, other special characters)
  3. Adding a fallback or error if sanitization produces an invalid username
♻️ Proposed enhancement with validation
+import re
+
 # Step 2: Create ZnDraw instance in target room
 # Worker IDs use format "celery:{task_id}" for tracking purposes.
 # Usernames don't allow colons (Redis key delimiter), so sanitize here.
-user_name = worker_id.replace(":", "-")
+user_name = worker_id.replace(":", "-")
+# Ensure sanitized username meets validation rules (1-64 chars, alphanumeric + _ -)
+if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", user_name):
+    # Fallback: hash the worker_id to create a valid username
+    import hashlib
+    user_name = f"worker-{hashlib.sha256(worker_id.encode()).hexdigest()[:16]}"
 vis = ZnDraw(room=room, url=server_url, user=user_name)
src/zndraw/app/utility_routes.py (5)

286-286: Move import to top of file.

As per coding guidelines, imports should always be at the top of the file. Consider moving PasswordValidationError import to the module level with other imports from zndraw.services.user_service.

♻️ Suggested refactor

At the top of the file (around line 22-25), update the import:

 from zndraw.services.user_service import (
+    PasswordValidationError,
     UsernameValidationError,
     validate_username,
 )

Then remove line 286.


324-326: Minor: Redundant exception object in log.exception call.

log.exception() automatically captures the exception context from the current exception handler. Passing e explicitly is redundant.

♻️ Suggested fix
     except Exception as e:
-        log.exception(f"Error registering user: {e}")
+        log.exception("Error registering user")
         return {"error": "Registration failed", "type": "ServerError"}, 500

353-353: Move import to top of file.

Same as the register endpoint - this import should be at the module level. After moving both imports, this line can be removed.


390-392: Minor: Redundant exception object in log.exception call.

Same pattern as register endpoint - log.exception() already captures exception context.

♻️ Suggested fix
     except Exception as e:
-        log.exception(f"Error upgrading user: {e}")
+        log.exception("Error upgrading user")
         return {"error": "Upgrade failed", "type": "ServerError"}, 500

246-255: Consider simplifying _determine_role logic.

The function checks user_service.username_exists() after callers have already verified user existence. This is defensive but slightly redundant. The fallback to "guest" at line 255 may never be reached given the caller's preconditions.

src/zndraw/api_manager.py (1)

237-242: Consider distinguishing room-already-exists error.

Currently all non-2xx responses raise a generic RuntimeError. For better UX, consider checking for 409 (Conflict) to provide a clearer message that the room already exists.

♻️ Optional improvement
         if response.status_code not in (200, 201):
+            if response.status_code == 409:
+                raise ValueError(f"Room '{self.room}' already exists")
             raise RuntimeError(
                 f"Failed to create room '{self.room}': {response.status_code} {response.text}"
             )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99633ba and 0042883.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • pyproject.toml
  • src/zndraw/api_manager.py
  • src/zndraw/app/utility_routes.py
  • src/zndraw/job_executor.py
  • src/zndraw/services/admin_service.py
  • src/zndraw/services/user_service.py
  • src/zndraw/utils/time.py
  • tests/test_auth.py
  • tests/test_zndraw_admin.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: If sensible, implement collections.abc interfaces for classes, such as MutableMapping or MutableSequence
Use numpy style docstrings
Docstrings must be concise and to the point
Use type hints wherever possible. Import typing as t if necessary, but use list[int|float] | None instead of t.Optional[t.List[int|float]]
Imports should always be at the top of the file

Files:

  • tests/test_zndraw_admin.py
  • src/zndraw/services/admin_service.py
  • src/zndraw/utils/time.py
  • tests/test_auth.py
  • src/zndraw/services/user_service.py
  • src/zndraw/app/utility_routes.py
  • src/zndraw/api_manager.py
  • src/zndraw/job_executor.py
**/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/test_*.py: Use pytest.mark.parametrize to avoid code duplication in tests
Tests should be very specific and test only one thing
Avoid complex test setups
Each test must be a function, not a method of a class

Files:

  • tests/test_zndraw_admin.py
  • tests/test_auth.py
🧬 Code graph analysis (3)
tests/test_auth.py (3)
tests/conftest.py (1)
  • app (179-189)
src/zndraw/auth.py (3)
  • create_jwt_token (54-86)
  • decode_jwt_token (89-116)
  • AuthError (20-34)
src/zndraw/utils/time.py (1)
  • utc_now (9-19)
src/zndraw/services/user_service.py (2)
src/zndraw/app/redis_keys.py (6)
  • GlobalIndexKeys (658-701)
  • UserKeys (518-564)
  • hash_key (534-536)
  • hash_key (642-644)
  • users_index (682-690)
  • visited_rooms (542-544)
src/zndraw/utils/time.py (1)
  • utc_now_iso (22-30)
src/zndraw/api_manager.py (2)
src/zndraw/app/room_routes.py (2)
  • create_room (137-207)
  • join_room (257-335)
src/zndraw/services/room_service.py (1)
  • create_room (70-112)
🪛 Ruff (0.14.10)
src/zndraw/services/user_service.py

50-52: Avoid specifying long messages outside the exception class

(TRY003)


86-86: Avoid specifying long messages outside the exception class

(TRY003)


89-91: Avoid specifying long messages outside the exception class

(TRY003)


94-97: Avoid specifying long messages outside the exception class

(TRY003)


399-399: Consider moving this statement to an else block

(TRY300)


433-433: Avoid specifying long messages outside the exception class

(TRY003)


436-436: Avoid specifying long messages outside the exception class

(TRY003)


472-472: Avoid specifying long messages outside the exception class

(TRY003)

src/zndraw/app/utility_routes.py

318-318: Consider moving this statement to an else block

(TRY300)


325-325: Redundant exception object included in logging.exception call

(TRY401)


391-391: Redundant exception object included in logging.exception call

(TRY401)

src/zndraw/api_manager.py

79-81: Avoid specifying long messages outside the exception class

(TRY003)


238-240: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pytest (3.12, ubuntu-latest)
  • GitHub Check: pytest (3.11, ubuntu-latest)
  • GitHub Check: pytest (3.13, ubuntu-latest)
🔇 Additional comments (18)
src/zndraw/job_executor.py (1)

207-207: LGTM: Function signature simplification.

Removing the unused public parameter streamlines the extension retrieval API. The change is consistent with the broader refactoring toward registry-driven extension management.

tests/test_zndraw_admin.py (1)

161-161: LGTM: Security improvement with unified error message.

Changing to "Authentication failed" prevents username enumeration attacks by not revealing whether the username or password was incorrect. This aligns with the PR's security improvements.

tests/test_auth.py (1)

106-151: LGTM: Well-structured JWT expiration tests.

Both new tests are focused and verify specific JWT expiration behavior:

  • test_jwt_token_has_expiration: Validates the presence and correctness of exp/iat claims
  • test_expired_jwt_token_raises_error: Confirms expired tokens are rejected

The tests correctly use the utc_now() utility instead of deprecated datetime.utcnow(), addressing previous review feedback.

src/zndraw/utils/time.py (1)

9-41: LGTM: Well-implemented UTC time utilities.

The implementation correctly:

  • Uses datetime.timezone.utc for better Python 3.11.x compatibility
  • Establishes utc_now() as the single source of truth for other helpers
  • Follows numpy-style docstrings as per coding guidelines
  • Includes appropriate type hints
  • Replaces deprecated datetime.utcnow() usage throughout the codebase
src/zndraw/services/admin_service.py (2)

80-89: LGTM: Excellent security improvement with constant-time comparison.

Using secrets.compare_digest prevents timing attacks that could leak information about credential validity. The null-safe handling with (self._admin_username or "") ensures the comparison works correctly even when admin credentials aren't configured.


91-106: LGTM: Useful helper for admin username detection.

The is_admin_username method provides a clean way to distinguish between wrong passwords for the admin user versus non-existent users, supporting better error handling and logging without revealing sensitive information.

src/zndraw/app/utility_routes.py (2)

31-32: Good security practice: Generic auth error message.

Using a consistent AUTH_FAILED_MSG prevents username enumeration attacks by not revealing whether a username exists or the password was wrong.


155-201: Login flow correctly enforces register-before-login pattern.

The refactored login properly validates username format, delegates to appropriate handlers based on password presence, and uses generic error messages to prevent enumeration.

src/zndraw/api_manager.py (3)

65-101: Register-then-login flow implemented correctly.

The guest registration flow properly handles:

  • 200/201 for new registration
  • 409 for existing user (treated as success, proceeds to login)
  • Server-generated usernames extracted from response

One minor observation: 401 is checked but not explicitly handled (falls through to the if that raises RuntimeError).


244-296: Join-or-create pattern implemented correctly.

The method properly attempts to join first, and creates the room on-demand if it doesn't exist. The sessionId is correctly stored for subsequent requests.


318-318: Good: Timeouts added consistently across HTTP requests.

Adding explicit timeouts to all HTTP requests improves reliability by preventing indefinite hangs when the server is slow or unresponsive. The timeout values are appropriately tiered (5-10s for quick operations, 30s for data-heavy operations).

Also applies to: 357-357, 698-698, 747-748, 767-768, 784-784, 803-804, 824-824, 846-846, 855-855, 871-871, 883-883, 898-898, 918-921, 940-942, 964-966, 988-988, 1013-1013, 1039-1039, 1067-1067, 1088-1088, 1111-1111, 1129-1129, 1158-1160, 1190-1190, 1214-1214, 1259-1259, 1293-1293, 1320-1320, 1347-1347, 1618-1618, 1652-1653, 1672-1672

src/zndraw/services/user_service.py (7)

20-21: Argon2 PasswordHasher with secure defaults is appropriate.

Using argon2-cffi's PasswordHasher() with default parameters provides secure, well-tuned settings for password hashing (Argon2id algorithm).


33-52: Password validation is appropriately minimal.

The 8-character minimum is a reasonable baseline. Additional complexity requirements (uppercase, numbers, symbols) are often counterproductive for security as they encourage predictable patterns. The current approach aligns with modern guidance from NIST SP 800-63B.


270-368: User registration logic handles both upgrade paths correctly.

The method properly handles:

  1. Same username (adding password to existing guest)
  2. Different username (migrating data to new username, deleting old)

Admin status is correctly transferred, and the pipeline ensures atomic execution of the multi-key operations.


370-401: Argon2 password verification implemented correctly.

The verification properly uses _ph.verify() and handles the VerifyMismatchError exception. The byte handling for Redis values is also correct.


493-520: User deletion is thorough.

The delete_user method properly cleans up all user-related Redis keys including the visited rooms set, and removes the user from both the users and admins indices.


522-565: list_all_users includes self-healing for stale index entries.

Good defensive programming: if a username exists in the index but the user data is missing, the stale entry is cleaned up (lines 543-545).


233-268: TOCTOU race condition in ensure_user_exists, but impact is acceptable for guest users.

A race exists between the username_exists() check (line 249) and pipeline execution (line 265). Two concurrent registrations of the same username could both pass the existence check and execute the creation pipeline.

The impact is benign: HSET and SADD are both idempotent, so concurrent writes result in consistent data. For guest users, this is acceptable.

If atomic guarantees become necessary, consider using Redis WATCH to wrap the check-then-create pattern, or use a Lua script.

- Move PasswordValidationError import to module level
- Remove redundant exception object from log.exception calls
- Add specific error message for 409 Conflict in room creation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/zndraw/api_manager.py:
- Around line 65-95: The registration handling currently treats 401 as
acceptable and can leave login_user_name as None; update the guest-registration
block so that the success codes are explicitly only 200/201 and 409 (remove the
"and != 401" acceptance) and treat 401 as an error (raise RuntimeError with
register_response.text); after parsing reg_data = register_response.json(), if
reg_data.get("userName") is missing and the incoming user_name is None, raise a
clear RuntimeError indicating "userName required from registration response" (or
alternatively generate a safe username) before calling the login request so that
login_payload["userName"] is never None; refer to register_response.status_code,
reg_data.get("userName", user_name), login_user_name and the subsequent
requests.post to locate the code to modify.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0042883 and 6aa5269.

📒 Files selected for processing (2)
  • src/zndraw/api_manager.py
  • src/zndraw/app/utility_routes.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: If sensible, implement collections.abc interfaces for classes, such as MutableMapping or MutableSequence
Use numpy style docstrings
Docstrings must be concise and to the point
Use type hints wherever possible. Import typing as t if necessary, but use list[int|float] | None instead of t.Optional[t.List[int|float]]
Imports should always be at the top of the file

Files:

  • src/zndraw/api_manager.py
  • src/zndraw/app/utility_routes.py
🧬 Code graph analysis (1)
src/zndraw/api_manager.py (2)
src/zndraw/app/room_routes.py (1)
  • create_room (137-207)
src/zndraw/services/room_service.py (1)
  • create_room (70-112)
🪛 Ruff (0.14.10)
src/zndraw/api_manager.py

79-81: Avoid specifying long messages outside the exception class

(TRY003)


238-238: Avoid specifying long messages outside the exception class

(TRY003)


240-242: Avoid specifying long messages outside the exception class

(TRY003)

src/zndraw/app/utility_routes.py

317-317: Consider moving this statement to an else block

(TRY300)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pytest (3.13, ubuntu-latest)
  • GitHub Check: pytest (3.11, ubuntu-latest)
  • GitHub Check: pytest (3.12, ubuntu-latest)
🔇 Additional comments (7)
src/zndraw/api_manager.py (3)

198-244: LGTM: Room creation method is well-implemented.

The new create_room() method properly:

  • Constructs payload with required roomId and optional parameters
  • Includes JWT authorization when available
  • Adds timeout for resilience
  • Provides specific error handling for 409 (room already exists)
  • Returns parsed room data

72-72: Excellent addition of timeouts throughout for resilience.

The systematic addition of explicit timeouts to all HTTP requests is a significant reliability improvement:

  • Prevents indefinite hangs on network issues or unresponsive servers
  • Timeout values are appropriate for operation types (5-10s for quick operations, 30s for data-intensive operations)
  • Consistent pattern applied across all request types

Also applies to: 94-94, 100-100, 194-194, 234-234, 274-274, 287-287, 320-320, 359-359, 404-404, 461-461, 509-509, 550-550, 596-596, 662-662, 680-680, 700-700, 749-749, 769-769, 786-786, 805-805, 826-826, 848-848, 857-857, 873-873, 885-885, 900-900, 923-923, 944-944, 968-968, 990-990, 1015-1015, 1041-1041, 1069-1069, 1090-1090, 1113-1113, 1131-1131, 1162-1162, 1192-1192, 1216-1216, 1261-1261, 1295-1295, 1322-1322, 1349-1349, 1620-1620, 1655-1655, 1674-1674


246-298: No changes needed. The join_room() method implements an intentional client-side convenience pattern: if the room doesn't exist (404), it automatically creates it before joining. This behavior is explicitly documented in the docstring, the caller in zndraw.py intentionally passes the creation parameters, and it aligns with standard "get-or-create" API patterns. The underlying REST endpoint remains pure join-only, and the wrapper provides appropriate abstraction for the client use case.

src/zndraw/app/utility_routes.py (4)

22-33: Good security practice: Generic authentication error message prevents username enumeration.

The introduction of AUTH_FAILED_MSG with the comment explaining its purpose demonstrates proper security awareness. This prevents attackers from determining which usernames exist in the system by observing different error messages for "user not found" vs "wrong password".

The centralized validation imports from user_service also promote consistency across the codebase.


156-263: Excellent refactoring: Login logic is well-organized with clear separation of concerns.

The refactored login flow demonstrates several best practices:

  1. Clear authentication paths: Separate handlers for password-based (_handle_password_login) and guest (_handle_guest_login) authentication
  2. Security-first validation: Username validation occurs early, and the generic AUTH_FAILED_MSG is used consistently to prevent enumeration
  3. Logical flow: Admin credentials checked first, then regular users, with proper fallbacks
  4. Single responsibility: Each helper function has a clear, focused purpose
  5. Documentation: Docstrings clearly state that login "NEVER creates users" - emphasizing that registration must occur first

The separation of role determination (_determine_role) and token issuance (_issue_token) into dedicated functions improves maintainability.


265-326: Registration endpoint is well-implemented with clear semantics.

The /api/user/register endpoint properly centralizes user creation with:

  • Clear documentation stating this is the "ONLY endpoint that creates users"
  • Random username generation for anonymous guests
  • Username validation and duplicate detection (409 response)
  • Support for both guest (no password) and registered (with password) user creation

The error handling at lines 319-325 uses a broad except Exception: block, which catches all exceptions including unexpected programming errors. While the logging and 500 response are appropriate for an API endpoint, consider whether specific exception types (beyond PasswordValidationError and ValueError) should be handled distinctly or allowed to propagate for debugging.


328-390: LGTM: Upgrade endpoint enables smooth guest-to-registered-user transition.

The /api/user/upgrade endpoint provides a clean user experience for guests who want to create permanent accounts:

  • Requires authentication (existing guest JWT)
  • Validates required fields before processing
  • Supports username changes during upgrade
  • Issues new JWT with updated username and role
  • Proper error handling with specific validation errors

The implementation correctly delegates to user_service.register_user() which handles the complexity of username changes and password hashing.

Comment thread src/zndraw/api_manager.py
Comment on lines +65 to +95
# For guests (no password), register first, then login
if password is None:
# Step 1: Register user (creates in backend)
register_payload = {"userName": user_name} if user_name else {}
register_response = requests.post(
f"{self.url}/api/user/register",
json=register_payload,
timeout=5.0,
)
# 409 = already exists, which is fine
if (
register_response.status_code not in (200, 201, 409)
and register_response.status_code != 401
):
raise RuntimeError(
f"Registration failed: {register_response.text}"
)

# Get the username (may be server-generated if not provided)
if register_response.status_code in (200, 201):
reg_data = register_response.json()
login_user_name = reg_data.get("userName", user_name)
else:
# 409 - user exists, use the provided username
login_user_name = user_name

# Step 2: Login (get JWT)
login_payload = {"userName": login_user_name}
response = requests.post(
f"{self.url}/api/login", json=login_payload, timeout=5.0
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify error handling for registration response and handle edge case where username is None.

The error handling logic at lines 75-81 is confusing:

  1. The condition != 401 seems to silently accept 401 responses without raising an error. If 401 means the user already exists or authentication failed during registration, this should be documented or handled explicitly.
  2. At line 86, if user_name is None (when not provided by caller) and the registration response doesn't include "userName", then login_user_name becomes None, which will cause the subsequent login request to fail with "userName required".
🔧 Suggested improvements
  1. Clarify the intent of accepting 401 responses:
-                    if (
-                        register_response.status_code not in (200, 201, 409)
-                        and register_response.status_code != 401
-                    ):
+                    # 200/201: user created, 409: user exists, 401: requires password (edge case)
+                    if register_response.status_code not in (200, 201, 409, 401):
                         raise RuntimeError(
                             f"Registration failed: {register_response.text}"
                         )
  1. Handle the edge case where username might be None:
                     if register_response.status_code in (200, 201):
                         reg_data = register_response.json()
                         login_user_name = reg_data.get("userName", user_name)
+                        if not login_user_name:
+                            raise RuntimeError("Server did not return a username")
                     else:
                         # 409 - user exists, use the provided username
                         login_user_name = user_name
+                        if not login_user_name:
+                            raise RuntimeError("Username is required for existing user login")
🧰 Tools
🪛 Ruff (0.14.10)

79-81: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In @src/zndraw/api_manager.py around lines 65 - 95, The registration handling
currently treats 401 as acceptable and can leave login_user_name as None; update
the guest-registration block so that the success codes are explicitly only
200/201 and 409 (remove the "and != 401" acceptance) and treat 401 as an error
(raise RuntimeError with register_response.text); after parsing reg_data =
register_response.json(), if reg_data.get("userName") is missing and the
incoming user_name is None, raise a clear RuntimeError indicating "userName
required from registration response" (or alternatively generate a safe username)
before calling the login request so that login_payload["userName"] is never
None; refer to register_response.status_code, reg_data.get("userName",
user_name), login_user_name and the subsequent requests.post to locate the code
to modify.

@PythonFZ PythonFZ merged commit 1165a1a into main Jan 8, 2026
10 of 12 checks passed
@PythonFZ PythonFZ deleted the feat/auth-refactoring branch January 8, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants