Skip to content
This repository was archived by the owner on Jun 20, 2026. It is now read-only.

Claude/analyze test coverage 9wv6 y#60

Merged
support371 merged 6 commits into
mainfrom
claude/analyze-test-coverage-9wv6Y
Mar 12, 2026
Merged

Claude/analyze test coverage 9wv6 y#60
support371 merged 6 commits into
mainfrom
claude/analyze-test-coverage-9wv6Y

Conversation

@support371

@support371 support371 commented Mar 11, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Introduce a preview-mode authenticated client portal with role-based access control, audit logging, and supporting UI/navigation for multiple portal sections.

New Features:

  • Add a login flow with preview user accounts and session cookie-based authentication for the client portal.
  • Implement a protected /portal area with dashboard, services, cybersecurity, real estate, wealth, legal, requests, users, and audit views backed by preview data.
  • Introduce API endpoints for login, logout, session info, dashboard data, and exporting audit logs as CSV.

Enhancements:

  • Add role-based route permissions and navigation filtering for portal sections using a centralized RBAC configuration.
  • Add middleware to enforce authentication and authorization on /portal routes and redirect unauthorized users with contextual messaging.
  • Add local file–based audit logging to track authentication events and portal actions.
  • Add dashboard-to-portal and legal convenience redirects in Next.js routing configuration.

Open with Devin

