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

Codex/upgrade admin routing and UI design l3iq4v#37

Open
support371 wants to merge 20 commits into
mainfrom
codex/upgrade-admin-routing-and-ui-design-l3iq4v
Open

Codex/upgrade admin routing and UI design l3iq4v#37
support371 wants to merge 20 commits into
mainfrom
codex/upgrade-admin-routing-and-ui-design-l3iq4v

Conversation

@support371

@support371 support371 commented Feb 14, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Rebuild the marketing homepage and intelligence experience into a unified, one-page enterprise platform while introducing an admin center with role-based authentication, inbox triage, and campaign architecture stubs backed by environment-driven configuration.

New Features:

  • Introduce an intelligence command center page with filterable analyst feeds, metrics, and ingestion notes.
  • Add an email campaign engine shell with analytics tiles, dispatch queue preview, guardrails list, and data model surfacing.
  • Create architecture specification and roadmap pages to document platform purpose, modules, and phased delivery.
  • Add an admin area with protected routes, including inbox triage, users, teams, organizations, grants, and diagnostics views.

Enhancements:

  • Redesign the main landing page into a multi-section one-page layout tied to new product modules and trust metrics.
  • Refresh global navigation, footer, live support styling, typography, and background theming to match the new enterprise brand.
  • Wire the marketing and layout surfaces to environment-based app metadata and dynamic support contact details.
  • Persist contact form submissions to a file-backed store and expose CSV export plus search and status controls from the admin inbox.
  • Implement a lightweight admin authentication system with hashed users, signed session cookies, and middleware-style protection for admin routes and APIs.

Build:

  • Add environment variable schema via a shared env helper to centralize configuration for app identity, support contacts, and backend integrations.

Deployment:

  • Add a Next.js middleware proxy to enforce authentication on admin UI and API routes while preserving deep-link redirects to the login page.

Documentation:

  • Replace the default README with product-focused documentation covering routes, environment configuration, quality checks, Tailwind setup, and Vercel deployment notes.
  • Add a report documenting the absence of Bitcoin services and capturing related scan outputs.

Chores:

  • Seed local JSON data stores for admin users and contact messages along with example reports and log files for operational context.

Open with Devin

@sourcery-ai

sourcery-ai Bot commented Feb 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Rebrands the public marketing experience into a single-page GEM CYBER enterprise platform and introduces a new admin center with file-backed data stores, authenticated routing, and operator-focused modules for inbox triage, diagnostics, and roadmap/spec documentation.

Sequence diagram for contact submission and admin triage flow

sequenceDiagram
  actor Visitor
  participant Browser as PublicApp_Browser
  participant ContactApi as ApiContactRoute
  participant Store as ContactMessageStore
  participant Smtp as SmtpServer
  actor Admin
  participant AdminBrowser as AdminApp_Browser
  participant AdminInboxPage as AdminInboxPage
  participant AdminInboxApi as ApiAdminInboxRoute

  Visitor->>Browser: Submit contact form
  Browser->>ContactApi: POST /api/contact{name, email, company, phone, service, message, sourcePage}
  ContactApi->>Store: createContactMessage(sourcePage, firstName, lastName, email, phone, serviceInterest, messageBody)
  Store-->>ContactApi: ContactMessage
  alt smtp configured
    ContactApi->>Smtp: send admin notification
    ContactApi->>Smtp: send visitor confirmation
  end
  ContactApi-->>Browser: 200 {success: true}

  Admin->>AdminBrowser: Open /admin/inbox
  AdminBrowser->>AdminInboxPage: Request server component
  AdminInboxPage->>AdminInboxApi: listContactMessages(q, status)
  AdminInboxApi->>Store: listContactMessages(q, status, assignedToUserId)
  Store-->>AdminInboxApi: ContactMessage[]
  AdminInboxApi-->>AdminInboxPage: messages
  AdminInboxPage-->>AdminBrowser: Render message list

  Admin->>AdminInboxPage: Update status/assignee (form submit)
  AdminInboxPage->>Store: updateContactMessage(id, status, assignedToUserId)
  Store-->>AdminInboxPage: updated ContactMessage
  AdminInboxPage-->>AdminBrowser: Revalidated inbox view
Loading

Class diagram for admin auth, contact messages, and campaign models

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(sourcePage, firstName, lastName, email, phone, serviceInterest, messageBody) ContactMessage
    +listContactMessages(q, status, assignedToUserId) ContactMessage[]
    +updateContactMessage(id, status, assignedToUserId, orgId, tags) ContactMessage
    +toCsv(messages) string
    -ensureStore() void
    -readMessages() ContactMessage[]
    -writeMessages(messages) void
  }

  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() AdminUser[]
    +authenticateUser(email, password) AdminUser
    -hashPassword(password) string
    -verifyPassword(password, passwordHash) boolean
    -defaultUsers() AdminUser[]
    -normalizeUser(user) AdminUser
    -ensureStore() void
  }

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

  class AdminAuth {
    +string ADMIN_COOKIE
    +createSessionToken(userId, email, name, role) string
    +readSessionToken(token) SessionPayload
    +getCurrentAdminSession() SessionPayload
    +hasRole(role, allowedRoles) boolean
    -authSecret() string
    -sign(payload) string
  }

  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 {
    <<value_object>>
    +string[] rules
  }

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

  class ContactPage
  class HomePage
  class Navbar
  class Footer

  ContactMessageStore --> ContactMessage
  ContactMessageStore --> MessageStatus

  AdminUserStore --> AdminUser
  AdminUserStore --> AdminRole

  AdminAuth --> SessionPayload
  AdminAuth --> AdminRole

  Campaign --> CampaignMode
  Campaign --> Segment
  Recipient --> Campaign
  Recipient --> Contact
  Event --> Recipient
  SuppressionListEntry --> Contact
  AuditLog --> AdminUser
  NotificationOutbox --> Campaign

  CampaignEngineGuardrails --> Campaign

  AppEnv ..> ContactPage : used by
  AppEnv ..> HomePage : used by
  AppEnv ..> Navbar : used by
  AppEnv ..> Footer : used by
Loading

File-Level Changes

Change Details Files
Replaced the legacy multi-page marketing site with a one-page enterprise surface and updated global navigation, footer, and styling to use centralized app branding/env config.
  • Rebuilt the home page into a sectioned one-page layout with new hero, services, intelligence preview, campaign overview, roadmap, and contact blocks, wired to appEnv for dynamic name and support contacts.
  • Refactored the navbar to anchor-scroll within the homepage, add direct product links (Intelligence, Campaigns, Specs), and use env-driven contact mailto instead of a contact route.
  • Updated the footer to emphasize product modules, reuse appEnv branding/support info, and simplify legacy links.
  • Replaced Geist fonts and theme tokens in globals.css with custom Inter-based typography, new color system, and subtle background/animation utilities used across the app.
  • Tweaked LiveSupport styling to align with the new cyan-focused design language.
src/app/page.tsx
src/components/layout/Navbar.tsx
src/components/layout/Footer.tsx
src/app/globals.css
src/app/layout.tsx
src/components/layout/LiveSupport.tsx
Introduced dedicated Intelligence Command Center and Email Campaign Engine routes with UI shells that preview filtering, queues, analytics, and guardrails backed by shared data-model stubs.
  • Replaced the static intelligence placeholder page with an interactive command-center style feed including filters, metrics tiles, and card-based intel items plus ingestion notes.
  • Added a new /campaigns route that surfaces campaign analytics tiles, builder/segment/suppression/scheduling panels, a dispatch queue table, and a guardrails section wired to shared constants.
  • Defined campaign data model interfaces (contacts, segments, campaigns, recipients, events, suppression_list, audit_logs, notification_outbox) and shared compliance guardrail copy in a dedicated models module for reuse by UI and future backend logic.
src/app/intelligence/page.tsx
src/app/campaigns/page.tsx
src/lib/campaigns/models.ts
Implemented file-backed stores for contact messages and admin users plus admin inbox APIs, enabling persistence, querying, CSV export, and mutation of inbound contact data.
  • Created a contactMessages library that maintains a JSON store with helpers to create, list (with free-text filtering and status/assignee filters), update status/assignee/tags, and export records as CSV.
  • Updated the public contact form API to validate inputs, capture phone and sourcePage metadata, normalize name into first/last, persist messages via createContactMessage, and then conditionally send SMTP notifications.
  • Added an admin inbox API that exposes GET with optional filters and CSV export, and PATCH for status/assignment/tags updates against the contact message store.
  • Wired the contact-us page to submit phone + sourcePage to the updated API, and added a phone field to the client form.
src/lib/contactMessages.ts
src/app/api/contact/route.ts
src/app/api/admin/inbox/route.ts
src/app/contact-us/page.tsx
Added an authenticated admin center with cookie-based session handling, role-based navigation, diagnostics, and stubbed operational modules for teams, organizations, and grants, including a super-admin-only users view.
  • Implemented admin user management via a file-backed admin-users.json store with password hashing, migration from any legacy plain-text passwords, and an authenticateUser helper.
  • Introduced an HMAC-signed, expiry-bound admin session token with helpers to create/read tokens, a cookie name constant, and a getCurrentAdminSession accessor plus simple role checking.
  • Built admin login/logout API routes that set/clear the admin session cookie, and a client-side login page that posts credentials, handles errors, and redirects to a next parameter (defaulting to the inbox).
  • Added an admin layout shell that reads the current session, shows user/role metadata, renders a side nav with conditional Users link for super_admin, and includes a logout form hitting the new logout API.
  • Created server-rendered admin routes for inbox (triage UI + server action for inline updates), diagnostics (env/storage/SMTP/auth/admin-user counts), teams, organizations, grants, and a super_admin-gated users listing page; added /admin index redirect to /admin/inbox.
  • Implemented a proxy middleware that protects /admin and /api/admin routes, allowing only unauthenticated access to /admin/login and returning 401 or redirecting to login with a next query string as appropriate.
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/layout.tsx
src/app/admin/inbox/page.tsx
src/app/admin/diagnostics/page.tsx
src/app/admin/teams/page.tsx
src/app/admin/organizations/page.tsx
src/app/admin/grants/page.tsx
src/app/admin/users/page.tsx
src/app/admin/page.tsx
src/proxy.ts
Documented platform routes, environment configuration, and roadmap/spec architecture via new markdown and Next.js routes, plus added crypto auditability reports.
  • Replaced the default Next.js README with project-specific docs outlining all key routes, environment variables, quality checks, Tailwind setup, and Vercel deployment guidance including branch workflow suggestions.
  • Added /specs and /architecture routes to present an architecture/guardrail specification that mirrors the product narrative, and a /roadmap route that details phased delivery objectives and deliverables.
  • Introduced env helper module appEnv to centralize branding and provider config, consumed across navbar, footer, home, and contact sections for consistent app name/support details.
  • Added Bitcoin services inventory markdown and supporting scan logs to reports/ for auditing that any Bitcoin-related behavior is content-only, not a functional service.
  • Seeded data/admin-users.json and data/contact-messages.json as initial stores for local development and inspection, plus a stub .env.example file for configuring admin and provider-related secrets.
README.md
src/app/specs/page.tsx
src/app/architecture/page.tsx
src/app/roadmap/page.tsx
src/lib/env.ts
reports/bitcoin-services-report.md
reports/bitcoin-services-scan.txt
reports/remote-push-20260212-013023.log
reports/remote-push-20260212-013053.log
reports/remote-push-20260212-040615.log
reports/remote-push-20260212-040627.log
data/admin-users.json
data/contact-messages.json
.env.example

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

…app-branch-nh36mg

Add Bitcoin services inventory and remote-push diagnostics; tighten .gitignore for env files
@vercel

vercel Bot commented Feb 15, 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 Ready Ready Preview, Comment Mar 3, 2026 10:55pm
gemassistenterprise Error Error Mar 3, 2026 10:55pm
my-web Ready Ready Preview, Comment Mar 3, 2026 10:55pm
mymain-emterprise-website Ready Ready Preview, Comment Mar 3, 2026 10:55pm
mymain-emterprise-website-6rb7 Ready Ready Preview, Comment Mar 3, 2026 10:55pm
mymain-emterprise-website-gerf Ready Ready Preview, Comment Mar 3, 2026 10:55pm
mymain-emterprise-website-h5el Error Error Mar 3, 2026 10:55pm
mymain-emterprise-website-s362 Ready Ready Preview, Comment Mar 3, 2026 10:55pm
mymain-emterprise-website-sr68 Ready Ready Preview, Comment, Open in v0 Mar 3, 2026 10:55pm

@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 src/lib/adminAuth.ts, timingSafeEqual can throw if the provided signature length differs from the expected one; consider validating lengths before calling it so malformed tokens don’t crash requests.
  • The proxy helper in src/proxy.ts won’t run unless it’s wired into Next.js middleware (e.g., via a middleware.ts that calls it or by renaming it accordingly); ensure it’s actually invoked for the intended admin routes.
  • On src/app/admin/inbox/page.tsx, the searchParams prop is typed and handled as a Promise, but in the App Router it’s passed as a plain object; updating the type and removing await will align with Next.js conventions and avoid type issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `src/lib/adminAuth.ts`, `timingSafeEqual` can throw if the provided signature length differs from the expected one; consider validating lengths before calling it so malformed tokens don’t crash requests.
- The `proxy` helper in `src/proxy.ts` won’t run unless it’s wired into Next.js middleware (e.g., via a `middleware.ts` that calls it or by renaming it accordingly); ensure it’s actually invoked for the intended admin routes.
- On `src/app/admin/inbox/page.tsx`, the `searchParams` prop is typed and handled as a `Promise`, but in the App Router it’s passed as a plain object; updating the type and removing `await` will align with Next.js conventions and avoid type issues.

## Individual Comments

### Comment 1
<location> `src/lib/contactMessages.ts:112-121` </location>
<code_context>
+
+export async function updateContactMessage(
+  id: string,
+  updates: Partial<Pick<ContactMessage, 'status' | 'assignedToUserId' | 'orgId' | 'tags'>>,
+): Promise<ContactMessage | null> {
+  const messages = await readMessages();
+  const index = messages.findIndex((message) => message.id === id);
+
+  if (index === -1) {
+    return null;
+  }
+
+  const current = messages[index];
+
+  messages[index] = {
+    ...current,
+    ...updates,
+    tags: updates.tags ?? current.tags,
+  };
</code_context>

<issue_to_address>
**issue (bug_risk):** Spreading `updates` can overwrite fields with `undefined` (e.g., status) unexpectedly.

Since `updates` is a `Partial`, callers (e.g. the admin API) can send `{ status: undefined }`, which will overwrite `current.status` when you spread `...updates`. Your special handling only protects `tags`. To avoid this, only merge fields that are explicitly defined, e.g.: 

```ts
const next: ContactMessage = {
  ...current,
  ...(updates.status !== undefined && { status: updates.status }),
  ...(updates.assignedToUserId !== undefined && { assignedToUserId: updates.assignedToUserId }),
  ...(updates.orgId !== undefined && { orgId: updates.orgId }),
  tags: updates.tags ?? current.tags,
};

messages[index] = next;
```

This prevents unintentionally overwriting existing values with `undefined`.
</issue_to_address>

### Comment 2
<location> `src/app/api/admin/inbox/route.ts:41-45` </location>
<code_context>
+    return NextResponse.json({ message: 'Missing message id' }, { status: 400 });
+  }
+
+  const updated = await updateContactMessage(body.id, {
+    status: body.status,
+    assignedToUserId: body.assignedToUserId ?? null,
+    tags: body.tags,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** PATCH handler may pass `undefined` status/tags through, relying on `updateContactMessage` to handle them.

Because `status` and `tags` are forwarded directly, omitted/`undefined` values can end up clearing fields in combination with the current `updateContactMessage` behavior. To better match PATCH semantics, only pass properties that are actually present in the request body, e.g.:

```ts
const updates: Parameters<typeof updateContactMessage>[1] = {};
if (body.status) updates.status = body.status;
if ('assignedToUserId' in body) updates.assignedToUserId = body.assignedToUserId ?? null;
if (Array.isArray(body.tags)) updates.tags = body.tags;

const updated = await updateContactMessage(body.id, updates);
```

This avoids unintentionally nullifying fields when they are omitted from the PATCH request.

```suggestion
  const updates: Parameters<typeof updateContactMessage>[1] = {};

  if (body.status) {
    updates.status = body.status;
  }

  if ('assignedToUserId' in body) {
    updates.assignedToUserId = body.assignedToUserId ?? null;
  }

  if (Array.isArray(body.tags)) {
    updates.tags = body.tags;
  }

  const updated = await updateContactMessage(body.id, updates);
```
</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 +112 to +121
updates: Partial<Pick<ContactMessage, 'status' | 'assignedToUserId' | 'orgId' | 'tags'>>,
): Promise<ContactMessage | null> {
const messages = await readMessages();
const index = messages.findIndex((message) => message.id === id);

if (index === -1) {
return null;
}

const current = messages[index];

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): Spreading updates can overwrite fields with undefined (e.g., status) unexpectedly.

Since updates is a Partial, callers (e.g. the admin API) can send { status: undefined }, which will overwrite current.status when you spread ...updates. Your special handling only protects tags. To avoid this, only merge fields that are explicitly defined, e.g.:

const next: ContactMessage = {
  ...current,
  ...(updates.status !== undefined && { status: updates.status }),
  ...(updates.assignedToUserId !== undefined && { assignedToUserId: updates.assignedToUserId }),
  ...(updates.orgId !== undefined && { orgId: updates.orgId }),
  tags: updates.tags ?? current.tags,
};

messages[index] = next;

This prevents unintentionally overwriting existing values with undefined.

Comment on lines +41 to +45
const updated = await updateContactMessage(body.id, {
status: body.status,
assignedToUserId: body.assignedToUserId ?? null,
tags: body.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.

suggestion (bug_risk): PATCH handler may pass undefined status/tags through, relying on updateContactMessage to handle them.

Because status and tags are forwarded directly, omitted/undefined values can end up clearing fields in combination with the current updateContactMessage behavior. To better match PATCH semantics, only pass properties that are actually present in the request body, e.g.:

const updates: Parameters<typeof updateContactMessage>[1] = {};
if (body.status) updates.status = body.status;
if ('assignedToUserId' in body) updates.assignedToUserId = body.assignedToUserId ?? null;
if (Array.isArray(body.tags)) updates.tags = body.tags;

const updated = await updateContactMessage(body.id, updates);

This avoids unintentionally nullifying fields when they are omitted from the PATCH request.

Suggested change
const updated = await updateContactMessage(body.id, {
status: body.status,
assignedToUserId: body.assignedToUserId ?? null,
tags: body.tags,
});
const updates: Parameters<typeof updateContactMessage>[1] = {};
if (body.status) {
updates.status = body.status;
}
if ('assignedToUserId' in body) {
updates.assignedToUserId = body.assignedToUserId ?? null;
}
if (Array.isArray(body.tags)) {
updates.tags = body.tags;
}
const updated = await updateContactMessage(body.id, updates);

@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 7 additional findings in Devin Review.

Open in Devin Review

Comment thread src/proxy.ts
Comment on lines +9 to +41
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;

const isAdminRoute = pathname.startsWith('/admin');
const isAdminApiRoute = pathname.startsWith('/api/admin');

if (!isAdminRoute && !isAdminApiRoute) {
return NextResponse.next();
}

if (isAdminRoute && isPublicAdminPath(pathname)) {
return NextResponse.next();
}

const token = request.cookies.get(ADMIN_COOKIE)?.value;
const session = readSessionToken(token || '');

if (session) {
return NextResponse.next();
}

if (isAdminApiRoute) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}

const loginUrl = new URL('/admin/login', request.url);
loginUrl.searchParams.set('next', `${pathname}${search}`);
return NextResponse.redirect(loginUrl);
}

export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
};

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 file misnamed — admin auth protection is completely inactive

