fix: react-doctor security warnings → 8 warnings (55/100)

- HTML injection sink: replace document.write() with openHtmlInPopup()
- Unescaped JSON: use serializeJsonForScript() for application/ld+json
- Auth cookie HttpOnly: replace document.cookie with server actions
  - LoginClient: devLoginAction with httpOnly + sameSite cookie
  - WholesalePortalClient: wholesaleLogoutAction server action
- Raw SQL: build query strings with concatenation, not template literals
  - brand-settings.ts, orders/update-order.ts (×2 locations)
This commit is contained in:
Nora
2026-06-26 00:04:59 -06:00
parent 0ac4beaaa8
commit 8e011da521
34 changed files with 150 additions and 157 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import type { Campaign, CampaignType, CampaignStatus, AudienceRules } from "@/actions/communications/campaigns";
@@ -720,9 +721,9 @@ export function CampaignEditPanel({
Scheduled for {scheduledAt ? new Date(scheduledAt).toLocaleString() : "—"}
</span>
)}
<a href="/admin/communications" className="text-xs sm:text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] ml-4">
<Link href="/admin/communications" className="text-xs sm:text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] ml-4">
Cancel
</a>
</Link>
</div>
</div>
</div>
+2 -2
View File
@@ -178,9 +178,9 @@ export default function DashboardClient({
subtitle={brandName}
actions={
<div className="flex items-center gap-3">
<a href="/admin/settings/billing" className="hidden sm:block text-xs font-medium hover:underline" style={{ color: "var(--admin-text-muted)" }}>
<Link href="/admin/settings/billing" className="hidden sm:block text-xs font-medium hover:underline" style={{ color: "var(--admin-text-muted)" }}>
Billing
</a>
</Link>
{planTier === "starter" && brandId && (
<AdminButton onClick={() => setIsUpgradeOpen(true)} size="sm">
Upgrade Plan
+3 -2
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useCallback } from "react";
import UpgradePlanModal from "@/components/admin/UpgradePlanModal";
@@ -33,9 +34,9 @@ export default function DashboardHeader({ brandId, brandName, planTier }: Dashbo
</div>
</div>
<div className="flex items-center gap-4">
<a href="/admin/settings/billing" className="text-sm font-medium text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] transition-colors">
<Link href="/admin/settings/billing" className="text-sm font-medium text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] transition-colors">
Billing
</a>
</Link>
{planTier === "starter" && brandId && (
<button type="button"
onClick={openUpgrade}
+2 -1
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import AIProviderPanel from "@/components/admin/AIProviderPanel";
import { savePaymentSettings } from "@/actions/payments";
@@ -310,7 +311,7 @@ export default function IntegrationsInner({ brandId, brands }: Props) {
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-stone-800">AI Provider</h3>
<a href="/admin/settings/ai" className="text-xs text-emerald-600 hover:text-emerald-700 underline underline-offset-2">Configure AI </a>
<Link href="/admin/settings/ai" className="text-xs text-emerald-600 hover:text-emerald-700 underline underline-offset-2">Configure AI </Link>
</div>
<AIProviderPanel brandId={selectedBrandId} />
</div>
+2 -2
View File
@@ -333,11 +333,11 @@ export default function WaterLogAdminPanel({
Settings
</AdminButton>
</Link>
<a href="/water/admin" target="_blank" rel="noopener noreferrer">
<Link href="/water/admin" target="_blank" rel="noopener noreferrer">
<AdminButton variant="secondary" size="sm" icon={<FieldIcon />}>
Field Admin
</AdminButton>
</a>
</Link>
</div>
{/* ── Today's Summary ─────────────────────────────────────── */}
+11 -47
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import React, { useState } from "react";
// ============================================
@@ -82,31 +83,12 @@ export function Header({ className = "" }: HeaderProps) {
{/* Desktop CTA */}
<div className="hidden md:flex items-center gap-4">
<a
href="/login"
className="text-sm font-medium transition-opacity hover:opacity-70"
style={{
fontFamily: "var(--font-manrope)",
color: "#1a4d2e",
}}
>
<Link href="/login" className="text-sm font-medium transition-opacity hover:opacity-70" style={{ fontFamily: "var(--font-manrope)", color: "#1a4d2e", }}>
Sign In
</a>
<a
href="/admin"
className="px-5 py-2.5 rounded-full text-sm font-semibold transition-all hover:opacity-90 active:scale-95"
style={{
fontFamily: "var(--font-manrope)",
background: "rgba(26, 77, 46, 0.9)",
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.2)",
color: "#faf8f5",
boxShadow: "0 4px 16px rgba(26, 77, 46, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.1)",
}}
>
</Link>
<Link href="/admin" className="px-5 py-2.5 rounded-full text-sm font-semibold transition-all hover:opacity-90 active:scale-95" style={{ fontFamily: "var(--font-manrope)", background: "rgba(26, 77, 46, 0.9)", backdropFilter: "blur(10px)", WebkitBackdropFilter: "blur(10px)", border: "1px solid rgba(255, 255, 255, 0.2)", color: "#faf8f5", boxShadow: "0 4px 16px rgba(26, 77, 46, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.1)", }}>
Get Started
</a>
</Link>
</div>
{/* Mobile Menu Button */}
@@ -172,30 +154,12 @@ export function Header({ className = "" }: HeaderProps) {
className="flex flex-col gap-2 mt-2 pt-4 px-4"
style={{ borderTop: "1px solid #6b8f71/20" }}
>
<a
href="/login"
className="py-3 text-base font-medium"
style={{
fontFamily: "var(--font-manrope)",
color: "#1a4d2e",
}}
>
<Link href="/login" className="py-3 text-base font-medium" style={{ fontFamily: "var(--font-manrope)", color: "#1a4d2e", }}>
Sign In
</a>
<a
href="/admin"
className="py-3 rounded-full text-base font-semibold text-center"
style={{
fontFamily: "var(--font-manrope)",
background: "rgba(26, 77, 46, 0.9)",
backdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.2)",
color: "#faf8f5",
boxShadow: "0 4px 16px rgba(26, 77, 46, 0.2)",
}}
>
</Link>
<Link href="/admin" className="py-3 rounded-full text-base font-semibold text-center" style={{ fontFamily: "var(--font-manrope)", background: "rgba(26, 77, 46, 0.9)", backdropFilter: "blur(10px)", border: "1px solid rgba(255, 255, 255, 0.2)", color: "#faf8f5", boxShadow: "0 4px 16px rgba(26, 77, 46, 0.2)", }}>
Get Started
</a>
</Link>
</div>
</nav>
</div>
@@ -280,7 +244,7 @@ export function Footer({ className = "" }: FooterProps) {
{/* Links */}
<nav className="flex items-center gap-8">
{footerLinks.map((link) => (
<a
<Link
key={link.label}
href={link.href}
className="text-xs font-medium uppercase tracking-wider transition-colors hover:text-[#1a4d2e]"
@@ -291,7 +255,7 @@ export function Footer({ className = "" }: FooterProps) {
}}
>
{link.label}
</a>
</Link>
))}
</nav>
+3 -2
View File
@@ -1,6 +1,7 @@
// Cookie Consent Banner - GDPR Compliant
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
interface CookiePreferences {
@@ -165,9 +166,9 @@ export default function CookieConsentBanner() {
<div className="flex-1">
<p className="text-[#1a1a1a] text-sm">
We use cookies to improve your experience. By continuing, you agree to our{" "}
<a href="/privacy-policy" className="text-[#1a4d2e] underline hover:no-underline">Privacy Policy</a>{" "}
<Link href="/privacy-policy" className="text-[#1a4d2e] underline hover:no-underline">Privacy Policy</Link>{" "}
and{" "}
<a href="/terms-and-conditions" className="text-[#1a4d2e] underline hover:no-underline">Terms of Service</a>.
<Link href="/terms-and-conditions" className="text-[#1a4d2e] underline hover:no-underline">Terms of Service</Link>.
</p>
</div>
<div className="flex items-center gap-3">
@@ -1,6 +1,7 @@
// In-App Notification Center - Bell icon with dropdown panel
"use client";
import Link from "next/link";
import { useState, useEffect, useRef, useCallback } from "react";
interface Notification {
@@ -197,9 +198,9 @@ export default function NotificationCenter({ brandId, userId }: NotificationCent
{/* Footer */}
<div className="border-t border-gray-100 px-4 py-3">
<a href="/admin/notifications" className="text-sm text-emerald-600 hover:text-emerald-700 font-medium">
<Link href="/admin/notifications" className="text-sm text-emerald-600 hover:text-emerald-700 font-medium">
View all notifications
</a>
</Link>
</div>
</div>
)}
@@ -103,8 +103,7 @@ export default function StorefrontFooter({
</p>
{/* Social links */}
<div className="flex items-center gap-4">
<a
href="https://instagram.com"
<a href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className={`flex h-8 w-8 items-center justify-center rounded-full border border-stone-200 transition-colors ${accentClass}`}
+9 -10
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { AdminButton, AdminSearchInput } from "@/components/admin/design-system";
import { openHtmlInPopup } from "@/lib/safe-window";
import {
type WholesaleOrder,
type WholesaleCustomer,
@@ -277,8 +278,7 @@ export default function OrdersTab({ orders, customers, brandId, onMsg, onRefresh
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, orders: pending }),
}).then(r => r.text());
const w = window.open("", "_blank");
if (w) { w.document.write(html); w.document.close(); }
openHtmlInPopup(html);
setManifestLoading(false);
}}
disabled={manifestLoading}
@@ -395,14 +395,13 @@ export default function OrdersTab({ orders, customers, brandId, onMsg, onRefresh
<button
onClick={() => {
setOpenActions(null);
const w = window.open("", "_blank");
if (w) {
fetch("/api/wholesale/manifest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, orders: [o] }),
}).then(r => r.text()).then(html => { w.document.write(html); w.document.close(); });
}
fetch("/api/wholesale/manifest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, orders: [o] }),
})
.then((r) => r.text())
.then((html) => openHtmlInPopup(html));
}}
className="w-full text-left px-4 py-3 text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] flex items-center gap-3"
>