claude added 4 commits March 11, 2026 18:50
- Add /login page with email/password authentication
- Add /portal/* authenticated route tree with all modules:
  dashboard, services, cybersecurity (incidents/monitoring/compliance),
  real-estate (deals/documents), wealth (investments/retirement/qfs),
  legal (poa/estate), requests, users, audit
- Add src/lib/auth with session management, RBAC, and preview users
- Add src/lib/audit with file-based audit logging
- Add portal API routes: auth/login, auth/logout, auth/session,
  portal/dashboard, portal/audit/export
- Add middleware.ts protecting /portal/* with auth + RBAC
- Add portal components: Sidebar, Topbar, Toast

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo
…e routes

- Add 17 dashboard→portal permanent redirects
- Add /privacy, /terms, /security convenience redirects
- Preserves all existing public route redirects

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo
- Remove package-lock.json (pnpm-only workflow)
- Add package-lock.json and data/audit-log.json to .gitignore
- pnpm-lock.yaml already tracked

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo
…audit for denied routes

- Extract PortalRole/PortalSession types to src/lib/auth/types.ts so
  client components (Sidebar) don't pull in node:crypto via session.ts
- Fix logout route to derive redirect URL from request.url instead of
  hardcoded localhost fallback
- Add denied-route audit logging when middleware redirects to /portal?denied=1
- Add metadata exports for /login and /portal layouts
- Update all auth module imports to use types.ts for type-only imports

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo
@vercel

vercel Bot commented Mar 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
gem-assist-joi5 Error Error Mar 12, 2026 7:38am
gemassistenterprise Error Error Mar 12, 2026 7:38am
my-web Ready Ready Preview, Comment Mar 12, 2026 7:38am
mymain-emterprise-website Error Error Mar 12, 2026 7:38am
mymain-emterprise-website-gerf Error Error Mar 12, 2026 7:38am
mymain-emterprise-website-h5el Error Error Mar 12, 2026 7:38am

@sourcery-ai

sourcery-ai Bot commented Mar 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a preview-mode, role-based authenticated client portal under /portal with login/logout, middleware-based session + RBAC enforcement, local file–backed audit logging (including CSV export API), session/dashboard APIs, and a suite of mock data–driven portal pages; also wires legacy dashboard/legal URLs to the new portal routes via Next.js redirects.

Sequence diagram for portal login with session cookie and audit logging

sequenceDiagram
    actor User
    participant LoginPage as LoginPage_next_app
    participant AuthAPI as API_auth_login
    participant AuthUsers as auth_users
    participant Session as auth_session
    participant Audit as audit_module
    participant Browser as Browser_cookies

    User->>LoginPage: Submit email,password
    LoginPage->>AuthAPI: POST /api/auth/login { email,password }
    AuthAPI->>AuthUsers: authenticatePortalUser(email,password)
    alt valid_credentials
        AuthUsers-->>AuthAPI: user
        AuthAPI->>Session: createPortalToken(sessionData)
        Session-->>AuthAPI: token
        AuthAPI->>Audit: writeAuditEntry(login_success)
        Audit-->>AuthAPI: AuditEntry
        AuthAPI->>Browser: Set-Cookie gem_portal_session=token
        AuthAPI-->>LoginPage: 200 { success,role,name }
        LoginPage->>Browser: router.push(nextUrl)
        LoginPage->>Browser: router.refresh()
    else invalid_credentials
        AuthUsers-->>AuthAPI: null
        AuthAPI->>Audit: writeAuditEntry(login_failed)
        Audit-->>AuthAPI: AuditEntry
        AuthAPI-->>LoginPage: 401 { message Invalid credentials }
        LoginPage-->>User: Show error Authentication failed
    end
Loading

Sequence diagram for protected portal route access with RBAC and denied feedback

sequenceDiagram
    actor User
    participant Browser
    participant Middleware as Next_middleware
    participant Session as auth_session
    participant RBAC as auth_rbac
    participant PortalLayout as Portal_layout
    participant PortalPage as Portal_page
    participant Audit as audit_module
    participant Toast as Toast_component

    User->>Browser: Navigate to /portal/some_route
    Browser->>Middleware: HTTP request /portal/some_route
    Middleware->>Session: readPortalToken(cookie)
    Session-->>Middleware: session or null
    alt no_session
        Middleware->>Browser: Redirect /login?next=/portal/some_route
    else has_session
        Middleware->>RBAC: isRouteAllowed(pathname,session.role)
        alt route_allowed
            RBAC-->>Middleware: true
            Middleware->>Browser: NextResponse.next()
            Browser->>PortalLayout: Render /portal layout
            PortalLayout->>Session: getCurrentPortalSession()
            Session-->>PortalLayout: session
            PortalLayout->>PortalPage: Render child page
        else route_denied
            RBAC-->>Middleware: false
            Middleware->>Browser: Redirect /portal?denied=1
            Browser->>PortalLayout: Render /portal?denied=1
            PortalLayout->>Session: getCurrentPortalSession()
            Session-->>PortalLayout: session
            PortalLayout->>PortalPage: Render portal dashboard
            PortalPage->>Audit: writeAuditEntry(route_denied)
            Audit-->>PortalPage: AuditEntry
            PortalLayout->>Toast: Render Toast
            Toast-->>User: Show Access denied toast
        end
    end
Loading

Entity relationship diagram for portal users and audit entries

erDiagram
    PortalUser {
      string id
      string email
      string name
      string role
      string passwordHash
      boolean isActive
    }

    AuditEntry {
      string id
      string timestamp
      string actorUserId
      string actorEmail
      string action
      string target
      string result
      json meta
    }

    PortalUser ||--o{ AuditEntry : generates
Loading

Class diagram for portal auth and audit modules

classDiagram
    class PortalSession {
      string userId
      string email
      string name
      PortalRole role
      number exp
    }

    class PortalUser {
      string id
      string email
      string name
      PortalRole role
      string passwordHash
      boolean isActive
    }

    class AuditEntry {
      string id
      string timestamp
      string actorUserId
      string actorEmail
      string action
      string target
      Record_meta meta
      string result
    }

    class AuthTypes {
      <<module>>
      PortalRole
      PortalSession
    }

    class AuthSession {
      <<module>>
      +string PORTAL_COOKIE
      +string authSecret()
      +string sign(payload)
      +string createPortalToken(session)
      +PortalSession readPortalToken(token)
      +PortalSession getCurrentPortalSession()
    }

    class AuthUsers {
      <<module>>
      +PortalUser[] previewUsers
      +PortalUser authenticatePortalUser(email,password)
      +PortalUser[] listPortalUsers()
      +PortalUser getPortalUser(id)
    }

    class AuthRBAC {
      <<module>>
      +Record_routePermissions routePermissions
      +boolean isRouteAllowed(pathname,role)
      +NavItem[] getAllowedNavItems(role)
    }

    class AuditModule {
      <<module>>
      +string AUDIT_FILE
      +AuditEntry writeAuditEntry(entry)
      +AuditEntry[] getAuditEntries(opts)
      +AuditEntry[] exportAuditLog()
    }

    class NavItem {
      string href
      string label
      PortalRole[] roles
    }

    class MiddlewareModule {
      <<middleware>>
      +middleware(request)
      +config matcher
    }

    class ApiAuthLoginRoute {
      <<route>>
      +POST(request)
    }

    class ApiAuthLogoutRoute {
      <<route>>
      +POST(request)
    }

    class ApiAuthSessionRoute {
      <<route>>
      +GET()
    }

    class ApiPortalDashboardRoute {
      <<route>>
      +GET()
    }

    class ApiPortalAuditExportRoute {
      <<route>>
      +GET()
    }

    AuthTypes <|-- PortalSession
    AuthTypes <|-- PortalRole

    AuthSession --> PortalSession
    AuthSession --> PortalRole

    AuthUsers --> PortalUser
    AuthUsers --> PortalRole

    AuthRBAC --> PortalRole
    AuthRBAC --> NavItem

    AuditModule --> AuditEntry

    MiddlewareModule --> AuthSession
    MiddlewareModule --> AuthRBAC

    ApiAuthLoginRoute --> AuthUsers
    ApiAuthLoginRoute --> AuthSession
    ApiAuthLoginRoute --> AuditModule

    ApiAuthLogoutRoute --> AuthSession
    ApiAuthLogoutRoute --> AuditModule

    ApiAuthSessionRoute --> AuthSession

    ApiPortalDashboardRoute --> AuthSession

    ApiPortalAuditExportRoute --> AuthSession
    ApiPortalAuditExportRoute --> AuditModule
Loading

Architecture flow diagram for portal routing, middleware, and APIs

flowchart LR
    subgraph BrowserLayer[Browser]
      B_User["User browser"]
    end

    subgraph NextRouter[Next_js_routing]
      R_Redirects["next.config redirects<br>/dashboard -> /portal<br>/privacy -> /legal/privacy-policy"]
      R_Middleware["middleware<br>protect /portal/*"]
    end

    subgraph PortalApp[Portal_application]
      L_LoginPage["/login page"]
      L_PortalLayout["/portal layout<br>Topbar + Sidebar + Toast"]
      P_Pages["Portal child pages<br>Dashboard, Services, Cybersecurity,<br>RealEstate, Wealth, Legal,<br>Requests, Users, Audit"]
    end

    subgraph AuthLib[Auth_library]
      A_Session["auth_session<br>createPortalToken<br>readPortalToken<br>getCurrentPortalSession"]
      A_RBAC["auth_rbac<br>isRouteAllowed<br>getAllowedNavItems"]
      A_Users["auth_users<br>previewUsers store"]
    end

    subgraph AuditLib[Audit_library]
      AU_Module["audit_module<br>writeAuditEntry<br>getAuditEntries<br>exportAuditLog"]
      AU_File["audit-log.json<br>local file store"]
    end

    subgraph ApiRoutes[API_routes]
      API_Login["POST /api/auth/login"]
      API_Logout["POST /api/auth/logout"]
      API_Session["GET /api/auth/session"]
      API_Dashboard["GET /api/portal/dashboard"]
      API_AuditExport["GET /api/portal/audit/export"]
    end

    B_User --> R_Redirects
    R_Redirects --> R_Middleware

    R_Middleware --> L_LoginPage
    R_Middleware --> L_PortalLayout

    L_PortalLayout --> P_Pages

    B_User --> API_Login
    B_User --> API_Logout
    B_User --> API_Session
    B_User --> API_Dashboard
    B_User --> API_AuditExport

    API_Login --> A_Users
    API_Login --> A_Session
    API_Login --> AU_Module

    API_Logout --> A_Session
    API_Logout --> AU_Module

    API_Session --> A_Session

    API_Dashboard --> A_Session

    API_AuditExport --> A_Session
    API_AuditExport --> AU_Module

    AU_Module --> AU_File

    R_Middleware --> A_Session
    R_Middleware --> A_RBAC

    L_PortalLayout --> A_Session
    P_Pages --> AU_Module
Loading

File-Level Changes

Change Details Files
Add preview-mode portal authentication, session handling, and RBAC utilities.
  • Introduce a PortalSession/PortalRole type model and HMAC-signed session token utilities with cookie integration.
  • Implement a preview in-memory user store with SHA-256 password hashing and helpers to authenticate and list users.
  • Add a route permission matrix and helpers for per-path authorization plus role-aware navigation item filtering.
src/lib/auth/types.ts
src/lib/auth/session.ts
src/lib/auth/users.ts
src/lib/auth/rbac.ts
src/lib/auth/index.ts
Protect /portal routes with middleware that enforces authentication and route-level authorization.
  • Read and validate the portal session cookie for incoming /portal requests and redirect unauthenticated users to /login with a next parameter.
  • Check the user role against the route permission matrix and redirect unauthorized portal requests back to /portal with a denied flag.
  • Configure Next.js middleware matcher to scope checks to /portal paths only.
middleware.ts
Implement login/logout flows and session/audience APIs for the portal.
  • Create a login page with a client-side form that posts to an auth API endpoint, handles error states, and redirects to the requested next URL on success.
  • Expose /api/auth/login and /api/auth/logout handlers that authenticate users, issue/clear signed session cookies, and write audit entries for login/logout events.
  • Add /api/auth/session and /api/portal/dashboard endpoints to expose current session metadata and mock dashboard data to authenticated callers.
src/app/login/page.tsx
src/app/login/layout.tsx
src/app/api/auth/login/route.ts
src/app/api/auth/logout/route.ts
src/app/api/auth/session/route.ts
src/app/api/portal/dashboard/route.ts
Add local file–based audit logging with a CSV export API and UI table.
  • Implement an audit log module that persists entries to a JSON file with simple append, filtering, and export helpers, capped at a fixed history.
  • Wire audit writes into login success/failure, logout, and access-denied flows, including route_denied events triggered by middleware redirects.
  • Expose /api/portal/audit/export for admin users to download the audit log as CSV and render a portal Audit Log page that lists recent entries and links to the export API.
src/lib/audit/index.ts
src/app/portal/audit/page.tsx
src/app/api/portal/audit/export/route.ts
src/app/portal/page.tsx
Create the portal shell (layout, navigation, toast) and main feature pages backed by mock data.
  • Introduce a /portal layout that requires a valid portal session, renders a top bar with user info, a role-filtered sidebar, and a toast surface, then child content.
  • Build a suite of portal pages for dashboard, services, cybersecurity (index, incidents, monitoring, compliance), real estate (index, deals, documents), wealth (investments, retirement, QFS), legal (POA, estate), requests, and users using static/mock data.
  • Add client-side components for the sidebar navigation, header/topbar with logout button, and an access-denied toast driven by the denied query string.
src/app/portal/layout.tsx
src/app/portal/page.tsx
src/app/portal/services/page.tsx
src/app/portal/cybersecurity/page.tsx
src/app/portal/cybersecurity/incidents/page.tsx
src/app/portal/cybersecurity/monitoring/page.tsx
src/app/portal/cybersecurity/compliance/page.tsx
src/app/portal/real-estate/page.tsx
src/app/portal/real-estate/deals/page.tsx
src/app/portal/real-estate/documents/page.tsx
src/app/portal/wealth/investments/page.tsx
src/app/portal/wealth/retirement/page.tsx
src/app/portal/wealth/qfs/page.tsx
src/app/portal/legal/poa/page.tsx
src/app/portal/legal/estate/page.tsx
src/app/portal/requests/page.tsx
src/app/portal/users/page.tsx
src/components/portal/Topbar.tsx
src/components/portal/Sidebar.tsx
src/components/portal/Toast.tsx
Add redirects from legacy dashboard and legal URLs into the new portal and legal routes.
  • Extend Next.js redirects configuration to map legacy /dashboard/* paths to their /portal equivalents.
  • Add convenience redirects for /privacy, /terms, and /security to the corresponding legal/services pages.
next.config.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@support371
support371 marked this pull request as ready for review March 11, 2026 21:13

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In readPortalToken, crypto.timingSafeEqual will throw if the provided signature length differs from the expected one; consider checking the lengths first and early-returning null to avoid runtime errors on malformed tokens.
  • The CSV export in src/app/api/portal/audit/export/route.ts joins raw values with commas without quoting or escaping, which will break if any field (e.g., target or action) contains commas, newlines, or quotes; wrapping and escaping fields according to CSV rules would make the export more robust.
  • The file-based audit log in src/lib/audit/index.ts uses synchronous fs operations and no locking, which can behave poorly under concurrent requests or in serverless environments; switching to async I/O and/or a more concurrency-friendly storage mechanism (even for preview mode) would make this more reliable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `readPortalToken`, `crypto.timingSafeEqual` will throw if the provided signature length differs from the expected one; consider checking the lengths first and early-returning `null` to avoid runtime errors on malformed tokens.
- The CSV export in `src/app/api/portal/audit/export/route.ts` joins raw values with commas without quoting or escaping, which will break if any field (e.g., `target` or `action`) contains commas, newlines, or quotes; wrapping and escaping fields according to CSV rules would make the export more robust.
- The file-based audit log in `src/lib/audit/index.ts` uses synchronous fs operations and no locking, which can behave poorly under concurrent requests or in serverless environments; switching to async I/O and/or a more concurrency-friendly storage mechanism (even for preview mode) would make this more reliable.

## Individual Comments

### Comment 1
<location path="src/app/api/portal/audit/export/route.ts" line_range="22-25" />
<code_context>
+    result: 'success',
+  });
+
+  const header = 'id,timestamp,actorEmail,action,target,result\n';
+  const rows = entries.map((e) =>
+    [e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result].join(',')
+  ).join('\n');
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Escape CSV fields to handle commas, quotes, or newlines safely in audit data.

Using `join(',')` means any commas, quotes, or newlines in fields (now or in future fields like `meta`) will corrupt the CSV and can enable CSV injection when opened in spreadsheets.

Wrap each field with proper CSV escaping instead, for example:

```ts
const escape = (value: string) => {
  const v = String(value).replace(/"/g, '""');
  return `"${v}"`;
};

const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries
  .map((e) => [e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result]
    .map(escape)
    .join(','))
  .join('\n');
```

This makes the CSV robust and safer to consume in spreadsheet tools.

```suggestion
  const escapeCsv = (value: unknown) => {
    const v = String(value).replace(/"/g, '""');
    return `"${v}"`;
  };

  const header = 'id,timestamp,actorEmail,action,target,result\n';
  const rows = entries
    .map((e) =>
      [e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result]
        .map(escapeCsv)
        .join(',')
    )
    .join('\n');
```
</issue_to_address>

### Comment 2
<location path="src/app/portal/page.tsx" line_range="4-10" />
<code_context>
+
+export default async function PortalDashboard({
+  searchParams,
+}: {
+  searchParams: Promise<Record<string, string | string[] | undefined>>;
+}) {
+  const session = await getCurrentPortalSession();
</code_context>
<issue_to_address>
**suggestion:** Type `searchParams` as the concrete object instead of a Promise to better match Next’s route signature.

Here `searchParams` is typed as `Promise<...>` and then awaited, but Next.js route components receive it as a plain object. This doesn’t break runtime behavior, but it misrepresents the API and can cause type-checking friction.

Type it as a plain object instead, e.g.:

```ts
export default async function PortalDashboard({
  searchParams,
}: {
  searchParams: Record<string, string | string[] | undefined>;
}) {
  // use searchParams directly
}
```

```suggestion
export default async function PortalDashboard({
  searchParams,
}: {
  searchParams: Record<string, string | string[] | undefined>;
}) {
  const session = await getCurrentPortalSession();
  const params = searchParams;
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +22 to +25
const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries.map((e) =>
[e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result].join(',')
).join('\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 suggestion (security): Escape CSV fields to handle commas, quotes, or newlines safely in audit data.

Using join(',') means any commas, quotes, or newlines in fields (now or in future fields like meta) will corrupt the CSV and can enable CSV injection when opened in spreadsheets.

Wrap each field with proper CSV escaping instead, for example:

const escape = (value: string) => {
  const v = String(value).replace(/"/g, '""');
  return `"${v}"`;
};

const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries
  .map((e) => [e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result]
    .map(escape)
    .join(','))
  .join('\n');

This makes the CSV robust and safer to consume in spreadsheet tools.

Suggested change
const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries.map((e) =>
[e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result].join(',')
).join('\n');
const escapeCsv = (value: unknown) => {
const v = String(value).replace(/"/g, '""');
return `"${v}"`;
};
const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries
.map((e) =>
[e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result]
.map(escapeCsv)
.join(',')
)
.join('\n');

Comment thread src/app/portal/page.tsx

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread src/lib/auth/session.ts
if (!encoded || !signature) return null;

const expected = sign(encoded);
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 timingSafeEqual crashes on malformed token signatures with mismatched buffer lengths

crypto.timingSafeEqual() at src/lib/auth/session.ts:32 throws RangeError: Input buffers must have the same byte length when the signature portion of the token is not exactly 64 hex characters. The HMAC-SHA256 expected digest is always 64 hex chars, but an attacker-supplied or corrupted cookie can have a signature of any length. Since there's no try/catch around this call in readPortalToken, the exception propagates to all callers: the middleware (middleware.ts:15), getCurrentPortalSession (session.ts:48), and every route handler using it. This results in a 500 Internal Server Error for any /portal/* request when the gem_portal_session cookie contains a syntactically valid (has a .) but non-standard-length signature.

Suggested change
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/app/api/portal/audit/export/route.ts
<aside className="bg-slate-900 border border-slate-800 rounded-xl p-3 h-fit sticky top-20">
<nav className="space-y-1">
{items.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Sidebar 'Dashboard' nav item is always highlighted as active on every portal sub-page

In src/components/portal/Sidebar.tsx:20, the isActive check is pathname === item.href || pathname.startsWith(item.href + '/'). For the Dashboard item with href='/portal', this means pathname.startsWith('/portal/') is true for every portal sub-route (e.g., /portal/cybersecurity, /portal/audit). As a result, the Dashboard link always has the active styling alongside whichever other nav item is actually active, making the navigation state misleading.

Suggested change
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
const isActive = item.href === '/portal' ? pathname === '/portal' : (pathname === item.href || pathname.startsWith(item.href + '/'));
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63648f4091

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/auth/session.ts
if (!encoded || !signature) return null;

const expected = sign(encoded);
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard timingSafeEqual against mismatched signature lengths

readPortalToken passes user-controlled cookie data directly into crypto.timingSafeEqual, which throws if the two buffers have different lengths. A malformed gem_portal_session cookie (truncated/extended signature) will therefore raise instead of returning null, causing middleware/API auth checks to fail with 500 rather than treating the request as unauthenticated.

Useful? React with 👍 / 👎.


const header = 'id,timestamp,actorEmail,action,target,result\n';
const rows = entries.map((e) =>
[e.id, e.timestamp, e.actorEmail || '', e.action, e.target, e.result].join(',')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Escape audit CSV fields before concatenating rows

The export endpoint emits CSV rows by joining raw values with commas, but actorEmail is attacker-controlled via failed login attempts and can include commas, newlines, or spreadsheet formulas. That allows a malicious login payload to poison exported files (row/column corruption) and can trigger formula execution when an admin opens the CSV in Excel/Sheets.

Useful? React with 👍 / 👎.

});
}

const response = NextResponse.redirect(new URL('/login', request.url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use See Other redirect for logout POST handler

NextResponse.redirect defaults to HTTP 307, which preserves the POST method. Submitting the logout form therefore redirects as POST /login instead of GET /login, which can surface a 405 page after logout on standard page routes; returning a 303/302 avoids this by forcing a GET navigation.

Useful? React with 👍 / 👎.

- Replace filesystem audit logging (fs/path) with in-memory store
- Replace middleware with edge-safe version (no crypto/fs imports)
- Add metadata export to src/app/login/page.tsx and src/app/portal/page.tsx
- Extract login form into client component so page.tsx can be a server
  component with metadata
- tsconfig path alias @/* -> src/* already correct
- Auth already reads process.env.PORTAL_AUTH_SECRET

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment thread middleware.ts Outdated
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
const session = req.cookies.get("portal_session") ?? req.cookies.get("gem_portal_session");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Middleware checks non-existent cookie name 'portal_session', allowing trivial auth guard bypass

The middleware at middleware.ts:5 checks for "portal_session" first via the ?? operator, but the actual cookie set by the login API is "gem_portal_session" (defined as PORTAL_COOKIE in src/lib/auth/session.ts:7). A cookie named "portal_session" is never set by the application. An unauthenticated user can set a dummy portal_session=anything cookie in their browser, which makes the ?? short-circuit with a truthy cookie object, bypassing the middleware's redirect-to-login guard entirely. While the server-side layout performs a secondary session check, this renders the middleware protection ineffective.

Suggested change
const session = req.cookies.get("portal_session") ?? req.cookies.get("gem_portal_session");
const session = req.cookies.get("gem_portal_session");
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/app/portal/layout.tsx
Comment on lines +15 to +20
export default async function PortalLayout({ children }: { children: ReactNode }) {
const session = await getCurrentPortalSession();

if (!session) {
redirect('/login');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 RBAC route permissions are defined but never enforced — any authenticated user can access admin-only pages

The isRouteAllowed function in src/lib/auth/rbac.ts:24 defines a role-based permission matrix (e.g., /portal/audit and /portal/users are admin-only), but it is never called anywhere in the codebase — not in the middleware, not in the portal layout, and not in individual page components. This means any authenticated user (including viewer role) can directly access admin-only pages like /portal/audit (which displays the full audit log) and /portal/users (which lists all users). The sidebar correctly hides these links via getAllowedNavItems, but the routes themselves are unprotected.

Prompt for agents
In src/app/portal/layout.tsx, after obtaining the session (line 16), add an RBAC check before rendering children. Import isRouteAllowed from @/lib/auth/rbac and the headers function to get the current pathname (or pass it another way). If isRouteAllowed(pathname, session.role) returns false, redirect the user to /portal?denied=1. Alternatively, add this check in the middleware.ts by decoding and validating the token and calling isRouteAllowed there. Every portal sub-page that has restricted access in the routePermissions map (e.g., /portal/audit, /portal/users, /portal/cybersecurity, /portal/wealth/qfs) must be protected.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Replace audit logger with in-memory store (no fs/path imports)
- Simplify middleware to edge-safe cookie check (no crypto/db imports)
- Add baseUrl to tsconfig.json, normalize @/* path alias
- Set metadata title to "Client Portal" on login and portal pages
- Add compress: true, poweredByHeader: false to next.config.ts
- Update all audit callers to new audit()/listAuditEvents() API

https://claude.ai/code/session_01SBUMqqQjy1X39Pwdg5oZmo

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

event.preventDefault();
setLoading(true);
setError(null);
const nextUrl = searchParams.get('next') || '/portal';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Open redirect via unvalidated next query parameter on login

The login form reads searchParams.get('next') and passes it directly to router.push(nextUrl) without any validation. An attacker can craft a login link like /login?next=https://evil.com to redirect users to a malicious site after they authenticate. The next parameter should be validated to ensure it's a relative path starting with /.

Suggested change
const nextUrl = searchParams.get('next') || '/portal';
const rawNext = searchParams.get('next') || '/portal';
const nextUrl = rawNext.startsWith('/') && !rawNext.startsWith('//') ? rawNext : '/portal';
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@support371
support371 merged commit 804c753 into main Mar 12, 2026
4 of 10 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants