Skip to content

chat interface and improve authentication flow#50

Merged
sarahmc253 merged 19 commits into
mainfrom
web-client-fixes
Jun 2, 2026
Merged

chat interface and improve authentication flow#50
sarahmc253 merged 19 commits into
mainfrom
web-client-fixes

Conversation

@sreejita-saha
Copy link
Copy Markdown
Collaborator

@sreejita-saha sreejita-saha commented Jun 2, 2026

Revamp the chat interface to resemble popular messaging apps, streamline navigation, and enhance user experience. Implement password validation, TOFU key pinning, and improve message handling. Extend JWT expiry for better session management and introduce automatic message polling to keep the inbox updated. Adjust frontend styles for consistency and usability.

Summary by CodeRabbit

  • New Features

    • Redesigned chat UI with two-column conversations, message bubbles, and navbar updates
    • New auth flows: login, registration (password rules), unlock screen, and change-password modal
    • Client-side E2E key handling, TOFU pinning, fingerprint display, message send/receive, forward/revoke/delete/download, and polling for new messages
    • On-chain “Verify” view
  • Bug Fixes

    • Fixed Windows C++ console UTF-8 output
  • Documentation

    • Updated web and C++ client READMEs
  • Chores

    • Session token lifetime extended to 1 hour

- Updated index.html to include new CSS files for authentication, chat, and verification styles.
- Removed the compose button from the navbar and adjusted the inbox view to reflect new chat terminology.
- Refactored app.js to remove the compose view and streamline navigation.
- Enhanced the inbox view in views.js to support conversation grouping and improved message rendering.
- Updated message retrieval logic in routes.py to handle both sent and received messages in a single query.
- Introduced new CSS styles for authentication, chat, and verification views to improve UI consistency and aesthetics.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e1159cb-0b02-47ad-8b79-1c2250ecb193

📥 Commits

Reviewing files that changed from the base of the PR and between 01f301b and d7d9ee4.

📒 Files selected for processing (1)
  • client-web/js/views/inbox.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • client-web/js/views/inbox.js

📝 Walkthrough

Walkthrough

This PR reorganizes the client-web frontend from a monolithic views module into separate auth, inbox, and helper modules while persisting crypto key material to sessionStorage for page-reload resilience. The server extends JWT lifetime to 1 hour, enforces password complexity at registration, and refactors message endpoints with UNION ALL queries, revised authorization semantics, and new sender metadata for TOFU verification.

Changes

Web Client UI Refactoring and Session Management

Layer / File(s) Summary
Session and API state persistence
client-web/js/api.js
Wrapped private key, KEK salt, public key, and username are persisted to sessionStorage alongside in-memory state. Exported getters fall back to sessionStorage when in-memory values are absent; setWrappedKey and clearWrappedKey write/remove from both stores. New exports getUsername(), getStoredWrappedKey(), getStoredKekSalt() enable re-derivation of crypto material after page reload.
Authentication and key management views
client-web/js/views/auth.js, client-web/js/views/helpers.js
renderLogin submits credentials and navigates to inbox; renderRegister generates X25519 keypairs, wraps private keys with the user's password, and calls api.register; renderUnlock decrypts wrapped keys on reload. Helpers export HTML escaping, date formatting, byte decoding, key fingerprinting, TOFU pinning, password validation, and inline error UI.
Inbox UI with message polling, encryption, and actions
client-web/js/views/inbox.js
Two-column chat layout with conversation sidebar and active thread. Fetches messages via api.getMessages(), decrypts client-side using sender ephemeral/static keys and user private key, performs TOFU verification on sender public keys, polls every 10 seconds for new messages. Supports message send/forward/delete/revoke actions and a change-password modal that re-wraps private keys.
Router wiring, app layout, and CSS styling
client-web/js/app.js, client-web/index.html, client-web/css/base.css, client-web/css/auth.css, client-web/css/chat.css, client-web/css/verify.css
Router detects auth views, controls navbar visibility, and renders renderUnlock when private key is missing in-memory. HTML replaces single stylesheet with modular CSS imports (base/auth/chat/verify) and updates navbar brand/buttons and footer visibility. base.css updated palette and auth-mode layout; auth.css styles signup/signin forms; chat.css defines two-column chat with message bubbles and action icons; verify.css styles on-chain verification UI.

Server Authentication and Session Updates

Layer / File(s) Summary
JWT lifetime extension and password complexity validation
server/app/__init__.py, server/app/auth/routes.py
JWT access token expiry changed from 15 minutes to 1 hour. Registration route validates passwords for minimum 8 characters, uppercase letter, lowercase letter, digit, and special character; returns HTTP 400 with error listing unmet requirements.

Server Message Endpoints Refactored

Layer / File(s) Summary
GET /messages query and message authorization updates
server/app/messages/routes.py
GET /messages replaced with a UNION ALL of received and sent messages adding direction and is_revoked; includes sender username and X25519 public key for TOFU verification. DELETE /messages/<message_id> restricted to the sender and performs a hard delete (404 if missing, 403 if requester not sender). forward_message blocks forwarding revoked messages back to the original recipient and uses hyphenless UUIDs for forwarded IDs. revoke_message lookup simplified to id-only and returns Message not found when absent.

Documentation and Project Configuration

Layer / File(s) Summary
README and configuration updates
.gitignore, client-cpp/README.md, client-web/README.md
.gitignore now ignores client-cpp/whatsas_pins.txt. client-cpp/README.md sets PowerShell $OutputEncoding to UTF-8 before launching the client. client-web/README.md documents prerequisites, setup, project structure, feature list, cryptographic summary, and troubleshooting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • Aoibheannmangan
  • sarahmc253
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly relates to the main changes: introducing a chat interface and improving the authentication flow through password validation, TOFU pinning, and session management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web-client-fixes

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

Copy link
Copy Markdown
Contributor

@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: 7

Caution

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

⚠️ Outside diff range comments (3)
server/app/__init__.py (1)

60-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the comment to reflect the new token expiry.

The comment still references "15 min" but tokens now expire after 1 hour (line 45).

📝 Proposed fix
-    # In-memory JTI blocklist — tokens expire in 15 min so the set stays small.
+    # In-memory JTI blocklist — tokens expire in 1 hour so the set stays small.
     # Cleared on server restart, which is acceptable: a restarted server issues new tokens.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/__init__.py` at line 60, Update the inline comment that currently
reads "# In-memory JTI blocklist — tokens expire in 15 min so the set stays
small." to reflect the new token expiry of one hour (e.g., "tokens expire in 1
hour"), ensuring the JTI blocklist comment accurately describes current behavior
near the in-memory JTI/blocklist declaration in server.app.__init__.py.
server/app/auth/routes.py (1)

233-233: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce password complexity for new passwords.

The /change-password endpoint does not validate new_password against the same complexity rules enforced at registration (lines 50-63). Users can change to weak passwords, defeating the security requirement.

🔒 Proposed fix: extract validation and apply to both endpoints

Add a helper function before the register route:

def _validate_password_complexity(password):
    """Validate password meets complexity requirements.
    
    Returns:
        list: Error messages for failed requirements, empty if valid.
    """
    errors = []
    if len(password) < 8:
        errors.append('at least 8 characters')
    if not re.search(r'[A-Z]', password):
        errors.append('one uppercase letter')
    if not re.search(r'[a-z]', password):
        errors.append('one lowercase letter')
    if not re.search(r'[0-9]', password):
        errors.append('one number')
    if not re.search(r'[^A-Za-z0-9]', password):
        errors.append('one special character')
    return errors

Then update /register (lines 50-63):

     pw = data['password']
-    pw_errors = []
-    if len(pw) < 8:
-        pw_errors.append('at least 8 characters')
-    if not re.search(r'[A-Z]', pw):
-        pw_errors.append('one uppercase letter')
-    if not re.search(r'[a-z]', pw):
-        pw_errors.append('one lowercase letter')
-    if not re.search(r'[0-9]', pw):
-        pw_errors.append('one number')
-    if not re.search(r'[^A-Za-z0-9]', pw):
-        pw_errors.append('one special character')
+    pw_errors = _validate_password_complexity(pw)
     if pw_errors:
         return jsonify({'error': f"Password must contain: {', '.join(pw_errors)}"}), 400

And add validation to /change-password before line 233:

