Skip to content

feat(web): polish auth and profile flows#405

Merged
onerandomdevv merged 3 commits into
devfrom
feat/web-register-split-layout
Jun 9, 2026
Merged

feat(web): polish auth and profile flows#405
onerandomdevv merged 3 commits into
devfrom
feat/web-register-split-layout

Conversation

@onerandomdevv

@onerandomdevv onerandomdevv commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Updates auth/onboarding screens with the split auth layout and email verification polish.
  • Adds public profile follow/unfollow behavior, follower/following/verified follower pages, and new follower notifications.
  • Polishes profile settings, sidebar store actions, logout menu, and auth middleware/session navigation behavior.

Details

  • Adds dedicated profile social graph routes:
    • /u/[username]/followers
    • /u/[username]/following
    • /u/[username]/verified_followers
  • Profile stats now link to the social graph pages.
  • Profile settings now supports a short multiline bio, avoids blank username save failures, and redirects back to the public profile after save.
  • Sidebar keeps a stable Create Store / Manage Store action and supports logout from the account dots menu.
  • Middleware now treats either access or refresh cookie as a session marker for route guarding.

Validation

  • Backend typecheck passed: npx.cmd tsc --noEmit
  • Focused backend auth registration tests passed: pnpm.cmd test -- auth.service.registration.spec.ts --runInBand (8/8)
  • Backend build passed: pnpm.cmd run build
  • Backend touched-file ESLint passed
  • Web lint passed: pnpm.cmd run lint
  • Web typecheck passed: npx.cmd tsc --noEmit
  • Web build passed: pnpm.cmd run build
  • git diff --check passed
  • Staged diff check passed
  • Staged secret scan passed

Notes

  • No secrets or env files included.
  • No schema changes included.

Summary by CodeRabbit

  • New Features

    • Follow/unfollow actions, follower counts, and follow-status indicators across profiles
    • Social graph pages: Followers, Following, and Verified Followers
    • Profile link sharing to clipboard; account menu with logout
  • Improvements

    • Email verification now issues session tokens and supports server-driven redirects
    • Redesigned auth pages (login/register/verify) and auth layout behaviors
    • Profile edit: bio normalization, character/line limits, display-name validation
    • Session detection now checks access or refresh cookies
  • Bug Fixes

    • Follow notifications link to a user’s profile when username is available

@codesandbox

codesandbox Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
twizrr-web Ready Ready Preview, Comment Jun 9, 2026 5:38am

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Backend: email verification now issues session tokens and sets auth cookies; user follow creation and follower-read APIs added with in-app NEW_FOLLOWER notifications. Frontend: follow/unfollow UI, social-graph list pages, auth layout/page UI updates, settings and left-nav UX changes, and middleware session cookie detection expanded.

Changes

Follow System with Email Verification Tokens & Social UI

