From a6eaa61ede0581f65fa2c7cf04ba4433bb0f83f7 Mon Sep 17 00:00:00 2001 From: waterlemonnn Date: Sat, 8 Aug 2026 00:48:53 +0700 Subject: [PATCH 1/2] fix(seo): disallow status and unsubscribe token routes in robots.txt Both /status/[token] and /unsubscribe/[token] carry a private token in the URL and are linked from outbound email, same as /edit/[token]. Add them to the disallow list and normalize /admin to have a trailing slash like the rest. Fixes #57 --- app/robots.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/robots.ts b/app/robots.ts index 8eae56d..7b6a10a 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -7,7 +7,7 @@ export default function robots(): MetadataRoute.Robots { rules: { userAgent: "*", allow: "/", - disallow: ["/admin", "/api/", "/edit/"], + disallow: ["/admin/", "/api/", "/edit/", "/status/", "/unsubscribe/"], }, sitemap: `${siteUrl}/sitemap.xml`, }; From 417d0c7a6d0a60302d96caa08a097d31b220aac5 Mon Sep 17 00:00:00 2001 From: waterlemonnn Date: Sat, 8 Aug 2026 00:52:37 +0700 Subject: [PATCH 2/2] fix(comments): use shared IP hash instead of local hardcoded-pepper copy app/api/comments/route.ts had its own hashIp/getIp instead of the lib/utils/hash helpers every other route uses. Three problems: it fell back to the literal string "default-pepper" when IP_HASH_PEPPER was unset instead of failing loudly, it used SHA256(ip + pepper) instead of HMAC-SHA256(pepper, ip) so the same visitor hashed differently here vs. other endpoints, and it never checked x-real-ip so proxies without x-forwarded-for collapsed every commenter into one rate-limit bucket. Swap in hashIp/getClientIp from lib/utils/hash and log the error in the catch block instead of swallowing it. Existing comments.ip_hash rows were written with the old scheme and won't match going forward. That's fine since the column is only used for rate limiting/abuse tracing, not identity, but a maintainer may want to clear the rate limit table on deploy. --- app/api/comments/route.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts index f996392..6cc9a14 100644 --- a/app/api/comments/route.ts +++ b/app/api/comments/route.ts @@ -1,19 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { createHash } from "crypto"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import { createSupabaseAdminClient } from "@/lib/supabase/admin"; import { consumeSharedRateLimit } from "@/lib/rate-limit/shared"; - -function hashIp(ip: string): string { - const pepper = process.env.IP_HASH_PEPPER ?? "default-pepper"; - return createHash("sha256") - .update(ip + pepper) - .digest("hex"); -} - -function getIp(req: NextRequest): string { - return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown"; -} +import { hashIp, getClientIp } from "@/lib/utils/hash"; interface CommentBody { post_id?: unknown; @@ -65,7 +54,7 @@ export async function POST(req: NextRequest) { ); } - const ip = getIp(req); + const ip = getClientIp(req.headers); const ip_hash = hashIp(ip); const rateLimit = await consumeSharedRateLimit( @@ -97,7 +86,8 @@ export async function POST(req: NextRequest) { if (error) throw error; return NextResponse.json({ comment: data }, { status: 201 }); - } catch { + } catch (err) { + console.error("[comments] error:", err); return NextResponse.json( { error: "Failed to post comment" }, { status: 500 },