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

Codex-generated pull request#29

Open
support371 wants to merge 10 commits into
mainfrom
codex/isolate-bitcoin-banking-app-branch
Open

Codex-generated pull request#29
support371 wants to merge 10 commits into
mainfrom
codex/isolate-bitcoin-banking-app-branch

Conversation

@support371

@support371 support371 commented Feb 12, 2026

Copy link
Copy Markdown
Owner

Codex generated this pull request, but encountered an unexpected error after generation. This is a placeholder PR message.


Codex Task

Summary by Sourcery

Rebrand the public marketing experience into a unified one-page GEM CYBER enterprise platform and introduce an initial admin center with file-backed contact message storage and role-based access.

New Features:

  • Single-page home experience with anchored sections for services, intelligence preview, campaigns, roadmap, and contact, plus dedicated routes for intelligence, campaigns, specs/architecture, roadmap, and architecture alias.
  • Email Campaign Engine shell with analytics tiles, dispatch queue view, guardrail description, and surfaced data model entities for future backend integration.
  • Intelligence Command Center page with filterable analyst feed preview, alert metrics, and ingestion notes.
  • Admin center with role-based login, protected /admin area, and sections for inbox, teams, organizations, grants, diagnostics, and super-admin user management.
  • File-backed contact message store with CSV export, search and status filtering, and admin triage UI in the inbox.
  • Role-based admin user model with password hashing, local JSON persistence, and minimal diagnostics reporting for environment and configuration health.

Enhancements:

  • Updated global navigation, footer, and visual theme (colors, typography, background, animations) to align with the new GEM CYBER enterprise brand and environment-driven app identity.
  • Extended contact form and API to capture phone and source metadata, persist submissions to the new contact message store, and continue optional SMTP email notifications.
  • Simplified root layout font handling by removing Geist font configuration in favor of a custom Inter-based stack tied to the new design tokens.

Documentation:

  • Replaced the default Next.js README with project-specific documentation covering routes, local setup, environment variables, quality checks, Tailwind integration, Vercel deployment, and data model notes.

@vercel

vercel Bot commented Feb 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
gemassistenterprise Ready Ready Preview, Comment Feb 12, 2026 7:44am
my-web Ready Ready Preview, Comment Feb 12, 2026 7:44am
mymain-emterprise-website Ready Ready Preview, Comment Feb 12, 2026 7:44am

@sourcery-ai

sourcery-ai Bot commented Feb 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Modernizes the marketing and intelligence surfaces, introduces a campaign engine UI, and adds a lightweight admin center with file-backed storage, auth, and inbox tooling wired into the contact API and environment-driven configuration.

Sequence diagram for contact form submission into admin inbox

sequenceDiagram
  actor Visitor
  participant ContactPage as ContactPage_/contact-us
  participant ContactApi as API_contact_POST
  participant ContactStore as contactMessages_store
  participant SMTP as SMTP_provider

  Visitor->>ContactPage: Fill and submit contact form
  ContactPage->>ContactApi: POST /api/contact
  ContactApi->>ContactApi: Validate required fields
  ContactApi->>ContactStore: createContactMessage(sourcePage, firstName, lastName, email, phone, serviceInterest, messageBody)
  ContactStore-->>ContactApi: ContactMessage record
  alt SMTP configured
    ContactApi->>SMTP: sendMail(admin_notification)
    ContactApi->>SMTP: sendMail(user_confirmation)
    SMTP-->>ContactApi: Delivery responses
  else SMTP not configured
    ContactApi-->>ContactApi: Skip outbound email
  end
  ContactApi-->>ContactPage: 200 OK { success: true }
  ContactPage-->>Visitor: Show success message
Loading

Sequence diagram for admin authentication and route protection

