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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ─────────────────────────────────────── */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user