The authentication middleware is placed at src/proxy.ts, but Next.js only auto-discovers middleware from middleware.ts (at project root) or src/middleware.ts. Because no file exists at either of those paths, Next.js never invokes the proxy function. The exported config.matcher and the proxy function are dead code.

Impact: All admin routes are unprotected

Since the middleware never runs, every admin UI route (/admin/inbox, /admin/users, /admin/diagnostics, etc.) and every admin API route (/api/admin/inbox, /api/admin/logout, etc.) is publicly accessible without any authentication. Anyone can view the admin inbox, export CSV data, modify message statuses, and see diagnostic information.

The file should be renamed from src/proxy.ts to src/middleware.ts, and the default export should be the middleware function (Next.js expects a default export or a named export called middleware):

// src/middleware.ts
export { proxy as middleware, config } from './proxy';

or simply rename and restructure the file.

Prompt for agents
The middleware file at src/proxy.ts is never loaded by Next.js because it is not at the expected path. Next.js requires middleware to be at either middleware.ts (project root) or src/middleware.ts. Additionally the function must be exported as default or as a named export called 'middleware'.

Option A: Rename src/proxy.ts to src/middleware.ts and rename the exported function from 'proxy' to 'middleware'.

Option B: Create a new file src/middleware.ts that re-exports:
  export { proxy as middleware, config } from './proxy';

Either approach will make Next.js discover and invoke the admin auth middleware.
Open in Devin Review

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

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.

🔴 timingSafeEqual throws on mismatched buffer lengths, crashing session validation

At src/lib/adminAuth.ts:48, crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) will throw a RangeError: Input buffers must have the same byte length if the signature portion of a cookie token has a different byte length than the expected HMAC hex digest (which is always 64 hex chars).

Root Cause and Impact

The expected value is always a 64-character hex string produced by crypto.createHmac('sha256', ...).digest('hex') at src/lib/adminAuth.ts:20. However, signature is extracted directly from user-supplied cookie data at line 40. If a user (or attacker) sets a malformed gem_admin_session cookie with a signature part of any other length, timingSafeEqual throws instead of returning false.

This error is unhandled — it propagates up through readSessionTokengetCurrentAdminSession (line 68) → the admin layout (src/app/admin/layout.tsx:14), causing a 500 Internal Server Error on every admin page. An attacker can intentionally trigger this by setting a crafted cookie value like abc.short.

The fix is to check buffer lengths before calling timingSafeEqual:

const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
  return null;
}
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/proxy.ts
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.

1 participant