feat: server-side middleware for auth redirects and role gates#249
Conversation
📝 WalkthroughWalkthroughA new Next.js middleware is introduced that enforces authentication and role-based access control by inspecting JWT tokens from the Changes
Sequence DiagramsequenceDiagram
actor Client as Browser/Client
participant MW as Next.js Middleware
participant JWT as JWT Decoder
participant Router as Route Handler
participant Auth as Auth Pages
participant DB as Role Dashboard
Client->>MW: Request to protected route
MW->>MW: Check request path
alt Token exists in cookie
MW->>JWT: Decode hwos_access_token
JWT-->>MW: Decoded payload & role
MW->>MW: Validate user role vs. route
alt Role matches route requirement
MW->>Router: Allow request through
Router-->>Client: Route handler responds
else Role mismatch
MW->>DB: Redirect to role dashboard
DB-->>Client: Dashboard loads
end
alt User accessing auth page
MW->>DB: Redirect to role dashboard
DB-->>Client: Dashboard loads
end
else No token (unauthenticated)
MW->>MW: Check if accessing protected route
alt Accessing /buyer, /merchant, /admin
MW->>Auth: Redirect to /login
Auth-->>Client: Login page with redirect param
else Accessing public route
MW->>Router: Allow request through
Router-->>Client: Route handler responds
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/web/src/middleware.ts (1)
102-119: Incorrect non-null assertions onuserRole.The non-null assertion
userRole!is used on lines 105, 111, and 117, butuserRolecan beundefined(e.g., if the JWT lacks arolefield or contains an unrecognized role). While the|| "/"fallback prevents runtime errors, the assertion is semantically incorrect.♻️ Remove non-null assertions
if (path.startsWith("/buyer") && userRole !== "BUYER") { return NextResponse.redirect( - new URL(ROLE_DASHBOARDS[userRole!] || "/", request.url), + new URL((userRole && ROLE_DASHBOARDS[userRole]) || "/", request.url), ); } if (path.startsWith("/merchant") && userRole !== "MERCHANT") { return NextResponse.redirect( - new URL(ROLE_DASHBOARDS[userRole!] || "/", request.url), + new URL((userRole && ROLE_DASHBOARDS[userRole]) || "/", request.url), ); } if (path.startsWith("/admin") && userRole !== "SUPER_ADMIN") { return NextResponse.redirect( - new URL(ROLE_DASHBOARDS[userRole!] || "/", request.url), + new URL((userRole && ROLE_DASHBOARDS[userRole]) || "/", request.url), ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/middleware.ts` around lines 102 - 119, The non-null assertions on userRole in the role gate redirects are incorrect because userRole can be undefined; update the redirect target lookup to safely handle a missing or unknown role by resolving a dashboard with a safe lookup (e.g., compute a dashboard variable using ROLE_DASHBOARDS[userRole] ?? "/" or ROLE_DASHBOARDS[userRole ?? ""] ?? "/") and use that variable in the NextResponse.redirect calls in the middleware where userRole is referenced (symbols: userRole, ROLE_DASHBOARDS, NextResponse.redirect).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/middleware.ts`:
- Around line 58-64: ROLE_DASHBOARDS is missing mappings for several entries
from the UserRole enum (OPERATOR, SUPPORT, SUPPLIER, ADMIN) so users with those
roles fall back to "/"—update ROLE_DASHBOARDS to include keys for OPERATOR,
SUPPORT, SUPPLIER, and ADMIN with the correct dashboard paths (e.g.,
"/operator/dashboard", "/support/dashboard" or your app's equivalent) and ensure
the Role type (type Role = keyof typeof ROLE_DASHBOARDS) still covers them;
adjust any middleware logic that reads ROLE_DASHBOARDS to rely on these new
keys.
- Around line 4-24: The decodeJwt function currently trusts an unverified JWT
payload and does raw Base64URL decoding without padding, which is a security
risk and can fail for some tokens; replace decodeJwt(token) usages with a proper
verification flow: either use the Edge-compatible jose.jwtVerify to validate
signature and claims (iss/aud/exp) before extracting the payload, or call your
backend/session validation endpoint for sensitive routes, and if you must keep a
lightweight decoder, implement Base64URL padding correction (add '=' until
length % 4 === 0) and explicitly mark the function as non-authoritative in
comments so it’s only used for non-security decisions.
- Around line 76-78: The middleware is reading the wrong cookie name
("hwos_access_token"), so update the token lookup to use the backend cookie name
"twizrr_access_token": change the code that sets token
(cookies.get("hwos_access_token")?.value) to
cookies.get("twizrr_access_token")?.value so decodeJwt(payload) and userRole
resolution (payload?.role as Role | undefined) work with the actual auth cookie
used by the backend.
---
Nitpick comments:
In `@apps/web/src/middleware.ts`:
- Around line 102-119: The non-null assertions on userRole in the role gate
redirects are incorrect because userRole can be undefined; update the redirect
target lookup to safely handle a missing or unknown role by resolving a
dashboard with a safe lookup (e.g., compute a dashboard variable using
ROLE_DASHBOARDS[userRole] ?? "/" or ROLE_DASHBOARDS[userRole ?? ""] ?? "/") and
use that variable in the NextResponse.redirect calls in the middleware where
userRole is referenced (symbols: userRole, ROLE_DASHBOARDS,
NextResponse.redirect).
🪄 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: 07d22eb7-62ec-4ceb-9ae4-118fa95910ea
📒 Files selected for processing (1)
apps/web/src/middleware.ts
| /** | ||
| * UTILITY: Decode JWT Payload without verification | ||
| * JWT structure: header.payload.signature | ||
| */ | ||
| function decodeJwt(token: string) { | ||
| try { | ||
| const parts = token.split("."); | ||
| if (parts.length !== 3) return null; | ||
|
|
||
| // Base64URL to Base64 | ||
| const base64Url = parts[1]; | ||
| const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); | ||
|
|
||
| // Decode and Parse | ||
| const payload = JSON.parse(atob(base64)); | ||
| return payload; | ||
| } catch (error) { | ||
| console.error("Middleware JWT decode error:", error); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Security risk: JWT decoded without signature verification.
The middleware trusts the JWT payload without verifying the signature. A malicious user could craft a token with an arbitrary role (e.g., SUPER_ADMIN) and gain unauthorized access to protected routes. While client-side JWT verification in Edge middleware has limitations, consider:
- Using
joselibrary'sjwtVerifywhich works in Edge runtime - Relying on a backend session/token validation endpoint for sensitive routes
- At minimum, document this as a defense-in-depth layer, not the sole authorization mechanism
Additionally, the base64 decoding may fail for some tokens due to missing padding:
🔒 Proposed fix for base64 padding
// Base64URL to Base64
const base64Url = parts[1];
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
+ let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
+ // Add padding if needed
+ const pad = base64.length % 4;
+ if (pad) {
+ base64 += "=".repeat(4 - pad);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * UTILITY: Decode JWT Payload without verification | |
| * JWT structure: header.payload.signature | |
| */ | |
| function decodeJwt(token: string) { | |
| try { | |
| const parts = token.split("."); | |
| if (parts.length !== 3) return null; | |
| // Base64URL to Base64 | |
| const base64Url = parts[1]; | |
| const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); | |
| // Decode and Parse | |
| const payload = JSON.parse(atob(base64)); | |
| return payload; | |
| } catch (error) { | |
| console.error("Middleware JWT decode error:", error); | |
| return null; | |
| } | |
| } | |
| /** | |
| * UTILITY: Decode JWT Payload without verification | |
| * JWT structure: header.payload.signature | |
| */ | |
| function decodeJwt(token: string) { | |
| try { | |
| const parts = token.split("."); | |
| if (parts.length !== 3) return null; | |
| // Base64URL to Base64 | |
| const base64Url = parts[1]; | |
| let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); | |
| // Add padding if needed | |
| const pad = base64.length % 4; | |
| if (pad) { | |
| base64 += "=".repeat(4 - pad); | |
| } | |
| // Decode and Parse | |
| const payload = JSON.parse(atob(base64)); | |
| return payload; | |
| } catch (error) { | |
| console.error("Middleware JWT decode error:", error); | |
| return null; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/middleware.ts` around lines 4 - 24, The decodeJwt function
currently trusts an unverified JWT payload and does raw Base64URL decoding
without padding, which is a security risk and can fail for some tokens; replace
decodeJwt(token) usages with a proper verification flow: either use the
Edge-compatible jose.jwtVerify to validate signature and claims (iss/aud/exp)
before extracting the payload, or call your backend/session validation endpoint
for sensitive routes, and if you must keep a lightweight decoder, implement
Base64URL padding correction (add '=' until length % 4 === 0) and explicitly
mark the function as non-authoritative in comments so it’s only used for
non-security decisions.
| const ROLE_DASHBOARDS = { | ||
| BUYER: "/buyer/feed", | ||
| MERCHANT: "/merchant/dashboard", | ||
| SUPER_ADMIN: "/admin/dashboard", | ||
| }; | ||
|
|
||
| type Role = keyof typeof ROLE_DASHBOARDS; |
There was a problem hiding this comment.
Missing dashboard mappings for other roles.
The UserRole enum includes OPERATOR, SUPPORT, SUPPLIER, and ADMIN (per packages/shared/src/enums/user-role.enum.ts), but ROLE_DASHBOARDS only maps three roles. Users with unmapped roles will be redirected to / which may not be the intended behavior.
💡 Suggested addition for completeness
const ROLE_DASHBOARDS = {
BUYER: "/buyer/feed",
MERCHANT: "/merchant/dashboard",
SUPER_ADMIN: "/admin/dashboard",
+ ADMIN: "/admin/dashboard",
+ OPERATOR: "/admin/dashboard",
+ SUPPORT: "/admin/dashboard",
+ SUPPLIER: "/merchant/dashboard",
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ROLE_DASHBOARDS = { | |
| BUYER: "/buyer/feed", | |
| MERCHANT: "/merchant/dashboard", | |
| SUPER_ADMIN: "/admin/dashboard", | |
| }; | |
| type Role = keyof typeof ROLE_DASHBOARDS; | |
| const ROLE_DASHBOARDS = { | |
| BUYER: "/buyer/feed", | |
| MERCHANT: "/merchant/dashboard", | |
| SUPER_ADMIN: "/admin/dashboard", | |
| ADMIN: "/admin/dashboard", | |
| OPERATOR: "/admin/dashboard", | |
| SUPPORT: "/admin/dashboard", | |
| SUPPLIER: "/merchant/dashboard", | |
| }; | |
| type Role = keyof typeof ROLE_DASHBOARDS; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/middleware.ts` around lines 58 - 64, ROLE_DASHBOARDS is missing
mappings for several entries from the UserRole enum (OPERATOR, SUPPORT,
SUPPLIER, ADMIN) so users with those roles fall back to "/"—update
ROLE_DASHBOARDS to include keys for OPERATOR, SUPPORT, SUPPLIER, and ADMIN with
the correct dashboard paths (e.g., "/operator/dashboard", "/support/dashboard"
or your app's equivalent) and ensure the Role type (type Role = keyof typeof
ROLE_DASHBOARDS) still covers them; adjust any middleware logic that reads
ROLE_DASHBOARDS to rely on these new keys.
| const token = cookies.get("hwos_access_token")?.value; | ||
| const payload = token ? decodeJwt(token) : null; | ||
| const userRole = payload?.role as Role | undefined; |
There was a problem hiding this comment.
Critical: Cookie name mismatch breaks authentication.
The middleware reads from hwos_access_token, but the backend (apps/backend/src/modules/auth/auth.controller.ts:41) sets the cookie as twizrr_access_token. This mismatch means the middleware will never find the auth token, causing all authenticated users to be treated as unauthenticated.
🐛 Fix the cookie name
- const token = cookies.get("hwos_access_token")?.value;
+ const token = cookies.get("twizrr_access_token")?.value;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/middleware.ts` around lines 76 - 78, The middleware is reading
the wrong cookie name ("hwos_access_token"), so update the token lookup to use
the backend cookie name "twizrr_access_token": change the code that sets token
(cookies.get("hwos_access_token")?.value) to
cookies.get("twizrr_access_token")?.value so decodeJwt(payload) and userRole
resolution (payload?.role as Role | undefined) work with the actual auth cookie
used by the backend.
Summary by CodeRabbit