Layer / File(s) Summary
Email verification with session tokens
apps/backend/src/domains/users/auth/auth.service.ts, apps/backend/src/domains/users/auth/auth.controller.ts, apps/backend/src/domains/users/auth/auth.service.registration.spec.ts
verifyEmail now accepts SessionMetadata, selects storeProfile, issues session tokens via createSessionTokens, and returns redirectTo; controller sets auth cookies. Tests expanded to mock JWT signing, session creation, OTP flows, and tier-upgrade calls.
Follow creation & follower listing with notifications
apps/backend/src/domains/users/user/user.service.ts, apps/backend/src/domains/users/user/user.controller.ts, apps/backend/src/domains/users/user/user.module.ts
UserService injects NotificationService; followUser uses a transaction, maps unique-constraint to FOLLOW_ALREADY_EXISTS, attempts NEW_FOLLOWER notification, and adds getFollowStatus, listFollowers, listVerifiedFollowers, listFollowing, and username normalization helper. UserModule imports NotificationModule and controller exposes listing endpoints.
Follow notification URL generation
apps/backend/src/domains/social/notification/notification.service.ts
NEW_FOLLOWER notifications now generate /u/${username} URLs when username is not the fallback "Someone".
Frontend follow API helpers & types
apps/web/src/lib/user.ts
Adds PublicFollowerProfile/PublicFollowListResponse types and helpers: followUser, unfollowUser, fetchUserFollowStatus, fetchUserFollowers, fetchUserFollowing, fetchVerifiedUserFollowers.
Public profile client: follow, share, actions
apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx
Refactored to manage follow state, load follow status on mount, provide follow/unfollow toggle with optimistic count updates and error handling, clipboard share feedback, back navigation, and ProfileActions for own vs other profiles.
Social graph client & pages
apps/web/src/app/(public)/u/[username]/_components/SocialGraphPageClient.tsx, followers/page.tsx, following/page.tsx, verified_followers/page.tsx
New SocialGraphPageClient renders followers/following/verified-followers lists with skeleton/error/empty states, per-row follow controls with 401 redirect and FOLLOW_ALREADY_EXISTS handling, plus helper utilities and route pages.
Auth pages UI & route-aware layout
apps/web/src/app/(auth)/layout.tsx, login/page.tsx, register/page.tsx, verify-email/page.tsx
AuthLayout is a client and uses usePathname() to vary header/layout. Login/Register refactored to two-column layouts with preview panels and shared helpers; Login calls router.refresh() before redirect. VerifyEmail page uses VerifyEmailShell and honors server-provided redirectTo.
Settings validation & LeftNav account menu
apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx, apps/web/src/components/layout/LeftNav.tsx
SettingsClient normalizes bio, enforces char/line limits, adds displayName validation and textarea helper. LeftNav is a client component with account dropdown state, outside/Escape close handlers, logout flow posting /auth/logout then router.refresh(), and simplified store action link.
Middleware session cookie detection
apps/web/src/middleware.ts
Middleware now treats presence of either ACCESS_COOKIE or REFRESH_COOKIE as an active session.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • SAHEED2010

Poem

🐰 A rabbit hops through social glade,
Following trails that devs have made,
Tokens set and profiles share,
Pages, panels, navs with care,
Hooray — the network blooms, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title 'feat(web): polish auth and profile flows' directly relates to the main changes in the PR, which focus on updating auth screens, adding profile follow features, and polishing profile/sidebar behavior.
Description check ✅ Passed The PR description covers all required template sections: what changed (split auth layout, follow features, profile polish), type of change (new feature), areas affected (backend and web), testing validation (multiple test runs documented), and a comprehensive pre-commit checklist.
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 feat/web-register-split-layout

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/backend/src/domains/users/user/user.service.ts (1)

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

Make follow creation atomic to avoid duplicate-follow race failures.

The separate existence check before insert introduces a race window. Two concurrent requests can both pass the check and one request will fail during insert on the unique key path instead of returning a clean conflict response.

Suggested fix
-    const existingFollow = await this.prisma.followRelation.findUnique({
-      where: {
-        followerId_targetUserId: {
-          followerId,
-          targetUserId,
-        },
-      },
-      select: { id: true },
-    });
-
-    if (existingFollow) {
-      throw new ConflictException({
-        message: "Already following this user",
-        code: "FOLLOW_ALREADY_EXISTS",
-      });
-    }
-
-    const [, follower] = await this.prisma.$transaction([
-      this.prisma.followRelation.create({
-        data: {
-          followerId,
-          targetUserId,
-          targetType: FollowTargetType.USER,
-        },
-        select: { id: true },
-      }),
-      this.prisma.user.findUnique({
-        where: { id: followerId },
-        select: {
-          id: true,
-          username: true,
-          displayName: true,
-        },
-      }),
-    ]);
+    let follower:
+      | { id: string; username: string | null; displayName: string | null }
+      | null
+      | undefined;
+    try {
+      [, follower] = await this.prisma.$transaction([
+        this.prisma.followRelation.create({
+          data: {
+            followerId,
+            targetUserId,
+            targetType: FollowTargetType.USER,
+          },
+          select: { id: true },
+        }),
+        this.prisma.user.findUnique({
+          where: { id: followerId },
+          select: {
+            id: true,
+            username: true,
+            displayName: true,
+          },
+        }),
+      ]);
+    } catch (error) {
+      throw new ConflictException({
+        message: "Already following this user",
+        code: "FOLLOW_ALREADY_EXISTS",
+      });
+    }
🤖 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 `@apps/backend/src/domains/users/user/user.service.ts` around lines 255 - 280,
The pre-check using this.prisma.followRelation.findUnique (existingFollow)
creates a race; remove the separate existence check and make follow creation
atomic by attempting the insert (this.prisma.followRelation.create / the current
this.prisma.$transaction call) and catching unique constraint/database errors;
on unique-constraint failure throw the same ConflictException with message
"Already following this user" and code "FOLLOW_ALREADY_EXISTS" so concurrent
requests return a clean conflict rather than relying on the separate findUnique
check.
🧹 Nitpick comments (2)
apps/web/src/components/layout/LeftNav.tsx (1)

