Skip to content

feat: server-side middleware for auth redirects and role gates#249

Merged
onerandomdevv merged 1 commit into
devfrom
feat/middleware
Apr 5, 2026
Merged

feat: server-side middleware for auth redirects and role gates#249
onerandomdevv merged 1 commit into
devfrom
feat/middleware

Conversation

@onerandomdevv
Copy link
Copy Markdown
Collaborator

@onerandomdevv onerandomdevv commented Apr 5, 2026

Summary by CodeRabbit

  • New Features
    • Added authentication enforcement across protected sections of the app
    • Implemented role-based access control with automatic routing to appropriate dashboards
    • Unauthenticated users are redirected to login when accessing restricted areas
    • Authenticated users are redirected away from login/registration pages

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

A new Next.js middleware is introduced that enforces authentication and role-based access control by inspecting JWT tokens from the hwos_access_token cookie. It redirects unauthenticated users to login, prevents authenticated users from accessing auth pages, and gates access to role-specific routes based on user roles.

Changes

Cohort / File(s) Summary
Authentication & Authorization Middleware
apps/web/src/middleware.ts
New middleware implementation with JWT token validation, user role extraction, conditional redirects for unauthenticated/authenticated users, role-based route gating for /buyer, /merchant, /admin paths, error handling for JWT decode failures, and matcher configuration for all non-static routes.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A guardian of paths hops through the code,
JWT tokens light its authentication road,
Roles are checked with care so keen,
Redirects guide users to where they've been,
Security blossoms—a carrot-earned treat! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely missing. The template requires multiple sections including 'What does this PR do', module selection, type of change, and a comprehensive checklist. Add a complete pull request description following the template: include brief explanation of middleware functionality, select 'Auth' and 'Frontend' modules, mark 'New feature' as the change type, and complete all applicable checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: introducing server-side middleware for authentication redirects and role-based access control, which matches the changeset's core functionality.

✏️ 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/middleware

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

🧹 Nitpick comments (1)
apps/web/src/middleware.ts (1)

102-119: Incorrect non-null assertions on userRole.

The non-null assertion userRole! is used on lines 105, 111, and 117, but userRole can be undefined (e.g., if the JWT lacks a role field 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0f5f6 and f75e510.

📒 Files selected for processing (1)
  • apps/web/src/middleware.ts

Comment on lines +4 to +24
/**
* 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;
}
}
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

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:

  1. Using jose library's jwtVerify which works in Edge runtime
  2. Relying on a backend session/token validation endpoint for sensitive routes
  3. 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.

Suggested change
/**
* 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.

Comment on lines +58 to +64
const ROLE_DASHBOARDS = {
BUYER: "/buyer/feed",
MERCHANT: "/merchant/dashboard",
SUPER_ADMIN: "/admin/dashboard",
};

type Role = keyof typeof ROLE_DASHBOARDS;
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 | 🟡 Minor

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.

Suggested change
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.

Comment on lines +76 to +78
const token = cookies.get("hwos_access_token")?.value;
const payload = token ? decodeJwt(token) : null;
const userRole = payload?.role as Role | undefined;
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 | 🔴 Critical

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.

@onerandomdevv onerandomdevv merged commit 673e594 into dev Apr 5, 2026
4 checks passed
@onerandomdevv onerandomdevv deleted the feat/middleware branch April 5, 2026 23:22
@coderabbitai coderabbitai Bot mentioned this pull request May 21, 2026
14 tasks
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