sequenceDiagram
  actor Admin
  participant AdminLogin as AdminLoginPage
  participant AdminLoginApi as API_admin_login_POST
  participant AdminUsers as adminUsers_store
  participant AdminAuth as adminAuth_token
  participant Browser as Browser_cookies
  participant Proxy as proxy_middleware
  participant AdminInbox as AdminInboxPage

  Admin->>AdminLogin: Open /admin/login
  AdminLogin->>AdminLoginApi: POST /api/admin/login { email, password }
  AdminLoginApi->>AdminUsers: authenticateUser(email, password)
  AdminUsers-->>AdminLoginApi: AdminUser | null
  alt valid user
    AdminLoginApi->>AdminAuth: createSessionToken(userId, email, name, role)
    AdminAuth-->>AdminLoginApi: token
    AdminLoginApi->>Browser: Set ADMIN_COOKIE=token
    AdminLoginApi-->>AdminLogin: 200 OK
    AdminLogin->>AdminInbox: router.push(/admin/inbox)
    Admin->>Proxy: Request /admin/inbox
    Proxy->>Browser: Read ADMIN_COOKIE
    Proxy->>AdminAuth: readSessionToken(token)
    AdminAuth-->>Proxy: SessionPayload
    Proxy-->>AdminInbox: Allow request
    AdminInbox-->>Admin: Render inbox UI
  else invalid user
    AdminLoginApi-->>AdminLogin: 401 Invalid credentials
    AdminLogin-->>Admin: Show error message
  end
Loading

ER diagram for contact messages, admin users, and campaign data model stubs