220-237: 💤 Low value

Consider adding ARIA roles for complete menu accessibility.

The trigger button has aria-haspopup="menu" and aria-expanded, but the popup panel lacks role="menu" and the logout button lacks role="menuitem". Adding these would complete the ARIA menu pattern.

Suggested enhancement
-          <div className="absolute bottom-[calc(100%+0.5rem)] left-0 hidden min-w-[240px] rounded-2xl bg-[var(--color-espresso)] p-2 text-[var(--color-bianca)] shadow-lg lg:block">
+          <div role="menu" className="absolute bottom-[calc(100%+0.5rem)] left-0 hidden min-w-[240px] rounded-2xl bg-[var(--color-espresso)] p-2 text-[var(--color-bianca)] shadow-lg lg:block">
             <button
               type="button"
+              role="menuitem"
               onClick={handleLogout}

Also applies to: 255-268

🤖 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 `@apps/web/src/components/layout/LeftNav.tsx` around lines 220 - 237, The popup
panel rendered when accountMenuOpen is true should include ARIA menu roles—add
role="menu" to the container div that currently renders the account menu and add
role="menuitem" to the logout button (the button that calls handleLogout and
uses isLoggingOut/visibleUsername). Ensure these attributes are added in the
same JSX where accountMenuOpen is used so the trigger's aria-haspopup="menu" and
aria-expanded remain consistent with the panel and menu item roles.
apps/web/src/app/(auth)/verify-email/page.tsx (1)

58-63: ⚡ Quick win

Frontend type doesn't match backend contract.

The backend's verifyEmail method (auth.service.ts:473) returns { verified: true; redirectTo: string } where redirectTo is required, but the frontend types it as optional (redirectTo?: string). While the fallback logic handles this safely, the type should reflect the actual API contract.

📝 Suggested type correction
-      const result = await api.post<{ verified: true; redirectTo?: string }>(
+      const result = await api.post<{ verified: true; redirectTo: string }>(
         "/auth/email/verify",
         { email, code },
       );
       setPageState("success");
-      setTimeout(() => router.push(result.redirectTo || next), 1500);
+      setTimeout(() => router.push(result.redirectTo), 1500);
🤖 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 `@apps/web/src/app/`(auth)/verify-email/page.tsx around lines 58 - 63, The
frontend types for the email verification response are too permissive: update
the API call in verify-email page (the api.post call that assigns to result in
page.tsx) to match the backend auth.service.ts verifyEmail contract by changing
the response type from { verified: true; redirectTo?: string } to { verified:
true; redirectTo: string } so redirectTo is required; keep the existing fallback
logic (result.redirectTo || next) if desired but ensure the generic type for
api.post and any related usages reflect the required redirectTo.
🤖 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
`@apps/web/src/app/`(public)/u/[username]/_components/SocialGraphPageClient.tsx:
- Around line 51-84: Replace the manual useEffect/useState fetch with TanStack
Query: use useQuery(['socialGraph', username, listType], () =>
loadSocialGraph(username, listType)) to drive result/isLoading/error instead of
setResult/setIsLoading/setError, and for the follow/unfollow mutation code (the
handlers around lines 281-312) replace imperative API calls with useMutation
hooks that call your follow/unfollow API and onSuccess invalidate the
social-graph query (useQueryClient().invalidateQueries(['socialGraph',
username]) and/or invalidate by listType with ['socialGraph', username,
listType]) so post-mutation UI is consistent; ensure you import and use
useQuery, useMutation, and useQueryClient from `@tanstack/react-query` and remove
the active-flag cleanup logic from the old effect.
- Around line 106-111: The back-button uses a 40×40 touch area (className
contains h-10 w-10) which is under the 44×44 mobile minimum; update the button
in SocialGraphPageClient (the element with aria-label="Back to profile" and
onClick={() => router.push(profileHref)}) to provide at least 44×44px — e.g.,
replace h-10 w-10 with h-11 w-11 or add min-h-[44px] min-w-[44px] to the
className so the touch target meets the guideline.

In `@apps/web/src/app/`(public)/u/[username]/PublicUserProfileClient.tsx:
- Around line 212-214: In PublicUserProfileClient, update the heading elements
to use the heading font classes: replace "font-cabinet" with "font-syne
font-bold" on the <h1> that renders {displayName} and the other heading at the
block around lines 243-245; keep "font-cabinet" for body text elsewhere so only
the heading elements change.
- Around line 203-208: The back button in PublicUserProfileClient (the <button>
that calls handleBackNavigation) uses h-10 w-10 (40×40) which is below the 44×44
mobile touch target; update its className to provide at least 44×44 hit area
(for example replace h-10 w-10 with h-11 w-11 or otherwise ensure min 44px
width/height) so the button meets the mobile minimum touch target while
preserving existing styles like rounded-full and focus/hover classes.

In `@apps/web/src/app/`(shopper)/buyer/settings/SettingsClient.tsx:
- Around line 336-343: The back button in SettingsClient uses h-10 w-10
(40×40px) which is below the 44×44px touch-target requirement; update the button
element in SettingsClient.tsx (the <button> that wraps ArrowLeft and calls
router.push(getPublicUserHref(profile.username))) so its interactive size is at
least 44×44px—either change the classes to h-11 w-11 (or min-h-11 min-w-11) or
add equivalent padding (e.g., p-1) while keeping the icon ArrowLeft at h-5 w-5
to preserve visual sizing; ensure CSS focus/hover classes remain applied.

---

Outside diff comments:
In `@apps/backend/src/domains/users/user/user.service.ts`:
- Around line 255-280: The pre-check using this.prisma.followRelation.findUnique
(existingFollow) creates a race; remove the separate existence check and make
follow creation atomic by attempting the insert
(this.prisma.followRelation.create / the current this.prisma.$transaction call)
and catching unique constraint/database errors; on unique-constraint failure
throw the same ConflictException with message "Already following this user" and
code "FOLLOW_ALREADY_EXISTS" so concurrent requests return a clean conflict
rather than relying on the separate findUnique check.

---

Nitpick comments:
In `@apps/web/src/app/`(auth)/verify-email/page.tsx:
- Around line 58-63: The frontend types for the email verification response are
too permissive: update the API call in verify-email page (the api.post call that
assigns to result in page.tsx) to match the backend auth.service.ts verifyEmail
contract by changing the response type from { verified: true; redirectTo?:
string } to { verified: true; redirectTo: string } so redirectTo is required;
keep the existing fallback logic (result.redirectTo || next) if desired but
ensure the generic type for api.post and any related usages reflect the required
redirectTo.

In `@apps/web/src/components/layout/LeftNav.tsx`:
- Around line 220-237: The popup panel rendered when accountMenuOpen is true
should include ARIA menu roles—add role="menu" to the container div that
currently renders the account menu and add role="menuitem" to the logout button
(the button that calls handleLogout and uses isLoggingOut/visibleUsername).
Ensure these attributes are added in the same JSX where accountMenuOpen is used
so the trigger's aria-haspopup="menu" and aria-expanded remain consistent with
the panel and menu item roles.
🪄 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

Run ID: 5c1870b3-4f5d-4f3a-95ae-a8c49edd2c49

📥 Commits

Reviewing files that changed from the base of the PR and between 405f7e1 and ce0b39f.

📒 Files selected for processing (20)
  • apps/backend/src/domains/social/notification/notification.service.ts
  • apps/backend/src/domains/users/auth/auth.controller.ts
  • apps/backend/src/domains/users/auth/auth.service.registration.spec.ts
  • apps/backend/src/domains/users/auth/auth.service.ts
  • apps/backend/src/domains/users/user/user.controller.ts
  • apps/backend/src/domains/users/user/user.module.ts
  • apps/backend/src/domains/users/user/user.service.ts
  • apps/web/src/app/(auth)/layout.tsx
  • apps/web/src/app/(auth)/login/page.tsx
  • apps/web/src/app/(auth)/register/page.tsx
  • apps/web/src/app/(auth)/verify-email/page.tsx
  • apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx
  • apps/web/src/app/(public)/u/[username]/_components/SocialGraphPageClient.tsx
  • apps/web/src/app/(public)/u/[username]/followers/page.tsx
  • apps/web/src/app/(public)/u/[username]/following/page.tsx
  • apps/web/src/app/(public)/u/[username]/verified_followers/page.tsx
  • apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx
  • apps/web/src/components/layout/LeftNav.tsx
  • apps/web/src/lib/user.ts
  • apps/web/src/middleware.ts

Comment thread apps/web/src/app/(public)/u/[username]/_components/SocialGraphPageClient.tsx Outdated
Comment thread apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx
Comment thread apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx Outdated
Comment thread apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@apps/web/src/components/layout/LeftNav.tsx`:
- Around line 225-229: The logout button currently calls handleLogout and
navigates away even when the /auth/logout request fails under JwtAuthGuard
(e.g., expired access token), which lets a stale refresh cookie keep the
session; update handleLogout to treat non-2xx responses as failures and, on a
401 (or any failure), call a server endpoint that explicitly clears HttpOnly
cookies without requiring auth (e.g., a new POST /auth/clear-cookies or
/auth/logout/public) so both cookies are removed server-side; only navigate away
after a successful clear and ensure isLoggingOut is set/unset correctly on both
success and failure paths and errors are surfaced instead of swallowed.
- Around line 221-227: The popup in LeftNav.tsx currently uses
role="menu"/role="menuitem" (the div with role="menu" and the button with
role="menuitem") but does not implement APG menu-button behavior; either
implement full WAI-ARIA menu semantics (on open move focus into first menu item,
implement ArrowUp/ArrowDown/Home/End keyboard handlers with roving tabindex or
aria-activedescendant, and restore focus to the trigger and update aria-expanded
on close) or remove the menu-specific ARIA roles/aria-haspopup/aria-expanded
from the trigger and menu items and rely on native button semantics; pick one
approach, update the code around the popup div and trigger button in LeftNav.tsx
accordingly, and ensure focus restoration if you implement the APG behavior.
🪄 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

Run ID: f6c0fb0c-6120-4985-9d7f-3ab6b49afa35

📥 Commits

Reviewing files that changed from the base of the PR and between ce0b39f and 86eb3ca.

📒 Files selected for processing (6)
  • apps/backend/src/domains/users/user/user.service.ts
  • apps/web/src/app/(auth)/verify-email/page.tsx
  • apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx
  • apps/web/src/app/(public)/u/[username]/_components/SocialGraphPageClient.tsx
  • apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx
  • apps/web/src/components/layout/LeftNav.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/backend/src/domains/users/user/user.service.ts
  • apps/web/src/app/(public)/u/[username]/_components/SocialGraphPageClient.tsx
  • apps/web/src/app/(auth)/verify-email/page.tsx
  • apps/web/src/app/(public)/u/[username]/PublicUserProfileClient.tsx
  • apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx

Comment on lines +221 to +227
<div
role="menu"
className="absolute bottom-[calc(100%+0.5rem)] left-0 hidden min-w-[240px] rounded-2xl bg-[var(--color-espresso)] p-2 text-[var(--color-bianca)] shadow-lg lg:block"
>
<button
type="button"
role="menuitem"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For a menu button pattern using role="menu" and role="menuitem", what keyboard and focus-management behaviors are required by the WAI-ARIA Authoring Practices?

💡 Result:

According to the WAI-ARIA Authoring Practices (APG), a menu button is a button that opens a menu, typically styled with an indicator like a downward-pointing arrow [1]. The pattern requires the following keyboard and focus-management behaviors: Keyboard Behaviors for the Menu Button When focus is on the button: - Enter or Space: Opens the menu and places focus on the first menu item [1][2]. - Down Arrow (optional): Opens the menu and moves focus to the first menu item [1][2]. - Up Arrow (optional): Opens the menu and moves focus to the last menu item [1][2]. Keyboard Behaviors for the Menu Once the menu is open, the menu acts as a composite widget where Tab and Shift + Tab do not move focus among the menu items [3]. Instead, users navigate using the following keys: - Down Arrow: Moves focus to the next menu item; if focus is on the last item, it wraps to the first [4][5]. - Up Arrow: Moves focus to the previous menu item; if focus is on the first item, it wraps to the last [4][5]. - Home: Moves focus to the first menu item [4][6]. - End: Moves focus to the last menu item [4][6]. - Escape: Closes the menu and returns focus to the menu button [4][5][6]. - A-Z/a-z: Moves focus to the next menu item starting with the typed character [4][6]. - Enter/Space: Activates the menu item and closes the menu [4][5][6]. Focus Management The APG mandates that the menu button must manage focus such that it returns to the button when the menu is closed [4][6][1]. Within the menu, focus management is typically implemented using one of two patterns: - Roving Tabindex: Each menu item has tabindex="-1" (to be focusable via script), and JavaScript manages focus movement using element.focus [5][3]. - aria-activedescendant: The menu container has tabindex="0" (or -1) and uses the aria-activedescendant property to indicate the currently "focused" item while the DOM focus remains on the container [4][3]. Additionally, the menu button must use the following roles, states, and properties: - Role: button [1]. - aria-haspopup: Set to "menu" or "true" [1]. - aria-expanded: Set to "true" when the menu is open and "false" when it is hidden [1]. - aria-controls: Optionally used to refer to the element with role menu [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/components/layout/LeftNav.tsx"

# Ensure file exists
test -f "$FILE"

echo "== Lines around 221-240 =="
nl -ba "$FILE" | sed -n '200,260p'

echo
echo "== Search for menu-button/focus-management patterns =="
rg -n --hidden --no-mmap -S "role=\"menu\"|role=\"menuitem\"|aria-activedescendant|aria-haspopup|aria-expanded|aria-controls|Escape|onKeyDown|keyDown|ArrowDown|ArrowUp|Home|End|tabindex=\"-1\"|focus\(|restore focus|activeDescendant|roving" "$FILE"

echo
echo "== Search for refs/focusable menu items =="
rg -n --hidden --no-mmap -S "useRef|useEffect|querySelectorAll|menuItem|menuItemRefs|first.*item|last.*item|focusFirst|focusLast" "$FILE"

Repository: coded-devs/twizrr

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/components/layout/LeftNav.tsx"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

echo "== Lines around 221-240 =="
cat -n "$FILE" | sed -n '200,260p'

echo
echo "== Search for menu-button/focus-management patterns =="
rg -n --hidden --no-mmap -S 'role="menu"|role="menuitem"|aria-activedescendant|aria-haspopup|aria-expanded|aria-controls|Escape|onKeyDown|keyDown|ArrowDown|ArrowUp|Home|End|tabindex="-1"|focus\(|restore focus|activeDescendant|roving|menu-button|menubutton' "$FILE"

echo
echo "== Search for refs/focusable menu items =="
rg -n --hidden --no-mmap -S 'useRef|useEffect|querySelectorAll|menuItem|menuItemRefs|first.*item|last.*item|focusFirst|focusLast|aria-activedescendant' "$FILE"

Repository: coded-devs/twizrr

Length of output: 3598


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/components/layout/LeftNav.tsx"
test -f "$FILE"

echo "== Imports / state / effects (1-120) =="
cat -n "$FILE" | sed -n '1,140p'

echo
echo "== Escape handling block (50-110) =="
cat -n "$FILE" | sed -n '40,110p'

echo
echo "== Account menu toggle button (240-320) =="
cat -n "$FILE" | sed -n '240,330p'

Repository: coded-devs/twizrr

Length of output: 9588


Fix ARIA menu semantics to match the WAI-ARIA APG focus/keyboard contract.

The popup uses role="menu"/role="menuitem" and the trigger has aria-haspopup="menu"/aria-expanded, but it only closes via outside-click and Escape—it does not (a) move focus to the first menu item when opened, (b) restore focus to the trigger when closed, or (c) implement the APG keyboard navigation for an open menu (ArrowUp/ArrowDown/Home/End with roving focus via tabindex and/or aria-activedescendant). Either implement the full APG menu-button behavior or remove role="menu"/role="menuitem" and rely on native button semantics.

🤖 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 `@apps/web/src/components/layout/LeftNav.tsx` around lines 221 - 227, The popup
in LeftNav.tsx currently uses role="menu"/role="menuitem" (the div with
role="menu" and the button with role="menuitem") but does not implement APG
menu-button behavior; either implement full WAI-ARIA menu semantics (on open
move focus into first menu item, implement ArrowUp/ArrowDown/Home/End keyboard
handlers with roving tabindex or aria-activedescendant, and restore focus to the
trigger and update aria-expanded on close) or remove the menu-specific ARIA
roles/aria-haspopup/aria-expanded from the trigger and menu items and rely on
native button semantics; pick one approach, update the code around the popup div
and trigger button in LeftNav.tsx accordingly, and ensure focus restoration if
you implement the APG behavior.

Comment on lines +225 to +229
<button
type="button"
role="menuitem"
onClick={handleLogout}
disabled={isLoggingOut}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't treat logout failures as success when the refresh cookie can still mark a session.

/auth/logout is guarded by JwtAuthGuard, so an expired access token will 401 before the backend clears the HttpOnly cookies. This click path still swallows that failure and navigates away; with the new middleware behavior that treats either the access or refresh cookie as a valid session marker, a stale refresh cookie can immediately keep the user signed in after “logout”. Make the logout flow clear both cookies even when only the refresh token remains, or route failures through a server-side clear-cookie path instead of ignoring them.

🤖 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 `@apps/web/src/components/layout/LeftNav.tsx` around lines 225 - 229, The
logout button currently calls handleLogout and navigates away even when the
/auth/logout request fails under JwtAuthGuard (e.g., expired access token),
which lets a stale refresh cookie keep the session; update handleLogout to treat
non-2xx responses as failures and, on a 401 (or any failure), call a server
endpoint that explicitly clears HttpOnly cookies without requiring auth (e.g., a
new POST /auth/clear-cookies or /auth/logout/public) so both cookies are removed
server-side; only navigate away after a successful clear and ensure isLoggingOut
is set/unset correctly on both success and failure paths and errors are surfaced
instead of swallowed.

@onerandomdevv onerandomdevv merged commit 0fd553f into dev Jun 9, 2026
10 checks passed
@onerandomdevv onerandomdevv deleted the feat/web-register-split-layout branch June 9, 2026 05:51
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.

1 participant