-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add articles #25
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
Changes from all commits
c131ae3
5b32b7d
f37b940
e413b40
a0ddf0c
4d914c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -61,6 +61,28 @@ function isSafeUrl(candidate: string): boolean { | |||||||||||||||
| return !PRIVATE_HOSTNAME_PATTERNS.some((pattern) => pattern.test(hostname)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const ALLOWED_ORIGINS = [ | ||||||||||||||||
| "https://devhub.vercel.app", | ||||||||||||||||
| "http://localhost:3000", | ||||||||||||||||
| "http://127.0.0.1:3000", | ||||||||||||||||
| ]; | ||||||||||||||||
|
|
||||||||||||||||
| function isAllowedOrigin(request: NextRequest): boolean { | ||||||||||||||||
| const origin = request.headers.get("origin"); | ||||||||||||||||
| if (origin) { | ||||||||||||||||
| return ALLOWED_ORIGINS.includes(origin); | ||||||||||||||||
| } | ||||||||||||||||
| const referer = request.headers.get("referer"); | ||||||||||||||||
| if (referer) { | ||||||||||||||||
| try { | ||||||||||||||||
| return ALLOWED_ORIGINS.includes(new URL(referer).origin); | ||||||||||||||||
| } catch { | ||||||||||||||||
| return false; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| return false; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export async function GET(request: NextRequest) { | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL SECURITY REGRESSION: The origin validation check has been removed from the link preview API endpoint. This removes CSRF protection and opens the endpoint to abuse from any origin, enabling potential SSRF attacks, internal network scanning, and rate limit bypass. This reverses the security improvement that was previously implemented. Confidence: 5/5 Suggested Fix
Suggested change
Restore the origin validation check that was removed. This is essential for:
Prompt for AICopy this prompt to your AI IDE to fix this issue locally: |
||||||||||||||||
| const { searchParams } = new URL(request.url); | ||||||||||||||||
| const url = searchParams.get("url"); | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,343 @@ | ||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import ShinyText from "@/components/bits/ShinyText"; | ||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||
| fadeInUp, | ||||||||||||||||||||||||||||||||
| staggerContainer, | ||||||||||||||||||||||||||||||||
| staggerContainerFast, | ||||||||||||||||||||||||||||||||
| } from "@/lib/animations"; | ||||||||||||||||||||||||||||||||
| import { accent, background, indigo, text } from "@/lib/colors"; | ||||||||||||||||||||||||||||||||
| import { motion } from "framer-motion"; | ||||||||||||||||||||||||||||||||
| import { Calendar, Clock, Tag } from "lucide-react"; | ||||||||||||||||||||||||||||||||
| import Image from "next/image"; | ||||||||||||||||||||||||||||||||
| import Link from "next/link"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Article type (must match articles-loader but client-safe — no fs) | ||||||||||||||||||||||||||||||||
| interface ArticleCard { | ||||||||||||||||||||||||||||||||
| slug: string; | ||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||
| description: string; | ||||||||||||||||||||||||||||||||
| banner: string; | ||||||||||||||||||||||||||||||||
| author: string; | ||||||||||||||||||||||||||||||||
| date: string; | ||||||||||||||||||||||||||||||||
| tags: string[]; | ||||||||||||||||||||||||||||||||
| readingTime: string; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| interface Props { | ||||||||||||||||||||||||||||||||
| articles: ArticleCard[]; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export default function ArticlesListingClient({ articles }: Props) { | ||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="min-h-screen relative" | ||||||||||||||||||||||||||||||||
| style={{ background: background.primary }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <div className="absolute inset-0 dot-bg opacity-50 pointer-events-none" /> | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="absolute top-0 right-0 w-[600px] h-[600px] pointer-events-none" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| background: `radial-gradient(ellipse, ${indigo(0.06)} 0%, transparent 70%)`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Corner brackets */} | ||||||||||||||||||||||||||||||||
| <div className="absolute top-20 left-8 hidden md:block"> | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| borderTop: `1px solid ${indigo(0.4)}`, | ||||||||||||||||||||||||||||||||
| borderLeft: `1px solid ${indigo(0.4)}`, | ||||||||||||||||||||||||||||||||
| width: 24, | ||||||||||||||||||||||||||||||||
| height: 24, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| <div className="absolute top-20 right-8 hidden md:block"> | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| borderTop: `1px solid ${indigo(0.4)}`, | ||||||||||||||||||||||||||||||||
| borderRight: `1px solid ${indigo(0.4)}`, | ||||||||||||||||||||||||||||||||
| width: 24, | ||||||||||||||||||||||||||||||||
| height: 24, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <div className="max-w-6xl mx-auto px-6 pt-32 pb-24"> | ||||||||||||||||||||||||||||||||
| {/* Page header */} | ||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||
| variants={staggerContainer} | ||||||||||||||||||||||||||||||||
| initial="hidden" | ||||||||||||||||||||||||||||||||
| animate="visible" | ||||||||||||||||||||||||||||||||
| className="mb-14 text-center" | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||
| variants={fadeInUp} | ||||||||||||||||||||||||||||||||
| className="flex items-center justify-center gap-3 mb-4" | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| color: indigo(0.5), | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {"{"} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| className="text-xs tracking-widest uppercase" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| color: accent.indigoLightest, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| Articles | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| color: indigo(0.5), | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {"}"} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| </motion.div> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <motion.h1 | ||||||||||||||||||||||||||||||||
| variants={fadeInUp} | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-pixelify), 'Pixelify Sans', monospace", | ||||||||||||||||||||||||||||||||
| fontSize: "clamp(2.2rem, 5vw, 3.8rem)", | ||||||||||||||||||||||||||||||||
| lineHeight: 1.15, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| className="mb-5" | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| background: `linear-gradient(135deg, ${text.primary} 0%, ${text.secondary} 40%, ${accent.indigoLightest} 70%, ${accent.violet} 100%)`, | ||||||||||||||||||||||||||||||||
| WebkitBackgroundClip: "text", | ||||||||||||||||||||||||||||||||
| WebkitTextFillColor: "transparent", | ||||||||||||||||||||||||||||||||
| backgroundClip: "text", | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| Community{" "} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| color: accent.indigoLight, | ||||||||||||||||||||||||||||||||
| WebkitTextFillColor: accent.indigoLight, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <ShinyText | ||||||||||||||||||||||||||||||||
| text="Articles" | ||||||||||||||||||||||||||||||||
| className="cursor-target" | ||||||||||||||||||||||||||||||||
| speed={3.5} | ||||||||||||||||||||||||||||||||
| delay={1} | ||||||||||||||||||||||||||||||||
| color={accent.indigoLight} | ||||||||||||||||||||||||||||||||
| shineColor={accent.indigoShine} | ||||||||||||||||||||||||||||||||
| spread={90} | ||||||||||||||||||||||||||||||||
| direction="left" | ||||||||||||||||||||||||||||||||
| yoyo={false} | ||||||||||||||||||||||||||||||||
| pauseOnHover={false} | ||||||||||||||||||||||||||||||||
| disabled={false} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| </motion.h1> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <motion.p | ||||||||||||||||||||||||||||||||
| variants={fadeInUp} | ||||||||||||||||||||||||||||||||
| className="text-sm md:text-base max-w-xl mx-auto leading-relaxed" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono), 'Geist Mono', monospace", | ||||||||||||||||||||||||||||||||
| color: text.dim, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| Guides, releases, and deep dives written by the DevHub community. | ||||||||||||||||||||||||||||||||
| </motion.p> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||
| variants={fadeInUp} | ||||||||||||||||||||||||||||||||
| className="mt-10 mx-auto" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| height: "1px", | ||||||||||||||||||||||||||||||||
| maxWidth: 280, | ||||||||||||||||||||||||||||||||
| background: `linear-gradient(90deg, transparent, ${indigo(0.35)}, transparent)`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </motion.div> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Articles grid */} | ||||||||||||||||||||||||||||||||
| {/* Articles grid */} | ||||||||||||||||||||||||||||||||
| {articles.length === 0 ? ( | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="text-center py-24" | ||||||||||||||||||||||||||||||||
| style={{ fontFamily: "var(--font-geist-mono)", color: text.dim }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| No articles yet. Check back soon. | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||
| variants={staggerContainerFast} | ||||||||||||||||||||||||||||||||
| initial="hidden" | ||||||||||||||||||||||||||||||||
| animate="visible" | ||||||||||||||||||||||||||||||||
| className={ | ||||||||||||||||||||||||||||||||
| articles.length < 3 | ||||||||||||||||||||||||||||||||
| ? "flex flex-wrap justify-center gap-6" | ||||||||||||||||||||||||||||||||
| : "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {articles.map((article) => ( | ||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||
| key={article.slug} | ||||||||||||||||||||||||||||||||
| variants={fadeInUp} | ||||||||||||||||||||||||||||||||
| className={ | ||||||||||||||||||||||||||||||||
| articles.length < 3 | ||||||||||||||||||||||||||||||||
| ? "w-full md:w-[calc(50%-12px)] lg:w-[360px]" | ||||||||||||||||||||||||||||||||
| : "" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <Link | ||||||||||||||||||||||||||||||||
| href={`/articles/${article.slug}`} | ||||||||||||||||||||||||||||||||
| className="group block h-full" | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <motion.article | ||||||||||||||||||||||||||||||||
| className="relative h-full flex flex-col overflow-hidden" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| background: "rgba(7, 7, 15, 0.7)", | ||||||||||||||||||||||||||||||||
| border: `1px solid ${indigo(0.12)}`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| whileHover={{ y: -4 }} | ||||||||||||||||||||||||||||||||
| transition={{ duration: 0.25, ease: "easeOut" }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {/* Banner image */} | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="relative overflow-hidden" | ||||||||||||||||||||||||||||||||
| style={{ height: "200px" }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {article.banner ? ( | ||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||||||||||
| src={article.banner} | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested FixReplace the native
Suggested change
Then replace the img tag with:
Suggested change
This provides automatic image optimization, lazy loading, and better security against malicious URLs. Prompt for AICopy this prompt to your AI IDE to fix this issue locally:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested Fix
Suggested change
Replace the <Image
src={article.banner}
alt={article.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>This provides automatic image optimization, lazy loading, and better security through Next.js's built-in protections. Prompt for AICopy this prompt to your AI IDE to fix this issue locally: |
||||||||||||||||||||||||||||||||
| alt={article.title} | ||||||||||||||||||||||||||||||||
| fill | ||||||||||||||||||||||||||||||||
|
Comment on lines
+218
to
+222
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested Fix
Suggested change
Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 218-222 |
||||||||||||||||||||||||||||||||
| className="object-cover transition-transform duration-500 group-hover:scale-105" | ||||||||||||||||||||||||||||||||
| unoptimized={article.banner.startsWith("http")} | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested FixInstead of disabling optimization for external images, configure Next.js to allow external image optimization. Remove the // next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**', // Or specify exact domains for better security
},
],
},
}Then update the component to remove the
Suggested change
This allows Next.js to optimize all images (both local and external) while maintaining security through the Prompt for AICopy this prompt to your AI IDE to fix this issue locally: |
||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="absolute inset-0" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| background: `linear-gradient(to bottom, transparent 50%, rgba(7,7,15,0.85) 100%)`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="w-full h-full flex items-center justify-center" | ||||||||||||||||||||||||||||||||
| style={{ background: indigo(0.06) }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| style={{ color: indigo(0.3), fontSize: "2rem" }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| ✦ | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Top-right corner bracket accent */} | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="absolute top-3 right-3 w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| borderTop: `1.5px solid ${indigo(0.6)}`, | ||||||||||||||||||||||||||||||||
| borderRight: `1.5px solid ${indigo(0.6)}`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Card body */} | ||||||||||||||||||||||||||||||||
| <div className="flex flex-col flex-1 p-5"> | ||||||||||||||||||||||||||||||||
| {/* Tags */} | ||||||||||||||||||||||||||||||||
| {article.tags.length > 0 && ( | ||||||||||||||||||||||||||||||||
| <div className="flex flex-wrap gap-1.5 mb-3"> | ||||||||||||||||||||||||||||||||
| {article.tags.slice(0, 3).map((tag) => ( | ||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||
| key={tag} | ||||||||||||||||||||||||||||||||
| className="inline-flex items-center gap-1 px-2 py-0.5 text-xs" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| background: indigo(0.08), | ||||||||||||||||||||||||||||||||
| border: `1px solid ${indigo(0.18)}`, | ||||||||||||||||||||||||||||||||
| color: accent.indigoLightest, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <Tag className="w-2 h-2" /> | ||||||||||||||||||||||||||||||||
| {tag} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Title */} | ||||||||||||||||||||||||||||||||
| <h2 | ||||||||||||||||||||||||||||||||
| className="font-bold text-base mb-2 leading-snug transition-colors group-hover:text-[#a5b4fc]" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| color: text.primary, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {article.title} | ||||||||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Description */} | ||||||||||||||||||||||||||||||||
| <p | ||||||||||||||||||||||||||||||||
| className="text-xs leading-relaxed flex-1 mb-4" | ||||||||||||||||||||||||||||||||
| style={ | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| color: text.dim, | ||||||||||||||||||||||||||||||||
| display: "-webkit-box", | ||||||||||||||||||||||||||||||||
| WebkitLineClamp: 3, | ||||||||||||||||||||||||||||||||
| WebkitBoxOrient: "vertical", | ||||||||||||||||||||||||||||||||
| overflow: "hidden", | ||||||||||||||||||||||||||||||||
| } as React.CSSProperties | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| {article.description} | ||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Footer meta */} | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| className="flex items-center justify-between text-xs pt-3" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| fontFamily: "var(--font-geist-mono)", | ||||||||||||||||||||||||||||||||
| borderTop: `1px solid ${indigo(0.08)}`, | ||||||||||||||||||||||||||||||||
| color: text.veryDim, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <span className="flex items-center gap-1.5"> | ||||||||||||||||||||||||||||||||
| <Calendar className="w-3 h-3" /> | ||||||||||||||||||||||||||||||||
| {article.date} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| <span className="flex items-center gap-1.5"> | ||||||||||||||||||||||||||||||||
| <Clock className="w-3 h-3" /> | ||||||||||||||||||||||||||||||||
| {article.readingTime} | ||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {/* Hover bottom border accent */} | ||||||||||||||||||||||||||||||||
| {/* <div | ||||||||||||||||||||||||||||||||
| className="absolute bottom-0 left-0 right-0 h-0.5 origin-left scale-x-0 group-hover:scale-x-100 transition-transform duration-300" | ||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||
| background: `linear-gradient(90deg, ${accent.indigo}, ${accent.violet}, transparent)`, | ||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||
| /> */} | ||||||||||||||||||||||||||||||||
| </motion.article> | ||||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||||
| </motion.div> | ||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||
| </motion.div> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
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.
The
isAllowedOriginfunction has a logic flaw: it returnsfalsewhen bothoriginandrefererheaders are missing (line 83). However, the caller at line 87 treatsfalseas "forbidden". This means legitimate requests without these headers (e.g., direct API calls, some mobile clients, or privacy-focused browsers) will be blocked.Additionally, if this is intended as CORS protection, the implementation is incomplete - it validates the origin but doesn't set CORS response headers, which means browsers will still block the response even if the origin is allowed.
Confidence: 4/5
Suggested Fix
Consider the intended behavior:
Option 1: If requests without origin/referer should be allowed (more permissive):
Option 2: If this is meant to be strict CORS protection, add proper CORS headers in the response:
After line 89, add CORS headers to the response throughout the function:
And include these headers in all
NextResponse.json()calls.Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
📍 This suggestion applies to lines 70-84