erDiagram
  AdminUser {
    string id
    string email
    string passwordHash
    string name
    string role
    boolean active
  }

  ContactMessage {
    string id
    string createdAt
    string sourcePage
    string firstName
    string lastName
    string email
    string phone
    string serviceInterest
    string messageBody
    string status
    string assignedToUserId
    string orgId
    string tags
  }

  Contact {
    string id
    string email
    string firstName
    string lastName
    string tags
    string status
  }

  Segment {
    string id
    string name
    string filters
    int contactCount
  }

  Campaign {
    string id
    string name
    string mode
    string status
    string segmentId
    string scheduledAtUtc
  }

  Recipient {
    string id
    string campaignId
    string contactId
    string deliveryStatus
  }

  Event {
    string id
    string recipientId
    string eventType
    string atUtc
  }

  SuppressionListEntry {
    string id
    string email
    string reason
    string createdAtUtc
  }

  AuditLog {
    string id
    string actor
    string action
    string targetId
    string createdAtUtc
  }

  NotificationOutbox {
    string id
    string channel
    string status
    string payloadRef
    string createdAtUtc
  }

  AdminUser ||--o{ ContactMessage : assigned_to
  Contact ||--o{ Recipient : receives
  Segment ||--o{ Campaign : filters
  Campaign ||--o{ Recipient : targets
  Recipient ||--o{ Event : generates
  Contact ||--o{ SuppressionListEntry : may_have
  Campaign ||--o{ AuditLog : changes_logged_in
  NotificationOutbox ||--o{ AuditLog : delivery_logged_in
Loading

Updated class diagram for contact message storage and admin inbox tooling

classDiagram
  class ContactMessage {
    string id
    string createdAt
    string sourcePage
    string firstName
    string lastName
    string email
    string phone
    string serviceInterest
    string messageBody
    MessageStatus status
    string assignedToUserId
    string orgId
    string[] tags
  }

  class MessageStatus {
    <<enumeration>>
    open
    triaged
    closed
  }

  class ContactMessageStore {
    +createContactMessage(input)
    +listContactMessages(filters)
    +updateContactMessage(id, updates)
    +toCsv(messages)
  }

  class ContactApiRoute {
    +POST(request)
  }

  class AdminInboxApiRoute {
    +GET(request)
    +PATCH(request)
  }

  class AdminInboxPage {
    +updateMessageAction(formData)
  }

  ContactMessageStore --> ContactMessage : manages
  ContactApiRoute ..> ContactMessageStore : uses
  AdminInboxApiRoute ..> ContactMessageStore : uses
  AdminInboxPage ..> ContactMessageStore : updates_via_actions
Loading

Updated class diagram for admin users, auth, and protected admin center

classDiagram
  class AdminUser {
    string id
    string email
    string passwordHash
    string name
    AdminRole role
    boolean active
  }

  class AdminRole {
    <<enumeration>>
    super_admin
    admin
    analyst
    auditor
  }

  class AdminUserStore {
    +listAdminUsers()
    +authenticateUser(email, password)
  }

  class SessionPayload {
    string userId
    string email
    string name
    AdminRole role
    number exp
  }

  class AdminAuth {
    +createSessionToken(userId, email, name, role)
    +readSessionToken(token)
    +getCurrentAdminSession()
    +hasRole(role, allowedRoles)
  }

  class AdminLoginApiRoute {
    +POST(request)
  }

  class AdminLogoutApiRoute {
    +POST(request)
  }

  class AdminLoginPage {
    +onSubmit(event)
  }

  class AdminLayout {
    +render(children)
  }

  class AdminUsersPage {
    +render()
  }

  class AdminProxyMiddleware {
    +proxy(request)
  }

  AdminUserStore --> AdminUser : manages
  AdminLoginApiRoute ..> AdminUserStore : authenticateUser
  AdminLoginApiRoute ..> AdminAuth : createSessionToken
  AdminLogoutApiRoute ..> AdminAuth : clear_cookie
  AdminLoginPage ..> AdminLoginApiRoute : fetch
  AdminLayout ..> AdminAuth : getCurrentAdminSession
  AdminUsersPage ..> AdminAuth : getCurrentAdminSession
  AdminUsersPage ..> AdminUserStore : listAdminUsers
  AdminProxyMiddleware ..> AdminAuth : readSessionToken
  AdminAuth --> SessionPayload : uses
Loading

Updated class diagram for campaign engine data model stubs and guardrails

classDiagram
  class CampaignMode {
    <<enumeration>>
    sequence
    newsletter
  }

  class Contact {
    string id
    string email
    string firstName
    string lastName
    string[] tags
    string status
  }

  class Segment {
    string id
    string name
    string[] filters
    number contactCount
  }

  class Campaign {
    string id
    string name
    CampaignMode mode
    string status
    string segmentId
    string scheduledAtUtc
  }

  class Recipient {
    string id
    string campaignId
    string contactId
    string deliveryStatus
  }

  class Event {
    string id
    string recipientId
    string eventType
    string atUtc
  }

  class SuppressionListEntry {
    string id
    string email
    string reason
    string createdAtUtc
  }

  class AuditLog {
    string id
    string actor
    string action
    string targetId
    string createdAtUtc
  }

  class NotificationOutbox {
    string id
    string channel
    string status
    string payloadRef
    string createdAtUtc
  }

  class CampaignEngineGuardrails {
    +campaignEngineGuardrails: string[]
  }

  Segment "1" --> "many" Campaign : selected_by
  Campaign "1" --> "many" Recipient : sends_to
  Recipient "1" --> "many" Event : tracked_by
  Contact "1" --> "many" Recipient : receives
  Contact "1" --> "many" SuppressionListEntry : may_be_suppressed_by
  Campaign "1" --> "many" AuditLog : operations_logged_in
  NotificationOutbox "1" --> "many" AuditLog : delivery_logged_in
  CampaignEngineGuardrails ..> Campaign : governs_behavior
Loading

Updated class diagram for environment configuration and layout components

classDiagram
  class AppEnv {
    +string appName
    +string supportEmail
    +string supportPhone
    +string dataProviderApiKey
    +string newsIngestionWebhookSecret
    +string emailProviderApiKey
    +string emailSendingDomain
    +string databaseUrl
    +string auditLogSink
  }

  class Navbar {
    +navigation
    +productNavigation
    +mobileNavigation
    +isProductRoute(pathname)
  }

  class Footer {
    +render()
  }

  class HomePage {
    +trustMetrics
    +services
    +architectureSurfaces
    +intelligencePreview
    +render()
  }

  class IntelligencePage {
    +filters
    +feedCards
    +render()
  }

  class CampaignsPage {
    +analytics
    +queueRows
    +dataModelStubs
    +render()
  }

  class RoadmapPage {
    +roadmapPhases
    +render()
  }

  class RootLayout {
    +metadata
    +render(children)
  }

  AppEnv <.. Navbar : uses_appEnv
  AppEnv <.. Footer : uses_appEnv
  AppEnv <.. HomePage : uses_appEnv
  AppEnv <.. ContactPage : uses_appEnv
  RootLayout ..> Navbar : includes
  RootLayout ..> Footer : includes
  RootLayout ..> HomePage : wraps
  RootLayout ..> IntelligencePage : wraps
  RootLayout ..> CampaignsPage : wraps
  RootLayout ..> RoadmapPage : wraps
Loading

File-Level Changes

Change Details Files
Rebuilds the marketing homepage and site-wide navigation/footer to use a one-page enterprise narrative wired to environment-based branding and support contact details.
  • Replace the home page hero, stats, and sections with new one-page layout sections (services, intelligence, campaigns, roadmap, contact) powered by inline config arrays and appEnv values.
  • Refactor the navbar to use anchor-based navigation for on-page sections plus product links, update styling, and read app name/support email from env.
  • Update the footer to align links with new product and services routes and to render dynamic company name and contact channels from env.
  • Adjust global CSS theme tokens, background, fonts, and add small animation/util classes for the new visual style.
  • Simplify RootLayout by removing Geist fonts and relying on global font configuration.
src/app/page.tsx
src/components/layout/Navbar.tsx
src/components/layout/Footer.tsx
src/app/globals.css
src/app/layout.tsx
Redesigns the Intelligence module into an interactive analyst command center with filterable cards and metrics.
  • Replace the previous static intelligence marketing copy with a dashboard-style layout that shows summary metrics, filter pills, and a feed of intelligence cards with severity labels.
  • Add local data structures for filters, metrics, and feed items; keep all logic client-side with simple JSX mapping.
src/app/intelligence/page.tsx
Introduces a Campaign Engine page that outlines campaign, segmentation, dispatch queue, guardrails, and data model surfaces using shared campaign model types.
  • Create a new /campaigns route that displays analytics tiles, campaign builder CTA, audience segments, suppression/schedule cards, a dispatch queue table, and data model tags.
  • Import guardrail copy from the campaign models library to keep UI text aligned with backend expectations.
src/app/campaigns/page.tsx
src/lib/campaigns/models.ts
Adds an architecture/specification and roadmap surface, plus an alias route, to describe platform modules and phased delivery.
  • Create a /specs route that summarizes platform purpose, intelligence, campaign engine, data/storage, guardrails, and backend surfaces using iconized cards.
  • Add a /roadmap route that lays out phased objectives and deliverables for the platform with simple presentational components.
  • Introduce an /architecture route that aliases to the specs page for URL flexibility.
src/app/specs/page.tsx
src/app/roadmap/page.tsx
src/app/architecture/page.tsx
Implements an admin center shell with RBAC-aware navigation and several stub admin pages (inbox, teams, organizations, grants, diagnostics, users) backed by a custom session mechanism and middleware-like proxy.
  • Add an /admin layout that reads the current admin session, renders a shared header/sidebar, and conditionally exposes a Users nav item for super admins.
  • Create stub admin pages for inbox, teams, organizations, grants, diagnostics, users, and index redirect, each rendering read-only tables or cards that model future admin workflows.
  • Add a proxy function (middleware) that protects /admin and /api/admin routes: redirecting unauthenticated UI requests to /admin/login and returning 401 for API calls.
  • Wire a diagnostics page to inspect runtime, storage accessibility, SMTP config, auth secret, and admin user count.
src/app/admin/layout.tsx
src/app/admin/page.tsx
src/app/admin/inbox/page.tsx
src/app/admin/teams/page.tsx
src/app/admin/organizations/page.tsx
src/app/admin/grants/page.tsx
src/app/admin/diagnostics/page.tsx
src/app/admin/users/page.tsx
src/proxy.ts
Builds a simple file-backed contact message store and exposes it via an admin inbox API and CSV export, while integrating it with the public contact-us flow.
  • Introduce a contactMessages library that reads/writes JSON on disk, supports create/list/update, and exports messages to CSV for reporting.
  • Update the public contact API to split name, persist messages via createContactMessage (with sourcePage, serviceInterest, etc.), and then optionally send emails via nodemailer.
  • Extend the contact-us page form to include a phone field and sourcePage metadata when calling the contact API.
  • Add an admin Inbox page that lists messages server-side, supports search and status filtering via searchParams, and posts server actions to update status and assignee.
  • Expose an /api/admin/inbox route with GET (JSON or CSV) and PATCH for updating message status/assignee/tags.
src/lib/contactMessages.ts
src/app/api/contact/route.ts
src/app/contact-us/page.tsx
src/app/admin/inbox/page.tsx
src/app/api/admin/inbox/route.ts
Adds a lightweight admin authentication system with file-backed users, password hashing, session cookies, and login/logout flows using env-configurable defaults.
  • Create an adminUsers library that manages a JSON-backed user store, supports legacy password migration, and seeds default users whose passwords are derived from env variables.
  • Implement an adminAuth helper that signs and verifies HMAC-based session tokens stored in an httpOnly cookie, and exposes helpers to read the current session and check roles.
  • Add API routes for admin login and logout that authenticate credentials, set/clear the session cookie, and return basic user info.
  • Provide a client-side /admin/login page that posts to the login API, handles error messaging, and redirects to a configurable next URL on success.
src/lib/adminUsers.ts
src/lib/adminAuth.ts
src/app/api/admin/login/route.ts
src/app/api/admin/logout/route.ts
src/app/admin/login/page.tsx
Centralizes environment configuration and documents required env vars and routes in the README and example env file.
  • Add an env helper that exposes app name, support contacts, and backend-related secrets/URLs from process.env, with sensible defaults for local dev.
  • Replace hard-coded branding and support contact details in Navbar, Footer, Home, and Contact surfaces with values from appEnv.
  • Expand the README with project overview, routes, development steps, env var list, and deployment guidance, and add an .env.example stub.
  • Add seed JSON files for admin users and contact messages to the data directory to support local admin/login and inbox flows.
src/lib/env.ts
src/components/layout/Navbar.tsx
src/components/layout/Footer.tsx
src/app/page.tsx
src/app/contact-us/page.tsx
README.md
.env.example
data/admin-users.json
data/contact-messages.json

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

@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 4 issues, and left some high level feedback:

  • The new src/proxy.ts with proxy() and config won’t actually run unless it’s wired up as a Next.js middleware.ts default export; consider moving this into middleware.ts or otherwise registering it so admin route protection is enforced.
  • The JSON stores under data/ (admin-users.json, contact-messages.json) are now committed to the repo, which risks shipping sample credentials/PII; it would be safer to add these to .gitignore and seed them at runtime instead.
  • The appEnv helper mixes public and secret-like values (e.g. API keys) and is imported by client components such as Navbar and Footer; consider splitting this into a client-safe config and a server-only config to prevent accidental exposure of sensitive env vars in the client bundle.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `src/proxy.ts` with `proxy()` and `config` won’t actually run unless it’s wired up as a Next.js `middleware.ts` default export; consider moving this into `middleware.ts` or otherwise registering it so admin route protection is enforced.
- The JSON stores under `data/` (`admin-users.json`, `contact-messages.json`) are now committed to the repo, which risks shipping sample credentials/PII; it would be safer to add these to `.gitignore` and seed them at runtime instead.
- The `appEnv` helper mixes public and secret-like values (e.g. API keys) and is imported by client components such as `Navbar` and `Footer`; consider splitting this into a client-safe config and a server-only config to prevent accidental exposure of sensitive env vars in the client bundle.

## Individual Comments

### Comment 1
<location> `src/lib/contactMessages.ts:61-67` </location>
<code_context>
+): Promise<ContactMessage> {
+  const messages = await readMessages();
+
+  const record: ContactMessage = {
+    id: crypto.randomUUID(),
+    createdAt: new Date().toISOString(),
+    status: 'open',
+    assignedToUserId: null,
+    orgId: null,
+    tags: [],
+    ...input,
+  };
</code_context>

<issue_to_address>
**issue (bug_risk):** crypto.randomUUID is used without importing crypto, which will throw at runtime.

In Node, the global `crypto` is the Web Crypto API and does not provide `randomUUID` like `node:crypto` does, so this call will fail. Import and use `randomUUID` from `node:crypto` (or import `crypto` from `node:crypto`) instead, for example:

```ts
import { randomUUID } from 'node:crypto';

const record: ContactMessage = {
  id: randomUUID(),
  // ...
};
```
</issue_to_address>

### Comment 2
<location> `src/lib/adminAuth.ts:40-48` </location>
<code_context>
+    return null;
+  }
+
+  const [encodedPayload, signature] = token.split('.');
+
+  if (!encodedPayload || !signature) {
+    return null;
+  }
+
+  const expected = sign(encodedPayload);
+
+  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
+    return null;
+  }
</code_context>

<issue_to_address>
**🚨 issue (security):** timingSafeEqual is called without checking length equality, which can throw and bypass auth instead of failing gracefully.

In `readSessionToken`, `crypto.timingSafeEqual` is called on `signature` and `expected` without normalizing/validating lengths. If they differ, Node throws instead of returning `false`, which can break or bypass auth handling. Guard on length first and compare buffers:

```ts
const sigBuf = Buffer.from(signature, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');

if (sigBuf.length !== expectedBuf.length) {
  return null;
}

if (!crypto.timingSafeEqual(sigBuf, expectedBuf)) {
  return null;
}
```

This keeps the comparison constant‑time while avoiding runtime errors for malformed tokens.
</issue_to_address>

### Comment 3
<location> `src/app/api/admin/inbox/route.ts:4-13` </location>
<code_context>
+export async function GET(request: Request) {
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Admin inbox API endpoints rely solely on middleware for auth; adding server-side session checks would harden access control.

These `GET`/`PATCH` handlers assume admin auth is enforced only by routing/middleware. If that layer is ever misconfigured or skipped, the endpoints effectively become public despite exposing sensitive contact data and allowing status/assignee changes.

Consider adding an explicit session + role check here (e.g. `getCurrentAdminSession()` with a role assertion) and returning `401/403` on failure, so the handlers themselves still enforce access control even if middleware is bypassed.

Suggested implementation:

```typescript
import { NextResponse } from 'next/server';
import { listContactMessages, toCsv, updateContactMessage } from '@/lib/contactMessages';
import { getCurrentAdminSession } from '@/lib/auth';

```

```typescript
export async function GET(request: Request) {
  const session = await getCurrentAdminSession();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  if (session.user?.role !== 'admin') {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  const url = new URL(request.url);

```

To fully apply this pattern across the inbox admin API and keep access control consistent:
1. Ensure `getCurrentAdminSession` exists in `@/lib/auth` (or adjust the import path) and that it returns a session object with a `user.role` field (or adapt the role check to your existing session shape).
2. Apply the same session + role check at the top of the `PATCH` handler (and any other handlers in this file) so write operations are also protected if middleware or routing is misconfigured.
3. If your project distinguishes between authentication errors (401) and authorization errors (403) differently, adjust the status codes and error payloads to match your existing API conventions.
</issue_to_address>

### Comment 4
<location> `src/app/admin/inbox/page.tsx:25-32` </location>
<code_context>
+
+export default async function InboxPage({
+  searchParams,
+}: {
+  searchParams: Promise<{ q?: string; status?: 'all' | MessageStatus }>;
+}) {
+  const params = await searchParams;
+  const q = params.q || '';
+  const status = params.status || 'all';
</code_context>

<issue_to_address>
**suggestion:** Typing searchParams as a Promise is misleading and unnecessary, even though await on a plain object works at runtime.

In the app router, `searchParams` is already a plain object, not a promise, so this typing is incorrect and the `await` is unnecessary. Please type it directly as:

```ts
export default async function InboxPage({
  searchParams,
}: {
  searchParams: { q?: string; status?: 'all' | MessageStatus };
}) {
  const q = searchParams.q || '';
  const status = searchParams.status || 'all';
  // ...
}
```

```suggestion
export default async function InboxPage({
  searchParams,
}: {
  searchParams: { q?: string; status?: 'all' | MessageStatus };
}) {
  const q = searchParams.q || '';
  const status = searchParams.status || 'all';
```
</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 +61 to +67
const record: ContactMessage = {
id: crypto.randomUUID(),
createdAt: new Date().toISOString(),
status: 'open',
assignedToUserId: null,
orgId: null,
tags: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): crypto.randomUUID is used without importing crypto, which will throw at runtime.

In Node, the global crypto is the Web Crypto API and does not provide randomUUID like node:crypto does, so this call will fail. Import and use randomUUID from node:crypto (or import crypto from node:crypto) instead, for example:

import { randomUUID } from 'node:crypto';

const record: ContactMessage = {
  id: randomUUID(),
  // ...
};

Comment thread src/lib/adminAuth.ts
Comment on lines +40 to +48
const [encodedPayload, signature] = token.split('.');

if (!encodedPayload || !signature) {
return null;
}

const expected = sign(encodedPayload);

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.

🚨 issue (security): timingSafeEqual is called without checking length equality, which can throw and bypass auth instead of failing gracefully.

In readSessionToken, crypto.timingSafeEqual is called on signature and expected without normalizing/validating lengths. If they differ, Node throws instead of returning false, which can break or bypass auth handling. Guard on length first and compare buffers:

const sigBuf = Buffer.from(signature, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');

if (sigBuf.length !== expectedBuf.length) {
  return null;
}

if (!crypto.timingSafeEqual(sigBuf, expectedBuf)) {
  return null;
}

This keeps the comparison constant‑time while avoiding runtime errors for malformed tokens.

Comment on lines +4 to +13
export async function GET(request: Request) {
const url = new URL(request.url);
const status = url.searchParams.get('status') || undefined;
const q = url.searchParams.get('q') || undefined;
const format = url.searchParams.get('format');

const messages = await listContactMessages({
q,
status: status === 'open' || status === 'triaged' || status === 'closed' ? status : undefined,
});

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): Admin inbox API endpoints rely solely on middleware for auth; adding server-side session checks would harden access control.

These GET/PATCH handlers assume admin auth is enforced only by routing/middleware. If that layer is ever misconfigured or skipped, the endpoints effectively become public despite exposing sensitive contact data and allowing status/assignee changes.

Consider adding an explicit session + role check here (e.g. getCurrentAdminSession() with a role assertion) and returning 401/403 on failure, so the handlers themselves still enforce access control even if middleware is bypassed.

Suggested implementation:

import { NextResponse } from 'next/server';
import { listContactMessages, toCsv, updateContactMessage } from '@/lib/contactMessages';
import { getCurrentAdminSession } from '@/lib/auth';
export async function GET(request: Request) {
  const session = await getCurrentAdminSession();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  if (session.user?.role !== 'admin') {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  const url = new URL(request.url);

To fully apply this pattern across the inbox admin API and keep access control consistent:

  1. Ensure getCurrentAdminSession exists in @/lib/auth (or adjust the import path) and that it returns a session object with a user.role field (or adapt the role check to your existing session shape).
  2. Apply the same session + role check at the top of the PATCH handler (and any other handlers in this file) so write operations are also protected if middleware or routing is misconfigured.
  3. If your project distinguishes between authentication errors (401) and authorization errors (403) differently, adjust the status codes and error payloads to match your existing API conventions.

Comment on lines +25 to +32
export default async function InboxPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; status?: 'all' | MessageStatus }>;
}) {
const params = await searchParams;
const q = params.q || '';
const status = params.status || 'all';

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: Typing searchParams as a Promise is misleading and unnecessary, even though await on a plain object works at runtime.

In the app router, searchParams is already a plain object, not a promise, so this typing is incorrect and the await is unnecessary. Please type it directly as:

export default async function InboxPage({
  searchParams,
}: {
  searchParams: { q?: string; status?: 'all' | MessageStatus };
}) {
  const q = searchParams.q || '';
  const status = searchParams.status || 'all';
  // ...
}
Suggested change
export default async function InboxPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; status?: 'all' | MessageStatus }>;
}) {
const params = await searchParams;
const q = params.q || '';
const status = params.status || 'all';
export default async function InboxPage({
searchParams,
}: {
searchParams: { q?: string; status?: 'all' | MessageStatus };
}) {
const q = searchParams.q || '';
const status = searchParams.status || 'all';

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

ℹ️ 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/proxy.ts
Comment on lines +19 to +20
if (isAdminRoute && isPublicAdminPath(pathname)) {
return NextResponse.next();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Exempt login API route from admin auth middleware

Because the matcher includes /api/admin/:path*, unauthenticated POST /api/admin/login requests also go through this guard, but the allowlist only bypasses /admin/login page routes. As written, fresh login attempts are intercepted and return 401 before the login handler runs, so admins cannot sign in from a logged-out state.

Useful? React with 👍 / 👎.

Comment thread src/lib/adminAuth.ts

const expected = sign(encodedPayload);

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 Check signature length before timingSafeEqual

readSessionToken passes the cookie-provided signature directly into crypto.timingSafeEqual without ensuring both buffers have equal length. In Node, mismatched lengths throw ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, so a malformed gem_admin_session cookie can trigger a 500 on protected admin requests instead of being treated as an invalid session.

Useful? React with 👍 / 👎.

Comment thread src/lib/adminAuth.ts
}

function authSecret(): string {
return process.env.ADMIN_AUTH_SECRET || 'change-me-auth-secret';

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 Remove hardcoded fallback for session signing key

Using a built-in default for ADMIN_AUTH_SECRET means any environment that forgets to set this variable signs and validates admin session cookies with a publicly known key. In that configuration, an attacker can forge a valid cookie payload (including elevated roles) and bypass authentication entirely.

Useful? React with 👍 / 👎.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant