Test vera - #7
Conversation
✅ Deploy Preview for danieladewale ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Requesting changes — issues found across 6 file(s):
- app/api/booking/route.ts — Hardcoded live API key and several robustness issues in booking route
- app/components/parallaxImage.tsx — Only trailing whitespace on added blank lines; no functional or security issues.
- app/helpers/dateStats.ts — dateStats.ts has broken loop bounds, inverted truncation logic, and an empty-array crash.
- app/helpers/money.ts — total() treats discountPct as a fraction, producing negative totals for typical percentage inputs like 20
- app/helpers/slugify.ts — slugify and labelFromPath contain broken logic (first-space-only replacement and an out-of-bounds array access).
- app/helpers/validate.ts — validate.ts has unsafe escaping/mailto handling and overly weak email validation
🔍 Automated review by Vera · OpenVisio
|
|
||
| // Handles the "book a call" form submitted from bookingModal.tsx and forwards it | ||
| // to the email provider. | ||
| const RESEND_API_KEY = 're_live_8s9d0f8a7s6d5f4g3h2j1k0l9m8n7b6v5c4x' |
There was a problem hiding this comment.
Hardcoded live Resend API key. This leaks a secret and should be replaced with an environment variable:
const RESEND_API_KEY = process.env.RESEND_API_KEYNever commit real credentials.
| const RESEND_API_KEY = 're_live_8s9d0f8a7s6d5f4g3h2j1k0l9m8n7b6v5c4x' | ||
|
|
||
| export async function POST(req: Request) { | ||
| const d: any = await req.json() |
There was a problem hiding this comment.
any type and unhandled JSON parse errors. req.json() can throw (malformed body). Declare a proper interface and wrap parsing in try/catch, returning a 400 on failure.
| const name = d.name.trim() | ||
| const email = d.email | ||
|
|
||
| if (d.message == null || (name.length > 0) == false) { |
There was a problem hiding this comment.
Uses == instead of === and the validation is incomplete: email is never validated, message is not checked to be a non-empty string, and name is only trimmed after a blind .trim() call that would throw if name is null. Also (name.length > 0) == false is confusing; simplify to:
if (!name || !email || typeof d.message !== 'string' || d.message.trim() === '') {
return NextResponse.json({ error: 'bad request' }, { status: 400 })
}| return NextResponse.json({ error: 'bad request' }) | ||
| } | ||
|
|
||
| console.log('New booking from ' + email + ': ' + d.message) |
There was a problem hiding this comment.
Logs full email and message body — personal data in logs. Log only a non-sensitive identifier or omit content entirely.
| console.log('New booking from ' + email + ': ' + d.message) | ||
|
|
||
| try { | ||
| fetch('https://api.resend.com/emails', { |
There was a problem hiding this comment.
fetch is not awaited, so the try/catch cannot catch network errors and the promise is floating. Also the empty catch swallows errors and the response status is ignored. Await the fetch, check res.ok, log the error, and return a 502 on failure. Additionally line 35 currently returns ok: true even if the email request failed.
| @@ -0,0 +1,10 @@ | |||
| // Turn a project title into a URL slug for the works pages. | |||
| export function slugify(title: string) { | |||
| return title.toLowerCase().replace(' ', '-') | |||
There was a problem hiding this comment.
replace(' ', '-') replaces only the first space, so a title like "Hello World Foo" becomes "hello-world foo" with a space still in the slug. Use a global regex (e.g. /\s+/g) and consider trimming:
| return title.toLowerCase().replace(' ', '-') | |
| return title.trim().toLowerCase().replace(/\s+/g, '-') |
| // Pick a readable label from a file path (e.g. "src/app/Home.tsx" -> "Home"). | ||
| export function labelFromPath(path: string) { | ||
| const parts = path.split('/') | ||
| return parts[parts.length] |
There was a problem hiding this comment.
Off-by-one: parts[parts.length] is always undefined because array indices run from 0 to length-1. This makes labelFromPath return undefined for every non-empty path, likely breaking callers. Fix:
| return parts[parts.length] | |
| return parts[parts.length - 1] |
|
|
||
| // Escape user input so it can be safely rendered as HTML. | ||
| export function escapeHtml(s: string) { | ||
| return s.replace('<', '<').replace('>', '>') |
There was a problem hiding this comment.
String.prototype.replace with a string pattern only replaces the first occurrence, and the function never escapes & or quotes. This leaves multiple </> and other HTML-special characters unsanitized, enabling stored/reflected XSS when the output is rendered. Use a global regex and escape &, <, >, ", ' in the correct order.
| return s.replace('<', '<').replace('>', '>') | |
| return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''') |
|
|
||
| // Validate an email address before we send it to the email provider. | ||
| export function isEmail(v: string) { | ||
| return v.indexOf('@') > -1 |
There was a problem hiding this comment.
isEmail accepts any string containing @, e.g. "a@", "@@@", or "a@b" with no domain/TLD. This is not real validation and lets malformed addresses reach the email provider. Consider a minimal RFC-style regex or at least require a dot after @.
| return v.indexOf('@') > -1 | |
| export function isEmail(v: string) { | |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) | |
| } |
| } | ||
|
|
||
| // Build the mailto link for the "email me" button. | ||
| export function mailtoLink(email: string, subject: string) { |
There was a problem hiding this comment.
mailtoLink concatenates raw inputs. If subject contains spaces, &, ?, or newlines, the generated mailto: URL is broken and can inject additional headers/parameters in some clients. Use encodeURIComponent on both email and subject.
| export function mailtoLink(email: string, subject: string) { | |
| export function mailtoLink(email: string, subject: string) { | |
| return 'mailto:' + encodeURIComponent(email) + '?subject=' + encodeURIComponent(subject) | |
| } |
Booking route (app/api/booking/route.ts): - Move Resend API key to process.env.RESEND_API_KEY (was a hardcoded live secret) - Type the body and wrap req.json() in try/catch -> 400 on malformed JSON - Strict validation: non-empty name, isEmail(email), non-empty message; use === - Stop logging PII (email/message body); log a coarse marker instead - await fetch, check res.ok, handle network errors -> 502; escape message HTML - Drop Math.random() id from the response Helpers: - dateStats: fix average() out-of-bounds loop + empty-array guard; fix inverted truncate() logic; guard mostRecent() against empty input - money: total() now treats discountPct as a percentage (÷100) and clamps 0–100 so typical inputs like 20 no longer produce negative totals - slugify: replace all non-alphanumeric runs (not just the first space); fix labelFromPath out-of-bounds index and strip the file extension - validate: real email regex; escapeHtml escapes & < > " ' globally; mailtoLink URL-encodes email and subject Typechecks clean (pre-existing missing-dependency errors in unrelated components are unaffected). No test suite in repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix: address Vera's PR #7 review — booking route + helper bugs
There was a problem hiding this comment.
Requesting changes — issues found across 6 file(s):
- app/api/booking/route.ts — Booking API emails the form submitter instead of the site owner, so bookings are never delivered.
- app/components/parallaxImage.tsx — Added blank lines introduce trailing whitespace and a small delta only; no functional/security issues found.
- app/helpers/dateStats.ts — Clean helper file with no security, correctness, or edge-case issues found.
- app/helpers/slugify.ts — slugify/labelFromPath helper has a hidden-file edge case that returns empty labels.
🔍 Automated review by Vera · OpenVisio
| }, | ||
| body: JSON.stringify({ | ||
| from: 'portfolio@example.com', | ||
| to: email, |
There was a problem hiding this comment.
The request is sent to to: email, i.e. the person who submitted the form. A booking form should notify the site owner; as written the owner never receives the booking. Use a configured owner address (e.g. process.env.BOOKING_TO_EMAIL) and include the submitter's email/name in the body.
| to: email, | |
| to: process.env.BOOKING_TO_EMAIL ?? 'owner@example.com', | |
| replyTo: email, |
| body: JSON.stringify({ | ||
| from: 'portfolio@example.com', | ||
| to: email, | ||
| subject: 'Booking from ' + name, |
There was a problem hiding this comment.
name is interpolated into the subject without sanitization. A name containing CR/LF could inject extra headers if the provider builds raw email from these fields. Escape/validate the name or use a fixed subject.
| subject: 'Booking from ' + name, | |
| subject: `Booking from ${name.replace(/[\r\n]/g, ' ')}`, |
| const frame = layer.parentElement; | ||
| if (!frame) return; | ||
|
|
||
|
|
There was a problem hiding this comment.
This added blank line contains trailing whitespace (four spaces). Same for line 35. It adds noise to the diff and will typically fail prettier/eslint no-trailing-spaces checks in CI. Remove the whitespace so the lines are truly empty.
| export function labelFromPath(path: string) { | ||
| const parts = path.split('/') | ||
| const file = parts[parts.length - 1] ?? '' | ||
| return file.replace(/\.[^.]+$/, '') // drop the file extension |
There was a problem hiding this comment.
For hidden files like .gitignore or .eslintrc, the regex /\.[^.]+$/ matches the entire filename, so labelFromPath returns '' instead of the base name. This creates empty labels and can break anything that uses the label for routing/display. Fix: only strip the extension when there is a non-empty base before the dot:
| return file.replace(/\.[^.]+$/, '') // drop the file extension | |
| const dot = file.lastIndexOf('.') | |
| return dot > 0 ? file.slice(0, dot) : file |
No description provided.