+    pw_errors = _validate_password_complexity(data['new_password'])
+    if pw_errors:
+        return jsonify({'error': f"New password must contain: {', '.join(pw_errors)}"}), 400
+
     new_hash = ph.hash(data['new_password'])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/auth/routes.py` at line 233, Extract the password complexity
checks into a helper function named _validate_password_complexity(password) that
returns a list of error messages (empty if valid) and include checks for min
length, uppercase, lowercase, digit, and special char; call this helper from the
register route (replacing the inline checks) and from the change-password flow
before hashing (i.e., before new_hash = ph.hash(data['new_password'])), and if
the helper returns errors return the same error response used in register (400
with descriptive message) so change-password enforces the same complexity rules.
client-web/js/api.js (1)

157-171: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear cached sent plaintext on logout.

renderInbox() writes sent bodies to sessionStorage as sent_plain_*, but logout only removes auth/key fields. That leaves previous-session plaintext readable after sign-out in the same tab.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client-web/js/api.js` around lines 157 - 171, The logout() function in
client-web/js/api.js fails to clear sent-message plaintext that renderInbox()
stores under sessionStorage keys like "sent_plain_*", leaving previous-session
plaintext accessible after sign-out; update logout() to iterate sessionStorage
keys and remove any entries whose key startsWith "sent_plain_" (and any other
"sent_*" keys you deem necessary) after clearing tokens/keys so all cached sent
plaintext is removed on logout. Ensure the removal runs in the same function
(logout) and does not rely on other UI flows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client-web/js/api.js`:
- Around line 43-44: The code drops _sessionUserId across reload/unlock causing
getUserId() to return null and breaking renderInbox() send/forward paths;
persist the user_id into sessionStorage when setting it and restore it when the
app initializes/unlocks. Update the functions related to _sessionUserId (e.g.,
getUserId, clearUserId and whatever setter around lines 118-119) to read/write
sessionStorage["user_id"] (or a consistent key) so that on reload/unlock the
code restores _sessionUserId from sessionStorage before renderInbox() runs and
before any send/forward handlers call getUserId().

In `@client-web/js/app.js`:
- Around line 40-45: isAuthView currently includes 'verify' causing the
auth-mode layout to apply to the public verification page; update the logic that
defines isAuthView (the expression that sets const isAuthView) so it excludes
'verify' (e.g., only treat actual auth routes like 'login' or 'signup' as auth
views), leaving the navbar.hidden and appEl.classList.toggle('auth-mode',
isAuthView) usage unchanged so the verify view renders with the normal
full-width layout.

In `@client-web/js/views/inbox.js`:
- Around line 600-647: The forward case currently re-encrypts and calls
api.forwardMessage directly, which bypasses sendFromThread() (and its TOFU
confirmation) and the sent_plain_* recovery path; change the flow to obtain
plaintext the same way (using decryptMessage as you already do) but then call
sendFromThread() (or a new helper that wraps sendFromThread) to perform the
actual send/forward so TOFU checks and sent_plain_* bookkeeping are preserved;
update the forward branch to pass the decrypted plaintext, recipientUsername
(and original message id/context if needed) into sendFromThread()/the wrapper
and remove the direct call to api.forwardMessage/encryptMessage so forwarded
messages follow the same send path and recovery semantics.
- Around line 414-458: The poll handler (pollInterval) currently only appends
unseen IDs and never reconciles deletes/revokes in currentConvMap or knownIds;
update the polling logic after receiving data.messages to detect messages marked
deleted/revoked (e.g., a flag like msg.deleted or msg.revoked) and for each such
message remove or update that message in currentConvMap and knownIds, then
remove any corresponding DOM bubble and call renderConvList(currentConvMap,
activePartner) to refresh sidebar previews and reopened threads; also ensure the
same reconciliation is applied in the other polling area around lines 571-579
and when tryDecrypt returns null/placeholder so revoked content doesn't get
reinserted via buildBubble/handleAction.
- Around line 345-358: The async getUser(...) callback can write a stale
fingerprint if the user switches threads; to fix, capture the expected partner
id before awaiting (e.g., const expectedPartner = partner) and, right before any
DOM updates (before fpEl.textContent / fpEl.style changes), verify the
currently-open thread still matches expectedPartner (for example compare a
stable source-of-truth such as a currentPartner variable or an attribute on the
thread-fingerprint element like fpEl.dataset.partner or another thread ID in the
UI); if they differ, return early and do not update the element. Keep existing
logic using getUser, keyFingerprint and tofuCheck but gate all DOM writes with
that equality check to prevent overwriting with stale data.
- Around line 181-191: Cache sender keys by the actual key value instead of only
msg.sender_username: compute a stable key identifier from
msg.sender_x25519_public_key (e.g. the base64 string itself), then look up
senderKeyCache using that identifier; if not present, call
tofuCheck(msg.sender_username, msg.sender_x25519_public_key) and import the key
with crypto.subtle.importKey, storing it under the key identifier. Additionally,
detect when an incoming msg.sender_x25519_public_key differs from any previously
cached key for msg.sender_username and run tofuCheck to surface key-rotation
warnings, then update the cache to the new key identifier so decryption uses the
correct imported key (references: senderKeyCache, tofuCheck,
msg.sender_x25519_public_key, msg.sender_username, crypto.subtle.importKey).

In `@server/app/messages/routes.py`:
- Around line 242-244: The revoke-forwarding check only blocks forwarding back
to the original recipient (uses message['is_revoked'] and recipient['id'] ==
message['recipient_id']) but doesn’t prevent other forwards; update the logic
based on the intended policy: either block all forwards of revoked messages by
changing the check to simply if message['is_revoked']: return 403, or if you
only want to prevent the original recipient from further distributing it, change
the check to compare the acting user (e.g. current_user['id'] or sender_id used
in this route) against message['recipient_id'] (i.e. if message['is_revoked']
and actor_id == message['recipient_id']: return 403), and add a clarifying
comment describing the chosen design.

---

Outside diff comments:
In `@client-web/js/api.js`:
- Around line 157-171: The logout() function in client-web/js/api.js fails to
clear sent-message plaintext that renderInbox() stores under sessionStorage keys
like "sent_plain_*", leaving previous-session plaintext accessible after
sign-out; update logout() to iterate sessionStorage keys and remove any entries
whose key startsWith "sent_plain_" (and any other "sent_*" keys you deem
necessary) after clearing tokens/keys so all cached sent plaintext is removed on
logout. Ensure the removal runs in the same function (logout) and does not rely
on other UI flows.

In `@server/app/__init__.py`:
- Line 60: Update the inline comment that currently reads "# In-memory JTI
blocklist — tokens expire in 15 min so the set stays small." to reflect the new
token expiry of one hour (e.g., "tokens expire in 1 hour"), ensuring the JTI
blocklist comment accurately describes current behavior near the in-memory
JTI/blocklist declaration in server.app.__init__.py.

In `@server/app/auth/routes.py`:
- Line 233: Extract the password complexity checks into a helper function named
_validate_password_complexity(password) that returns a list of error messages
(empty if valid) and include checks for min length, uppercase, lowercase, digit,
and special char; call this helper from the register route (replacing the inline
checks) and from the change-password flow before hashing (i.e., before new_hash
= ph.hash(data['new_password'])), and if the helper returns errors return the
same error response used in register (400 with descriptive message) so
change-password enforces the same complexity rules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d4269d69-15e1-4c8f-830d-21ecef6550be

📥 Commits

Reviewing files that changed from the base of the PR and between f4f918f and 01f301b.

⛔ Files ignored due to path filters (1)
  • client-web/images/bows_background.png is excluded by !**/*.png
📒 Files selected for processing (17)
  • .gitignore
  • client-cpp/README.md
  • client-web/README.md
  • client-web/css/auth.css
  • client-web/css/base.css
  • client-web/css/chat.css
  • client-web/css/verify.css
  • client-web/index.html
  • client-web/js/api.js
  • client-web/js/app.js
  • client-web/js/views.js
  • client-web/js/views/auth.js
  • client-web/js/views/helpers.js
  • client-web/js/views/inbox.js
  • server/app/__init__.py
  • server/app/auth/routes.py
  • server/app/messages/routes.py
💤 Files with no reviewable changes (1)
  • client-web/js/views.js

Comment thread client-web/js/api.js
Comment thread client-web/js/app.js
Comment thread client-web/js/views/inbox.js
Comment thread client-web/js/views/inbox.js
Comment thread client-web/js/views/inbox.js
Comment thread client-web/js/views/inbox.js
Comment thread server/app/messages/routes.py
@sarahmc253 sarahmc253 merged commit e5720d1 into main Jun 2, 2026
1 check passed
@sarahmc253 sarahmc253 deleted the web-client-fixes branch June 2, 2026 10:37
@sarahmc253 sarahmc253 restored the web-client-fixes branch June 2, 2026 10:37
@sarahmc253 sarahmc253 deleted the web-client-fixes branch June 2, 2026 10:37
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.

2 participants