-
Notifications
You must be signed in to change notification settings - Fork 0
feat: secure admin authentication with obfuscated paths #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Replace hardcoded /admin routes with dynamic /p/[id] paths - Admin URL now controlled by ADMIN_PATH environment variable - Implement signed HMAC-SHA256 authentication tokens - Add timing-safe password comparison to prevent timing attacks - Protect /api/books POST, PUT, DELETE endpoints with auth - Add defense-in-depth auth checks in getServerSideProps - Remove old admin files, no trace of "admin" in codebase - Update README and AGENTS.md with security documentation Security improvements: - Tokens cannot be forged without ADMIN_PASSWORD - Tokens expire after 24 hours - Multiple validation layers (middleware + SSR + API) - No stack traces exposed for missing env vars Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add responsive home buttons that display icons on mobile and text on desktop. Dashboard has Home and Logout buttons side-by-side; login page has a "Back to Home" link below the sign-in form. Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe admin interface transitions from a client-side page to a protected server-side authenticated dashboard accessible only via a secret path. A new multi-layered authentication system is introduced, featuring password validation, signed tokens, HMAC-based cookie authentication, and middleware-level route protection. The previous direct client-side admin page is removed in favor of dedicated login, logout, and auth API endpoints with environment-driven security controls. Changes
Sequence DiagramssequenceDiagram
participant Client
participant Middleware
participant AuthAPI as /api/p/[id]/auth
participant LoginPage as /p/[id]/login
participant DashboardPage as /p/[id]/index
participant Cookies
rect rgb(200, 220, 255)
note over Client,Cookies: Login Flow
Client->>LoginPage: GET /p/{ADMIN_PATH}/login
LoginPage->>Middleware: [ADMIN_PATH set & user not authenticated]
Middleware-->>LoginPage: Allow (no auth cookie)
LoginPage-->>Client: Render login form
Client->>AuthAPI: POST /api/p/{ADMIN_PATH}/auth {password}
AuthAPI->>AuthAPI: Validate ADMIN_PASSWORD (timing-safe)
alt Password Valid
AuthAPI->>AuthAPI: Generate signed token (HMAC-SHA256)
AuthAPI->>Cookies: Set HTTP-only auth cookie
AuthAPI-->>Client: 200 {redirectTo: /p/{ADMIN_PATH}}
Client->>DashboardPage: Navigate to /p/{ADMIN_PATH}
else Password Invalid
AuthAPI-->>Client: 401 Unauthorized
Client->>LoginPage: Show error message
end
end
rect rgb(220, 255, 220)
note over Client,Cookies: Dashboard Access Flow
Client->>DashboardPage: GET /p/{ADMIN_PATH}
DashboardPage->>Middleware: Request with auth_auth cookie
Middleware->>Middleware: Validate token (expiry, signature)
alt Token Valid
Middleware-->>DashboardPage: Allow request
DashboardPage-->>Client: Render admin dashboard
else Token Invalid/Missing
Middleware-->>Client: Redirect to /p/{ADMIN_PATH}/login
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pages/p/[id]/index.tsx (1)
44-70: Guard againstidbeing undefined or an array before use.
router.query.idis typed asstring | string[] | undefined. When callinghandleLogoutbefore the router is ready,idcould be undefined, resulting in a request to/api/p/undefined/logout.🛠️ Proposed fix
export default function AdminDashboard() { const router = useRouter(); const { id } = router.query; + const adminPath = typeof id === 'string' ? id : ''; // Form state ... async function handleLogout() { + if (!adminPath) return; try { - const response = await fetch(`/api/p/${id}/logout`, { method: 'POST' }); + const response = await fetch(`/api/p/${adminPath}/logout`, { method: 'POST' }); const data = await response.json(); if (data.redirectTo) { router.push(data.redirectTo); } } catch (error) { setMessage({ type: 'error', text: 'Failed to logout' }); } }
🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Around line 112-131: Update the Admin Security section to include the missing
implementation details: document the token-based authentication using
HMAC-SHA256 signed tokens with 24-hour expiry, mention the middleware
enforcement via middleware.ts that protects /p/* and API routes, describe use of
crypto.timingSafeEqual for timing-safe comparisons during password/token
validation, and state the defense-in-depth approach (middleware +
getServerSideProps validation + API route checks) so readers can understand the
full security model and where to find the relevant implementations
(middleware.ts, getServerSideProps in pages/p/[id]/, and API handlers in
pages/api/p/[id]/).
- Around line 90-92: Add documentation for the required AUTH_SECRET environment
variable alongside the existing ADMIN_PATH and ADMIN_PASSWORD entries: state
that AUTH_SECRET is required (no default) and used as the HMAC-SHA256 signing
secret for tokens, include a short usage note (e.g., format/length guidance or
how to generate/store securely), and mirror the style of the existing lines
(bullet entry like `- AUTH_SECRET: ...`) so it appears with ADMIN_PATH and
ADMIN_PASSWORD in AGENTS.md.
In `@lib/auth.ts`:
- Around line 46-50: The token age check in lib/auth.ts only rejects overly old
tokens but allows future timestamps (negative tokenAge); update the validation
around tokenAge (computed from timestamp via parseInt) to also reject tokenAge <
0 (return false) and ensure isNaN(tokenAge) is still handled—i.e., after
computing tokenAge, add a check like if (isNaN(tokenAge) || tokenAge < 0 ||
tokenAge > COOKIE_MAX_AGE * 1000) return false so tokens with future timestamps
are rejected.
In `@middleware.ts`:
- Around line 61-65: The middleware currently logs and calls NextResponse.next()
when adminPath or adminPassword are missing, which allows requests to proceed;
change this to fail closed by returning an explicit 500 response (or redirect to
a safe page) instead of NextResponse.next(). Locate the check using adminPath
and adminPassword and replace the NextResponse.next() branch with a
NextResponse.error() or a NextResponse.redirect(...) that provides a clear error
response so admin routes cannot be accessed when those env vars are unset.
In `@package.json`:
- Line 10: The dependency bump to "@prisma/client": "^6.19.2" requires
performing Prisma v4→v6 migration steps: update your Prisma schema to handle
implicit many-to-many changes and run a migration (e.g., run the migrate dev
flow), remove previewFeatures entries "fullTextIndex" and "fullTextSearch" and
if you use Postgres full-text search rename the flag to "fullTextSearchPostgres"
in the schema, search for and replace any code importing or catching
NotFoundError (it no longer exists), audit code that converts between
Buffer/Uint8Array for any API changes, ensure no Prisma model names use reserved
words like async/await/using, regenerate the client (prisma generate) and run
the full test suite to surface TypeScript/runtime issues.
In `@pages/api/p/`[id]/auth.ts:
- Around line 13-29: The handler currently reads id from req.query and compares
it to adminPath but req.query.id may be a string array; update the code that
extracts id (from req.query) to normalize it to a single string (e.g., if
Array.isArray(req.query.id) use the first element or join appropriately) and
handle undefined before comparing to adminPath so the comparison in the existing
conditional (id !== adminPath) behaves correctly; adjust references to id,
req.query, and adminPath accordingly to ensure proper 404 handling.
In `@pages/p/`[id]/login.tsx:
- Around line 41-42: router.query.id can be undefined on initial render causing
requests to go to /api/p/undefined/auth; update the page to ensure a stable id
before submitting by either (A) guard using router.isReady and only read
router.query.id once router.isReady is true (and disable the form/submit until
ready), or (B) extract the id server-side in getServerSideProps and pass it into
the component as a prop (use that prop instead of router.query.id in the submit
handler); locate uses of useRouter, router.query, and the form submit handler in
pages/p/[id]/login.tsx and change the id source or add the isReady check
accordingly.
🧹 Nitpick comments (5)
README.md (1)
27-39: Consider documentinglib/auth.tsin project structure.The project structure section (lines 78-96) lists
lib/prisma.tsbut omitslib/auth.ts, which is a key file for the new authentication system. Consider updating the structure to include it for completeness.📝 Suggested documentation update
lib/ -└── prisma.ts # Prisma client +├── prisma.ts # Prisma client +└── auth.ts # Authentication utilitiespages/p/[id]/index.tsx (1)
211-230: Use Next.jsLinkcomponent for client-side navigation.Using a plain
<a>tag for internal navigation bypasses Next.js client-side routing, causing a full page reload. Use theLinkcomponent fromnext/linkfor better performance.♻️ Proposed refactor
+import Link from 'next/link'; ... - <a + <Link href="/" className="px-3 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2" aria-label="Home" > ... <span className="hidden sm:inline">Home</span> - </a> + </Link>lib/auth.ts (1)
10-23: Consider using a dedicated signing secret separate from the password.Using
ADMIN_PASSWORDas the HMAC signing key means that changing the password will invalidate all existing sessions, which may be acceptable. However, a dedicatedAUTH_SECRETenvironment variable would provide better separation of concerns and allow password changes without forcing all sessions to expire.pages/api/p/[id]/logout.ts (1)
4-26: Consider adding authentication check for logout.The logout endpoint clears the auth cookie without verifying the user is authenticated first. While this is generally harmless (clearing a non-existent cookie has no effect), adding an auth check would:
- Maintain consistency with other protected endpoints
- Prevent potential logout CSRF attacks where an attacker could force-logout authenticated users
That said, this is a minor concern since the impact is limited to session termination.
♻️ Optional: Add authentication check
import type { NextApiRequest, NextApiResponse } from 'next'; -import { generateClearAuthCookie } from '@/lib/auth'; +import { generateClearAuthCookie, isAuthenticated } from '@/lib/auth'; export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const { id } = req.query; const adminPath = process.env.ADMIN_PATH; if (!adminPath) { console.error('ADMIN_PATH environment variable is not set'); return res.status(500).json({ error: 'Server configuration error' }); } // Validate the path matches if (id !== adminPath) { return res.status(404).json({ error: 'Not found' }); } + // Only allow logout if authenticated (prevents logout CSRF) + if (!isAuthenticated(req.cookies)) { + return res.status(401).json({ error: 'Unauthorized' }); + } + // Clear the auth cookie res.setHeader('Set-Cookie', generateClearAuthCookie()); return res.status(200).json({ success: true, redirectTo: `/p/${adminPath}/login` }); }middleware.ts (1)
4-5: Duplicate constants fromlib/auth.ts.
AUTH_COOKIE_NAMEandCOOKIE_MAX_AGEare defined in bothlib/auth.tsand here. While Edge middleware can't import Node.js modules, these constants could be extracted to a shared module without Node.js dependencies.♻️ Extract shared constants
Create a new file
lib/auth-constants.ts:export const AUTH_COOKIE_NAME = 'admin_auth'; export const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours in secondsThen import from both
lib/auth.tsandmiddleware.ts:-const AUTH_COOKIE_NAME = 'admin_auth'; -const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours in seconds +import { AUTH_COOKIE_NAME, COOKIE_MAX_AGE } from '@/lib/auth-constants';
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
AGENTS.mdREADME.mdlib/auth.tsmiddleware.tspackage.jsonpages/api/books/[id].tspages/api/books/index.tspages/api/p/[id]/auth.tspages/api/p/[id]/logout.tspages/p/[id]/index.tsxpages/p/[id]/login.tsxprisma/schema.prisma
🧰 Additional context used
🧬 Code graph analysis (6)
pages/api/books/[id].ts (1)
lib/auth.ts (1)
isAuthenticated(111-114)
pages/api/p/[id]/logout.ts (1)
lib/auth.ts (1)
generateClearAuthCookie(102-105)
middleware.ts (1)
lib/auth.ts (3)
AUTH_COOKIE_NAME(3-3)COOKIE_MAX_AGE(4-4)validateAuthToken(29-59)
pages/p/[id]/index.tsx (2)
pages/p/[id]/login.tsx (1)
getServerSideProps(7-35)lib/auth.ts (2)
AUTH_COOKIE_NAME(3-3)validateAuthToken(29-59)
pages/p/[id]/login.tsx (2)
pages/p/[id]/index.tsx (1)
getServerSideProps(14-42)lib/auth.ts (2)
AUTH_COOKIE_NAME(3-3)validateAuthToken(29-59)
pages/api/books/index.ts (4)
pages/api/books/[id].ts (1)
handler(6-40)pages/api/p/[id]/auth.ts (1)
handler(8-44)types/book.ts (2)
ApiResponse(21-25)BookWithAuthors(17-19)lib/auth.ts (1)
isAuthenticated(111-114)
🔇 Additional comments (15)
prisma/schema.prisma (2)
10-16: LGTM!The explicit
@relation("AuthorToBook")correctly names this side of the many-to-many relationship. This improves schema clarity and ensures Prisma unambiguously resolves the relation.
18-22: LGTM!The relation name
"AuthorToBook"correctly matches the corresponding relation on theBookmodel, ensuring Prisma properly resolves the bidirectional many-to-many relationship.lib/auth.ts (2)
64-77: Good implementation of timing-safe comparison with constant-time length handling.The dummy comparison when lengths differ maintains constant execution time, which is the correct approach to prevent timing attacks. The implementation correctly uses
crypto.timingSafeEqualfor the actual comparison.
94-97: LGTM! Cookie security attributes are properly configured.The cookie implementation correctly sets
HttpOnly,SameSite=Strict, and conditionally addsSecurein production. This provides good protection against XSS and CSRF attacks.pages/api/p/[id]/auth.ts (1)
31-43: LGTM! Authentication flow is well-structured.The authentication logic correctly:
- Uses timing-safe password comparison via
validatePassword- Generates signed tokens with
generateAuthToken- Sets HTTP-only cookies with proper security attributes
- Returns appropriate status codes (401 for invalid password, 404 for path mismatch)
package.json (1)
27-27: TypeScript 5.9.3 is available and stable on npm registry, currently marked as the latest release. The version specification is valid.pages/api/books/index.ts (2)
3-3: LGTM!Import of
isAuthenticatedis correctly added to enable authentication checks for write operations.
13-18: LGTM!The authentication guard for POST requests is correctly implemented, consistent with the pattern used in
pages/api/books/[id].tsfor PUT and DELETE operations. The 401 response format matches the API response type.pages/api/books/[id].ts (2)
3-3: LGTM!Import added correctly for authentication utility.
25-36: LGTM!Authentication guards for PUT and DELETE operations are correctly implemented. The pattern is consistent with the POST protection in
pages/api/books/index.ts, providing uniform protection for all write operations.middleware.ts (2)
44-54: LGTM on timing-safe comparison.The constant-time comparison implementation correctly prevents timing attacks by:
- First checking length equality (constant-time short-circuit)
- XOR-ing all characters and OR-ing results to avoid early exit
70-89: API routes are adequately protected at the route level.API routes
/api/p/[id]/authand/api/p/[id]/logouthave proper path validation. Both routes validate that theidparameter matchesadminPathearly in the handler and return a 404 status if validation fails, making middleware-level protection unnecessary.pages/p/[id]/login.tsx (2)
7-35: LGTM!Server-side props correctly implement defense-in-depth by:
- Validating
ADMIN_PATHenvironment variable- Checking path parameter matches admin path
- Redirecting already-authenticated users to dashboard
This is consistent with the pattern in
pages/p/[id]/index.tsx.
72-136: LGTM on the UI implementation.The login form includes:
- Proper accessibility with
htmlForandidattributes- Loading state to prevent double submissions
- Error display for failed attempts
- Disabled button styling during loading
- Home navigation link
AGENTS.md (1)
46-54: Documentation accurately describes the admin page structure.The documentation clearly explains the protected admin dashboard and login page with their key characteristics.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| - `ADMIN_PATH`: Secret URL path segment for admin access (required, no default) | ||
| - `ADMIN_PASSWORD`: Password for admin authentication | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing required environment variable AUTH_SECRET.
The PR objectives explicitly state that AUTH_SECRET is a required environment variable for HMAC-SHA256 signed tokens. This critical configuration is not documented.
📝 Add AUTH_SECRET to documentation
Required in `.env`:
- `DATABASE_URL`: PostgreSQL connection string for Prisma
- `ADMIN_PATH`: Secret URL path segment for admin access (required, no default)
- `ADMIN_PASSWORD`: Password for admin authentication
+- `AUTH_SECRET`: Secret key for HMAC-SHA256 token signing (required for authentication)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - `ADMIN_PATH`: Secret URL path segment for admin access (required, no default) | |
| - `ADMIN_PASSWORD`: Password for admin authentication | |
| - `ADMIN_PATH`: Secret URL path segment for admin access (required, no default) | |
| - `ADMIN_PASSWORD`: Password for admin authentication | |
| - `AUTH_SECRET`: Secret key for HMAC-SHA256 token signing (required for authentication) |
🤖 Prompt for AI Agents
In `@AGENTS.md` around lines 90 - 92, Add documentation for the required
AUTH_SECRET environment variable alongside the existing ADMIN_PATH and
ADMIN_PASSWORD entries: state that AUTH_SECRET is required (no default) and used
as the HMAC-SHA256 signing secret for tokens, include a short usage note (e.g.,
format/length guidance or how to generate/store securely), and mirror the style
of the existing lines (bullet entry like `- AUTH_SECRET: ...`) so it appears
with ADMIN_PATH and ADMIN_PASSWORD in AGENTS.md.
| ### Admin Security (Path Obfuscation) | ||
|
|
||
| The admin interface uses a multi-layered security approach: | ||
|
|
||
| 1. **Secret URL Path**: The admin is only accessible at `/p/{ADMIN_PATH}` where `ADMIN_PATH` is an environment variable. There are no hardcoded paths like `/admin` in the codebase. | ||
|
|
||
| 2. **Dynamic Route Validation**: Pages at `pages/p/[id]/` use `getServerSideProps` to validate the `id` parameter matches `ADMIN_PATH`. Non-matching paths return 404. | ||
|
|
||
| 3. **API Route Validation**: API endpoints at `pages/api/p/[id]/` validate the path segment before processing requests. Invalid paths return 404. | ||
|
|
||
| 4. **Password Authentication**: Even with the correct path, users must authenticate with `ADMIN_PASSWORD` via a login form. | ||
|
|
||
| 5. **HTTP-only Cookies**: Auth state is stored in HTTP-only cookies to prevent XSS attacks. | ||
|
|
||
| **Why this approach**: The source code is public, so we can't hardcode secret paths. By using environment variables and dynamic routes with server-side validation: | ||
| - Scanning the codebase reveals nothing about the admin URL | ||
| - Brute-forcing `/p/{random}` returns 404 for incorrect guesses | ||
| - API enumeration doesn't reveal admin-related endpoints | ||
| - Even finding the path requires knowing the password | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security documentation is incomplete.
The security section provides good coverage of path obfuscation but omits several critical security features mentioned in the PR objectives:
- Token-based authentication: HMAC-SHA256 signed tokens with 24-hour expiry
- Middleware protection:
middleware.tsenforces authentication on protected routes - Timing-safe comparisons:
crypto.timingSafeEqualprevents timing attacks on password/token validation - Defense-in-depth: Multiple validation layers (middleware + getServerSideProps + API)
These implementation details are essential for understanding the full security model.
📝 Suggested additions to security documentation
5. **HTTP-only Cookies**: Auth state is stored in HTTP-only cookies to prevent XSS attacks.
+6. **Token-based Authentication**: Authentication uses HMAC-SHA256 signed tokens with 24-hour expiry. Tokens are validated using `crypto.timingSafeEqual` to prevent timing attacks.
+
+7. **Middleware Protection**: `middleware.ts` provides the first layer of defense, enforcing authentication on all `/p/:id*` routes using the Web Crypto API (Edge-compatible).
+
+8. **Defense-in-Depth**: Authentication is validated at multiple layers:
+ - Middleware (route-level protection)
+ - `getServerSideProps` (server-side page rendering)
+ - API route handlers (endpoint-level validation)
+
**Why this approach**: The source code is public, so we can't hardcode secret paths. By using environment variables and dynamic routes with server-side validation:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Admin Security (Path Obfuscation) | |
| The admin interface uses a multi-layered security approach: | |
| 1. **Secret URL Path**: The admin is only accessible at `/p/{ADMIN_PATH}` where `ADMIN_PATH` is an environment variable. There are no hardcoded paths like `/admin` in the codebase. | |
| 2. **Dynamic Route Validation**: Pages at `pages/p/[id]/` use `getServerSideProps` to validate the `id` parameter matches `ADMIN_PATH`. Non-matching paths return 404. | |
| 3. **API Route Validation**: API endpoints at `pages/api/p/[id]/` validate the path segment before processing requests. Invalid paths return 404. | |
| 4. **Password Authentication**: Even with the correct path, users must authenticate with `ADMIN_PASSWORD` via a login form. | |
| 5. **HTTP-only Cookies**: Auth state is stored in HTTP-only cookies to prevent XSS attacks. | |
| **Why this approach**: The source code is public, so we can't hardcode secret paths. By using environment variables and dynamic routes with server-side validation: | |
| - Scanning the codebase reveals nothing about the admin URL | |
| - Brute-forcing `/p/{random}` returns 404 for incorrect guesses | |
| - API enumeration doesn't reveal admin-related endpoints | |
| - Even finding the path requires knowing the password | |
| ### Admin Security (Path Obfuscation) | |
| The admin interface uses a multi-layered security approach: | |
| 1. **Secret URL Path**: The admin is only accessible at `/p/{ADMIN_PATH}` where `ADMIN_PATH` is an environment variable. There are no hardcoded paths like `/admin` in the codebase. | |
| 2. **Dynamic Route Validation**: Pages at `pages/p/[id]/` use `getServerSideProps` to validate the `id` parameter matches `ADMIN_PATH`. Non-matching paths return 404. | |
| 3. **API Route Validation**: API endpoints at `pages/api/p/[id]/` validate the path segment before processing requests. Invalid paths return 404. | |
| 4. **Password Authentication**: Even with the correct path, users must authenticate with `ADMIN_PASSWORD` via a login form. | |
| 5. **HTTP-only Cookies**: Auth state is stored in HTTP-only cookies to prevent XSS attacks. | |
| 6. **Token-based Authentication**: Authentication uses HMAC-SHA256 signed tokens with 24-hour expiry. Tokens are validated using `crypto.timingSafeEqual` to prevent timing attacks. | |
| 7. **Middleware Protection**: `middleware.ts` provides the first layer of defense, enforcing authentication on all `/p/:id*` routes using the Web Crypto API (Edge-compatible). | |
| 8. **Defense-in-Depth**: Authentication is validated at multiple layers: | |
| - Middleware (route-level protection) | |
| - `getServerSideProps` (server-side page rendering) | |
| - API route handlers (endpoint-level validation) | |
| **Why this approach**: The source code is public, so we can't hardcode secret paths. By using environment variables and dynamic routes with server-side validation: | |
| - Scanning the codebase reveals nothing about the admin URL | |
| - Brute-forcing `/p/{random}` returns 404 for incorrect guesses | |
| - API enumeration doesn't reveal admin-related endpoints | |
| - Even finding the path requires knowing the password |
🤖 Prompt for AI Agents
In `@AGENTS.md` around lines 112 - 131, Update the Admin Security section to
include the missing implementation details: document the token-based
authentication using HMAC-SHA256 signed tokens with 24-hour expiry, mention the
middleware enforcement via middleware.ts that protects /p/* and API routes,
describe use of crypto.timingSafeEqual for timing-safe comparisons during
password/token validation, and state the defense-in-depth approach (middleware +
getServerSideProps validation + API route checks) so readers can understand the
full security model and where to find the relevant implementations
(middleware.ts, getServerSideProps in pages/p/[id]/, and API handlers in
pages/api/p/[id]/).
| // Check if token is expired (older than COOKIE_MAX_AGE) | ||
| const tokenAge = Date.now() - parseInt(timestamp, 10); | ||
| if (isNaN(tokenAge) || tokenAge > COOKIE_MAX_AGE * 1000) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject tokens with future timestamps.
The token age validation only checks if the token is too old, but doesn't reject tokens with future timestamps. A token with a timestamp in the future would have a negative tokenAge and pass the expiry check.
🛠️ Proposed fix
// Check if token is expired (older than COOKIE_MAX_AGE)
const tokenAge = Date.now() - parseInt(timestamp, 10);
- if (isNaN(tokenAge) || tokenAge > COOKIE_MAX_AGE * 1000) {
+ if (isNaN(tokenAge) || tokenAge < 0 || tokenAge > COOKIE_MAX_AGE * 1000) {
return false;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Check if token is expired (older than COOKIE_MAX_AGE) | |
| const tokenAge = Date.now() - parseInt(timestamp, 10); | |
| if (isNaN(tokenAge) || tokenAge > COOKIE_MAX_AGE * 1000) { | |
| return false; | |
| } | |
| // Check if token is expired (older than COOKIE_MAX_AGE) | |
| const tokenAge = Date.now() - parseInt(timestamp, 10); | |
| if (isNaN(tokenAge) || tokenAge < 0 || tokenAge > COOKIE_MAX_AGE * 1000) { | |
| return false; | |
| } |
🤖 Prompt for AI Agents
In `@lib/auth.ts` around lines 46 - 50, The token age check in lib/auth.ts only
rejects overly old tokens but allows future timestamps (negative tokenAge);
update the validation around tokenAge (computed from timestamp via parseInt) to
also reject tokenAge < 0 (return false) and ensure isNaN(tokenAge) is still
handled—i.e., after computing tokenAge, add a check like if (isNaN(tokenAge) ||
tokenAge < 0 || tokenAge > COOKIE_MAX_AGE * 1000) return false so tokens with
future timestamps are rejected.
| // If env vars not set, let the request through - pages will handle the error | ||
| if (!adminPath || !adminPassword) { | ||
| console.error('ADMIN_PATH or ADMIN_PASSWORD environment variable is not set'); | ||
| return NextResponse.next(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security concern: Requests proceed when environment variables are missing.
When ADMIN_PATH or ADMIN_PASSWORD are not set, the middleware logs an error but allows the request through. This could expose admin routes if environment variables are accidentally unset in production.
Consider returning a 500 error or redirecting to a safe page instead of allowing the request to proceed.
🔒 Proposed fix
// If env vars not set, let the request through - pages will handle the error
if (!adminPath || !adminPassword) {
console.error('ADMIN_PATH or ADMIN_PASSWORD environment variable is not set');
- return NextResponse.next();
+ // Return 500 to prevent exposing potentially misconfigured admin routes
+ return new NextResponse('Server configuration error', { status: 500 });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // If env vars not set, let the request through - pages will handle the error | |
| if (!adminPath || !adminPassword) { | |
| console.error('ADMIN_PATH or ADMIN_PASSWORD environment variable is not set'); | |
| return NextResponse.next(); | |
| } | |
| // If env vars not set, let the request through - pages will handle the error | |
| if (!adminPath || !adminPassword) { | |
| console.error('ADMIN_PATH or ADMIN_PASSWORD environment variable is not set'); | |
| // Return 500 to prevent exposing potentially misconfigured admin routes | |
| return new NextResponse('Server configuration error', { status: 500 }); | |
| } |
🤖 Prompt for AI Agents
In `@middleware.ts` around lines 61 - 65, The middleware currently logs and calls
NextResponse.next() when adminPath or adminPassword are missing, which allows
requests to proceed; change this to fail closed by returning an explicit 500
response (or redirect to a safe page) instead of NextResponse.next(). Locate the
check using adminPath and adminPassword and replace the NextResponse.next()
branch with a NextResponse.error() or a NextResponse.redirect(...) that provides
a clear error response so admin routes cannot be accessed when those env vars
are unset.
| "dependencies": { | ||
| "@next-auth/prisma-adapter": "^1.0.5", | ||
| "@prisma/client": "^4.6.1", | ||
| "@prisma/client": "^6.19.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Prisma 6 breaking changes migration guide from Prisma 4
💡 Result:
Short migration checklist (Prisma 4 → Prisma 6)
- Update packages
- npm install
@prisma/client@6 && npm install -D prisma@6. [1]
- Check runtime / toolchain minimums
- Prisma 6 raises minimum Node.js and TypeScript versions — confirm your versions and upgrade if needed. [1]
- Handle DB/schema changes
- Implicit many-to-many (m-n) relation tables on PostgreSQL are altered in v6. After upgrading, create a dedicated migration immediately: npx prisma migrate dev --name upgrade-to-v6. (Recommended: run npx prisma db pull first if your DB has schema elements not yet in schema.prisma.) [1]
- Preview features / feature-flag changes
- fullTextIndex and fullTextSearch are GA; remove them from previewFeatures. If using PostgreSQL full-text search, rename the flag to fullTextSearchPostgres. [1]
- Client/typing/runtime API changes to check
- Buffer/Uint8Array conversions changed — inspect any code that relies on Buffer or Uint8Array round-trips. [1]
- NotFoundError was removed — update code that imports/handles it. [1]
- New reserved keywords for model names: async, await, using — rename any models using those identifiers. [1]
- Run tests and TypeScript compile
- Regenerate client (npx prisma generate), run your test suite, fix any TypeScript / runtime errors. Review Prisma schema validation (npx prisma validate). [1][2]
- Read full upgrade notes
- Review the official Prisma 6 upgrade guide and changelog for full details and any additional breaking changes that may affect your app. [1][2]
Sources
- Prisma: Upgrade to Prisma ORM 6 (breaking changes & migration steps). [1]
- Prisma changelog / upgrade guides. [2]
🌐 Web query:
TypeScript 5.9 version release date
💡 Result:
TypeScript 5.9 was released on August 1, 2025. [1][2]
Sources:
[1] TypeScript blog — "Announcing TypeScript 5.9" (Aug 1, 2025).
[2] InfoWorld — "TypeScript 5.9 arrives…" (Aug 6, 2025, referencing the Aug 1 release).
Verify Prisma 4→6 migration requirements and address breaking changes.
This major version upgrade (Prisma 4.x → 6.x) requires specific migration steps. Before deploying, ensure:
- Database schema compatibility: implicit many-to-many relations on PostgreSQL are altered in v6; run
npx prisma migrate dev --name upgrade-to-v6after upgrading - Preview features: remove
fullTextIndexandfullTextSearchfrompreviewFeatures(both are now GA); if using PostgreSQL full-text search, rename the flag tofullTextSearchPostgres - API changes:
NotFoundErrorwas removed—update any imports or error handling that reference it; verifyBuffer/Uint8Arrayconversions are compatible - Reserved keywords: model names cannot use
async,await, orusingin Prisma 6 - Regenerate client (
npx prisma generate) and run full test suite to catch any TypeScript or runtime errors
Also applies to: 24-24
🤖 Prompt for AI Agents
In `@package.json` at line 10, The dependency bump to "@prisma/client": "^6.19.2"
requires performing Prisma v4→v6 migration steps: update your Prisma schema to
handle implicit many-to-many changes and run a migration (e.g., run the migrate
dev flow), remove previewFeatures entries "fullTextIndex" and "fullTextSearch"
and if you use Postgres full-text search rename the flag to
"fullTextSearchPostgres" in the schema, search for and replace any code
importing or catching NotFoundError (it no longer exists), audit code that
converts between Buffer/Uint8Array for any API changes, ensure no Prisma model
names use reserved words like async/await/using, regenerate the client (prisma
generate) and run the full test suite to surface TypeScript/runtime issues.
| const { id } = req.query; | ||
| const adminPath = process.env.ADMIN_PATH; | ||
|
|
||
| if (!adminPath) { | ||
| console.error('ADMIN_PATH environment variable is not set'); | ||
| return res.status(500).json({ error: 'Server configuration error' }); | ||
| } | ||
|
|
||
| if (!process.env.ADMIN_PASSWORD) { | ||
| console.error('ADMIN_PASSWORD environment variable is not set'); | ||
| return res.status(500).json({ error: 'Server configuration error' }); | ||
| } | ||
|
|
||
| // Validate the path matches | ||
| if (id !== adminPath) { | ||
| return res.status(404).json({ error: 'Not found' }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle id query parameter as potential array.
The id parameter from req.query is typed as string | string[] | undefined. If the URL structure allows nested paths, id could be an array, causing the comparison id !== adminPath to behave unexpectedly.
🛠️ Proposed fix
- const { id } = req.query;
+ const id = Array.isArray(req.query.id) ? req.query.id[0] : req.query.id;
const adminPath = process.env.ADMIN_PATH;
if (!adminPath) {🤖 Prompt for AI Agents
In `@pages/api/p/`[id]/auth.ts around lines 13 - 29, The handler currently reads
id from req.query and compares it to adminPath but req.query.id may be a string
array; update the code that extracts id (from req.query) to normalize it to a
single string (e.g., if Array.isArray(req.query.id) use the first element or
join appropriately) and handle undefined before comparing to adminPath so the
comparison in the existing conditional (id !== adminPath) behaves correctly;
adjust references to id, req.query, and adminPath accordingly to ensure proper
404 handling.
| const router = useRouter(); | ||
| const { id } = router.query; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
router.query.id may be undefined on initial client render.
During hydration, router.query can be empty until the router is ready. If the form is submitted before the router is ready, the API call would go to /api/p/undefined/auth.
Consider using router.isReady guard or extracting id from getServerSideProps and passing it as a prop.
🐛 Proposed fix: Pass id as prop
-export const getServerSideProps: GetServerSideProps = async (context) => {
+export const getServerSideProps: GetServerSideProps<{ adminPath: string }> = async (context) => {
const { id } = context.params as { id: string };
const adminPath = process.env.ADMIN_PATH;
// ... validation logic ...
- return { props: {} };
+ return { props: { adminPath } };
};
-export default function Login() {
+export default function Login({ adminPath }: { adminPath: string }) {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
- const router = useRouter();
- const { id } = router.query;
+ const router = useRouter();
async function handleSubmit(event: React.FormEvent) {
// ...
- const response = await fetch(`/api/p/${id}/auth`, {
+ const response = await fetch(`/api/p/${adminPath}/auth`, {🤖 Prompt for AI Agents
In `@pages/p/`[id]/login.tsx around lines 41 - 42, router.query.id can be
undefined on initial render causing requests to go to /api/p/undefined/auth;
update the page to ensure a stable id before submitting by either (A) guard
using router.isReady and only read router.query.id once router.isReady is true
(and disable the form/submit until ready), or (B) extract the id server-side in
getServerSideProps and pass it into the component as a prop (use that prop
instead of router.query.id in the submit handler); locate uses of useRouter,
router.query, and the form submit handler in pages/p/[id]/login.tsx and change
the id source or add the isReady check accordingly.
Summary
ADMIN_PATHenvironment variable (e.g.,/p/x7k9m2p4instead of/admin)Changes
Security Features
Environment Variables Required
Test plan
/adminand/admin-loginreturn 404/p/wrong-guessreturns 404/p/${ADMIN_PATH}redirects to login when unauthenticated🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.