1→"use client";
     2→
     3→import { useState, useRef, useCallback, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import Link from "next/link";
     7→import toast from "react-hot-toast";
     8→import { createClient } from "@/lib/supabase/client";
     9→import { Button } from "@/components/ui/button";
    10→import { Card, CardContent } from "@/components/ui/card";
    11→import { Input } from "@/components/ui/input";
    12→import { Label } from "@/components/ui/label";
    13→import { Badge } from "@/components/ui/badge";
    14→import {
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  Sparkles,
    19→  Check,
    20→  MapPin,
    21→  Calendar,
    22→  Users,
    23→  DollarSign,
    24→  HardHat,
    25→  Wrench,
    26→  Hammer,
    27→  Blocks,
    28→  Fence,
    29→  CircleDot,
    30→  Shovel,
    31→  Mic,
    32→  Square,
    33→} from "lucide-react";
    34→import { jsonToPostgresPoint } from "@/lib/location-utils";
    35→import { csrfFetch, withCSRFToken, getCSRFCookie, initializeCSRF, addCSRFTokenToFormData } from "@/lib/csrf-client";
    36→import { stripContactInfo } from "@/lib/sanitize";
    37→
    38→const TRADE_CATEGORIES = [
    39→  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
    40→  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
    41→  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
    42→  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
    43→  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
    44→  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
    45→  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
    46→];
    47→
    48→const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
    49→  "steel-fixer": {
    50→    title: "Steel Fixer Needed",
    51→    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    52→    certs: ["White Card", "Working at Heights"],
    53→    rateMin: 45,
    54→    rateMax: 65,
    55→    rateType: "hourly",
    56→  },
    57→  "formworker": {
    58→    title: "Formworker Required",
    59→    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    60→    certs: ["White Card"],
    61→    rateMin: 42,
    62→    rateMax: 58,
    63→    rateType: "hourly",
    64→  },
    65→  "labourer": {
    66→    title: "General Labourer Needed",
    67→    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    68→    certs: ["White Card"],
    69→    rateMin: 32,
    70→    rateMax: 42,
    71→    rateType: "hourly",
    72→  },
    73→  "concreter": {
    74→    title: "Concreter Required",
    75→    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    76→    certs: ["White Card"],
    77→    rateMin: 45,
    78→    rateMax: 65,
    79→    rateType: "hourly",
    80→  },
    81→  "carpenter": {
    82→    title: "Carpenter Needed",
    83→    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    84→    certs: ["White Card", "Trade Certificate"],
    85→    rateMin: 45,
    86→    rateMax: 65,
    87→    rateType: "hourly",
    88→  },
    89→  "scaffolder": {
    90→    title: "Scaffolder Required",
    91→    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    92→    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    93→    rateMin: 48,
    94→    rateMax: 68,
    95→    rateType: "hourly",
    96→  },
    97→  "other": {
    98→    title: "Construction Worker Needed",
    99→    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
   100→    certs: ["White Card"],
   101→    rateMin: 35,
   102→    rateMax: 55,
   103→    rateType: "hourly",
   104→  },
   105→};
   106→
   107→export default function PostJobPage() {
   108→  const router = useRouter();
   109→  const supabase = createClient();
   110→
   111→  // Ensure CSRF token is available (defensive — may have been missed during signup)
   112→  useEffect(() => {
   113→    if (!getCSRFCookie()) {
   114→      initializeCSRF();
   115→    }
   116→  }, []);
   117→
   118→  const [step, setStep] = useState(1);
   119→  const [loading, setLoading] = useState(false);
   120→  const [aiLoading, setAiLoading] = useState(false);
   121→  const [aiLoadingMessage, setAiLoadingMessage] = useState("");
   122→
   123→  // Voice recording state
   124→  const [voiceRecording, setVoiceRecording] = useState(false);
   125→  const [voiceStarting, setVoiceStarting] = useState(false);
   126→  const [voiceError, setVoiceError] = useState<string | null>(null);
   127→  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
   128→  const chunksRef = useRef<Blob[]>([]);
   129→
   130→  // Step 1: Trade
   131→  const [selectedTrade, setSelectedTrade] = useState("");
   132→
   133→  // Step 2: AI-filled job details + location/date/workers (COMBINED)
   134→  const [title, setTitle] = useState("");
   135→  const [description, setDescription] = useState("");
   136→  const [certs, setCerts] = useState<string[]>([]);
   137→  const [rateMin, setRateMin] = useState("");
   138→  const [rateMax, setRateMax] = useState("");
   139→  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
   140→  const [suburb, setSuburb] = useState("");
   141→  const [siteAddress, setSiteAddress] = useState("");
   142→  const [startDate, setStartDate] = useState("");
   143→  const [duration, setDuration] = useState("");
   144→  const [workersNeeded, setWorkersNeeded] = useState("1");
   145→
   146→  // Voice recording handlers
   147→  const startVoiceRecording = useCallback(async () => {
   148→    setVoiceError(null);
   149→    setVoiceStarting(true);
   150→    try {
   151→      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
   152→      const mediaRecorder = new MediaRecorder(stream, {
   153→        mimeType: MediaRecorder.isTypeSupported("audio/webm")
   154→          ? "audio/webm"
   155→          : "audio/mp4",
   156→      });
   157→      mediaRecorderRef.current = mediaRecorder;
   158→      chunksRef.current = [];
   159→
   160→      mediaRecorder.ondataavailable = (e) => {
   161→        if (e.data.size > 0) {
   162→          chunksRef.current.push(e.data);
   163→        }
   164→      };
   165→
   166→      mediaRecorder.onstop = () => {
   167→        const blob = new Blob(chunksRef.current, { type: mediaRecorder.mimeType });
   168→        stream.getTracks().forEach((track) => track.stop());
   169→        handleVoiceBlob(blob);
   170→      };
   171→
   172→      mediaRecorder.start();
   173→      setVoiceRecording(true);
   174→    } catch (err) {
   175→      if (err instanceof DOMException) {
   176→        switch (err.name) {
   177→          case "NotAllowedError":
   178→            setVoiceError("Microphone access denied. Please allow microphone access in your browser settings.");
   179→            break;
   180→          case "NotFoundError":
   181→            setVoiceError("No microphone found. Please connect a microphone and try again.");
   182→            break;
   183→          case "AbortError":
   184→            break;
   185→          case "SecurityError":
   186→            setVoiceError("Microphone access requires a secure connection (HTTPS).");
   187→            break;
   188→          default:
   189→            setVoiceError("Could not access microphone. Please try again.");
   190→        }
   191→      } else {
   192→        setVoiceError("Could not access microphone. Please try again.");
   193→      }
   194→    } finally {
   195→      setVoiceStarting(false);
   196→    }
   197→  }, []);
   198→
   199→  const stopVoiceRecording = useCallback(() => {
   200→    if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
   201→      mediaRecorderRef.current.stop();
   202→      setVoiceRecording(false);
   203→    }
   204→  }, []);
   205→
   206→  async function handleVoiceBlob(blob: Blob) {
   207→    setAiLoading(true);
   208→    setAiLoadingMessage("Listening to your voice...");
   209→    setStep(2);
   210→
   211→    try {
   212→      const formData = new FormData();
   213→      formData.append("audio", blob, "recording.webm");
   214→      addCSRFTokenToFormData(formData);
   215→
   216→      setAiLoadingMessage("AI writing your job post...");
   217→
   218→      const res = await fetch("/api/ai/voice-to-job", withCSRFToken({
   219→        method: "POST",
   220→        body: formData,
   221→      }));
   222→
   223→      if (!res.ok) {
   224→        const errData = await res.json().catch(() => ({}));
   225→        throw new Error(errData.error || "Failed to process voice");
   226→      }
   227→
   228→      const data = await res.json();
   229→
   230→      if (data.fallback) {
   231→        toast.error("AI couldn\u2019t fully process your voice. Please fill in the details.");
   232→      }
   233→
   234→      // Auto-fill all form fields from voice
   235→      if (data.trade) setSelectedTrade(data.trade);
   236→      if (data.title) setTitle(data.title);
   237→      if (data.description) setDescription(data.description);
   238→      if (data.certifications) setCerts(data.certifications);
   239→      if (data.rateMin != null) {
   240→        setRateMin(String(data.rateMin));
   241→        // Default max to min unless AI explicitly gave a different max
   242→        setRateMax(String(data.rateMax ?? data.rateMin));
   243→      }
   244→      if (data.rateType) setRateType(data.rateType);
   245→      if (data.suburb) setSuburb(data.suburb);
   246→      if (data.siteAddress) setSiteAddress(data.siteAddress);
   247→      if (data.startDate) setStartDate(data.startDate);
   248→      if (data.duration) setDuration(data.duration);
   249→      if (data.workersNeeded) setWorkersNeeded(String(data.workersNeeded));
   250→    } catch (error) {
   251→      logger.error("Voice-to-job error:", error);
   252→      toast.error("Voice processing failed. Try again or type your job details.");
   253→      setStep(1);
   254→    } finally {
   255→      setAiLoading(false);
   256→      setAiLoadingMessage("");
   257→    }
   258→  }
   259→
   260→  async function handleTradeSelect(tradeId: string) {
   261→    setSelectedTrade(tradeId);
   262→    setAiLoading(true);
   263→    setStep(2);
   264→
   265→    // Try real AI generation, fall back to hardcoded suggestions
   266→    try {
   267→      const res = await csrfFetch("/api/ai/generate-job", {
   268→        method: "POST",
   269→        headers: { "Content-Type": "application/json" },
   270→        body: JSON.stringify({ trade: tradeId }),
   271→      });
   272→
   273→      if (res.ok) {
   274→        const data = await res.json();
   275→        setTitle(data.title);
   276→        setDescription(data.description);
   277→        setCerts(data.certifications);
   278→        setRateMin(String(data.rateMin));
   279→        setRateMax(String(data.rateMax));
   280→        setRateType(data.rateType);
   281→        setAiLoading(false);
   282→        return;
   283→      }
   284→    } catch {
   285→      // Fall through to hardcoded fallback
   286→    }
   287→
   288→    // Fallback: use hardcoded suggestions
   289→    const suggestion = AI_SUGGESTIONS[tradeId];
   290→    if (suggestion) {
   291→      setTitle(suggestion.title);
   292→      setDescription(suggestion.description);
   293→      setCerts(suggestion.certs);
   294→      setRateMin(String(suggestion.rateMin));
   295→      setRateMax(String(suggestion.rateMax));
   296→      setRateType(suggestion.rateType);
   297→    }
   298→    setAiLoading(false);
   299→  }
   300→
   301→  async function handlePost() {
   302→    // Validate required fields
   303→    if (!title || !suburb || !description.trim()) {
   304→      toast.error("Please fill in the title, description, and suburb.");
   305→      return;
   306→    }
   307→
   308→    const rateMinNum = parseFloat(rateMin);
   309→    const rateMaxNum = parseFloat(rateMax);
   310→
   311→    // Validate rates are positive
   312→    if (rateMin && rateMinNum <= 0) {
   313→      toast.error("Minimum rate must be greater than $0.");
   314→      return;
   315→    }
   316→    if (rateMax && rateMaxNum <= 0) {
   317→      toast.error("Maximum rate must be greater than $0.");
   318→      return;
   319→    }
   320→
   321→    // Validate rateMin <= rateMax
   322→    if (rateMin && rateMax && rateMinNum > rateMaxNum) {
   323→      toast.error("Minimum rate cannot be greater than maximum rate.");
   324→      return;
   325→    }
   326→
   327→    setLoading(true);
   328→
   329→    try {
   330→      const { data: { user } } = await supabase.auth.getUser();
   331→      if (!user) {
   332→        toast.error("Session expired. Please log in again.");
   333→        router.push("/auth/login");
   334→        return;
   335→      }
   336→
   337→      // Get contractor's company
   338→      const { data: company } = await supabase
   339→        .from("companies")
   340→        .select("id")
   341→        .eq("profile_id", user.id)
   342→        .single();
   343→
   344→      if (!company) {
   345→        toast.error("Please complete your company profile first.");
   346→        setLoading(false);
   347→        return;
   348→      }
   349→
   350→      // Geocode the suburb to get lat/lng coordinates
   351→      let location: string | null = null;
   352→      if (suburb) {
   353→        try {
   354→          const geocodeRes = await csrfFetch("/api/geocode", {
   355→            method: "POST",
   356→            headers: { "Content-Type": "application/json" },
   357→            body: JSON.stringify({ suburb }),
   358→          });
   359→
   360→          if (geocodeRes.ok) {
   361→            const geocodeData = await geocodeRes.json();
   362→            if (geocodeData.location) {
   363→              location = jsonToPostgresPoint(geocodeData.location);
   364→            }
   365→          }
   366→        } catch (geoError) {
   367→          // Continue without location - suburb text will still be saved
   368→        }
   369→      }
   370→
   371→      // Strip phone numbers and emails to prevent platform bypass
   372→      const sanitizedTitle = stripContactInfo(title);
   373→      const sanitizedDescription = stripContactInfo(description);
   374→
   375→      const { error: insertError } = await supabase.from("jobs").insert({
   376→        company_id: company.id,
   377→        title: sanitizedTitle,
   378→        description: sanitizedDescription,
   379→        trade: selectedTrade,
   380→        suburb,
   381→        location,
   382→        site_address: siteAddress,
   383→        rate_min: parseFloat(rateMin) || null,
   384→        rate_max: parseFloat(rateMax) || parseFloat(rateMin) || null,
   385→        rate_type: rateType,
   386→        start_date: startDate || null,
   387→        duration: duration || null,
   388→        workers_needed: parseInt(workersNeeded) || 1,
   389→        certifications_required: certs,
   390→        status: "active",
   391→        ai_generated: true,
   392→      });
   393→
   394→      if (insertError) {
   395→        toast.error(`Failed to post job: ${insertError.message}`);
   396→        setLoading(false);
   397→        return;
   398→      }
   399→
   400→      // Record first_job_posted milestone (fire-and-forget)
   401→      fetch('/api/success-tracking/record', {
   402→        method: 'POST',
   403→        headers: { 'Content-Type': 'application/json' },
   404→        body: JSON.stringify({ milestone_type: 'first_job_posted' }),
   405→      }).catch(() => {});
   406→
   407→      setStep(3); // Success (was step 5, now step 3)
   408→    } catch (error) {
   409→      logger.error("Job post error:", error);
   410→      toast.error("Something went wrong. Please try again.");
   411→      setLoading(false);
   412→    }
   413→  }
   414→
   415→  return (
   416→

   417→
   418→        {/* STEP 1: Select Trade or Voice */}
   419→        {step === 1 && (
   420→

   421→            {/* Voice Input — Primary CTA */}
   422→

   423→
Post a Job

   424→

   425→                Tap the mic and describe what you need
   426→

   427→
   428→              <button
   429→                type="button"
   430→                onClick={voiceRecording ? stopVoiceRecording : startVoiceRecording}
   431→                disabled={voiceStarting}
   432→                className={`mt-6 flex h-24 w-24 items-center justify-center rounded-full transition-all active:scale-95 ${
   433→                  voiceRecording
   434→                    ? "animate-pulse bg-red-500 text-white shadow-lg shadow-red-500/40"
   435→                    : "bg-primary text-primary-foreground shadow-lg hover:shadow-xl"
   436→                } ${voiceStarting ? "opacity-50 cursor-not-allowed" : ""}`}
   437→                aria-label={voiceRecording ? "Stop recording" : "Start recording"}
   438→              >
   439→                {voiceStarting ? (
   440→                  <Loader2 className="h-10 w-10 animate-spin" />
   441→                ) : voiceRecording ? (
   442→                  <Square className="h-10 w-10" />
   443→                ) : (
   444→                  <Mic className="h-10 w-10" />
   445→                )}
   446→              </button>
   447→
   448→

   449→                {voiceStarting
   450→                  ? "Starting..."
   451→                  : voiceRecording
   452→                    ? "Listening... tap to stop"
   453→                    : "\"I need two steelfixers in Parramatta, 45 an hour\""}
   454→

   455→
   456→              {voiceError && (
   457→
{voiceError}

   458→              )}
   459→

   460→
   461→            {/* Divider */}
   462→

   463→

   464→              or pick a trade
   465→

   466→

   467→
   468→            {/* Trade Selection Grid */}
   469→

   470→              {TRADE_CATEGORIES.map((trade) => {
   471→                const Icon = trade.icon;
   472→                return (
   473→                  <button
   474→                    key={trade.id}
   475→                    onClick={() => handleTradeSelect(trade.id)}
   476→                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
   477→                  >
   478→                    <Icon className="h-10 w-10" />
   479→                    {trade.label}
   480→                  </button>
   481→                );
   482→              })}
   483→

   484→

   485→        )}
   486→
   487→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   488→        {step === 2 && (
   489→

   490→            {aiLoading ? (
   491→

   492→

   493→                  <Sparkles className="h-12 w-12 text-primary animate-pulse" />
   494→

   495→

   496→                  {aiLoadingMessage || "AI writing your job post..."}
   497→

   498→

   499→                  Based on what you said and market rates
   500→

   501→

   502→            ) : (
   503→

   504→

   505→                  <Sparkles className="h-5 w-5 text-primary" />
   506→
Job Details

   507→

   508→

   509→                  AI pre-filled these. Edit anything you like.
   510→

   511→
   512→

   513→                  {/* Job Details Section */}
   514→

   515→                    Job Title
   516→                    <Input
   517→                      id="title"
   518→                      value={title}
   519→                      onChange={(e) => setTitle(e.target.value)}
   520→                      className="mt-2 h-14 text-base"
   521→                    />
   522→

   523→
   524→

   525→                    Description
   526→                    <textarea
   527→                      id="description"
   528→                      value={description}
   529→                      onChange={(e) => setDescription(e.target.value)}
   530→                      rows={5}
   531→                      maxLength={5000}
   532→                      className="mt-2 flex w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
   533→                    />
   534→

   535→
   536→

   537→                    Pay Rate (AUD)
   538→

   539→

   540→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   541→                        <Input
   542→                          type="number"
   543→                          value={rateMin}
   544→                          onChange={(e) => {
   545→                            setRateMin(e.target.value);
   546→                            // Keep max in sync unless user explicitly set a range
   547→                            if (!rateMax || rateMax === rateMin) {
   548→                              setRateMax(e.target.value);
   549→                            }
   550→                          }}
   551→                          placeholder="e.g. 60"
   552→                          className="h-14 pl-10 text-base"
   553→                        />
   554→

   555→                      {rateMin && rateMax && rateMin !== rateMax && (
   556→

   557→                          to
   558→

   559→                            <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   560→                            <Input
   561→                              type="number"
   562→                              value={rateMax}
   563→                              onChange={(e) => setRateMax(e.target.value)}
   564→                              placeholder="Max"
   565→                              className="h-14 pl-10 text-base"
   566→                            />
   567→

   568→                          <Button
   569→                            type="button"
   570→                            variant="ghost"
   571→                            size="sm"
   572→                            onClick={() => setRateMax(rateMin)}
   573→                            className="text-xs text-muted-foreground shrink-0"
   574→                          >
   575→                            Use exact rate
   576→                          </Button>
   577→

   578→                      )}
   579→

   580→

   581→                      <Button
   582→                        type="button"
   583→                        variant={rateType === "hourly" ? "default" : "outline"}
   584→                        onClick={() => setRateType("hourly")}
   585→                        className="h-12 text-base font-semibold"
   586→                      >
   587→                        Per Hour
   588→                      </Button>
   589→                      <Button
   590→                        type="button"
   591→                        variant={rateType === "daily" ? "default" : "outline"}
   592→                        onClick={() => setRateType("daily")}
   593→                        className="h-12 text-base font-semibold"
   594→                      >
   595→                        Per Day
   596→                      </Button>
   597→

   598→

   599→
   600→                  {/* Location Section */}
   601→

   602→

   603→                      <MapPin className="h-5 w-5" />
   604→                      Location
   605→

   606→
   607→

   608→

   609→                        Site Address
   610→                        <Input
   611→                          id="siteAddress"
   612→                          value={siteAddress}
   613→                          onChange={(e) => setSiteAddress(e.target.value)}
   614→                          placeholder="e.g. 123 George St, Sydney"
   615→                          className="mt-2 h-14 text-base"
   616→                        />
   617→

   618→
   619→

   620→                        Suburb
   621→                        <Input
   622→                          id="suburb"
   623→                          value={suburb}
   624→                          onChange={(e) => setSuburb(e.target.value)}
   625→                          placeholder="e.g. Parramatta"
   626→                          className="mt-2 h-14 text-base"
   627→                        />
   628→

   629→

   630→

   631→
   632→                  {/* Date & Workers Section */}
   633→

   634→

   635→                      <Calendar className="h-5 w-5" />
   636→                      Schedule
   637→

   638→
   639→

   640→

   641→                        Start Date
   642→                        <Input
   643→                          id="startDate"
   644→                          type="date"
   645→                          value={startDate}
   646→                          onChange={(e) => setStartDate(e.target.value)}
   647→                          className="mt-2 h-14 text-base"
   648→                        />
   649→

   650→
   651→

   652→                        Duration
   653→                        <Input
   654→                          id="duration"
   655→                          value={duration}
   656→                          onChange={(e) => setDuration(e.target.value)}
   657→                          placeholder="e.g. 2 weeks, ongoing"
   658→                          className="mt-2 h-14 text-base"
   659→                        />
   660→

   661→
   662→

   663→
   664→                          <Users className="h-5 w-5" />
   665→                          Workers Needed
   666→
   667→

   668→                          <Button
   669→                            type="button"
   670→                            variant="outline"
   671→                            onClick={() =>
   672→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   673→                            }
   674→                            className="h-14 w-14 text-2xl font-bold"
   675→                          >
   676→                            -
   677→                          </Button>
   678→
   679→                            {workersNeeded}
   680→
   681→                          <Button
   682→                            type="button"
   683→                            variant="outline"
   684→                            onClick={() =>
   685→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   686→                            }
   687→                            className="h-14 w-14 text-2xl font-bold"
   688→                          >
   689→                            +
   690→                          </Button>
   691→

   692→

   693→

   694→

   695→
   696→                  {/* Required Certifications */}
   697→                  {certs.length > 0 && (
   698→

   699→                      Required Certifications
   700→

   701→                        {certs.map((cert) => (
   702→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   703→                            {cert}
   704→                            <button
   705→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   706→                              className="ml-2 text-muted-foreground hover:text-foreground"
   707→                            >
   708→                              ×
   709→                            </button>
   710→                          </Badge>
   711→                        ))}
   712→

   713→

   714→                  )}
   715→

   716→

   717→            )}
   718→

   719→        )}
   720→
   721→        {/* STEP 3: Success */}
   722→        {step === 3 && (
   723→          <Card className="mt-8">
   724→            <CardContent className="flex flex-col items-center py-10 text-center">
   725→

   726→                <Check className="h-10 w-10 text-green-600" />
   727→

   728→
Job Posted!

   729→

   730→                We're matching you with the best workers now.
   731→                You'll get notified when we find matches.
   732→

   733→

   734→                <Button
   735→                  onClick={() => router.push("/contractor/matches")}
   736→                  className="w-full h-12 text-base font-semibold"
   737→                >
   738→                  View Matches
   739→                </Button>
   740→                <Button
   741→                  onClick={() => router.push("/dashboard")}
   742→                  variant="outline"
   743→                  className="w-full h-12 text-base"
   744→                >
   745→                  Back to Dashboard
   746→                </Button>
   747→

   748→            </CardContent>
   749→          </Card>
   750→        )}
   751→
   752→
   753→      {/* Sticky Bottom CTA (Step 2 only) */}
   754→      {step === 2 && !aiLoading && (
   755→

   756→

   757→            <Button
   758→              onClick={handlePost}
   759→              className="w-full h-12 text-base font-semibold"
   760→              disabled={loading || !title || !suburb || !description.trim()}
   761→            >
   762→              {loading ? (
   763→                <>
   764→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   765→                  Posting...
   766→                </>
   767→              ) : (
   768→                <>
   769→                  Post Job Now
   770→                  <Check className="ml-2 h-6 w-6" />
   771→                </>
   772→              )}
   773→            </Button>
   774→

   775→              Free to post. You only pay $50 when you hire someone.
   776→

   777→

   778→

   779→      )}
   780→

   781→  );
   782→}
   783→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"use client";

import { useState, useRef, useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import { logger } from "@/lib/logger";
import Link from "next/link";
import toast from "react-hot-toast";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
  ArrowLeft,
  ArrowRight,
  Loader2,
  Sparkles,
  Check,
  MapPin,
  Calendar,
  Users,
  DollarSign,
  HardHat,
  Wrench,
  Hammer,
  Blocks,
  Fence,
  CircleDot,
  Shovel,
  Mic,
  Square,
} from "lucide-react";
import { jsonToPostgresPoint } from "@/lib/location-utils";
import { csrfFetch, withCSRFToken, getCSRFCookie, initializeCSRF, addCSRFTokenToFormData } from "@/lib/csrf-client";
import { stripContactInfo } from "@/lib/sanitize";

const TRADE_CATEGORIES = [
  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
];

const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
  "steel-fixer": {
    title: "Steel Fixer Needed",
    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    certs: ["White Card", "Working at Heights"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "formworker": {
    title: "Formworker Required",
    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    certs: ["White Card"],
    rateMin: 42,
    rateMax: 58,
    rateType: "hourly",
  },
  "labourer": {
    title: "General Labourer Needed",
    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    certs: ["White Card"],
    rateMin: 32,
    rateMax: 42,
    rateType: "hourly",
  },
  "concreter": {
    title: "Concreter Required",
    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    certs: ["White Card"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "carpenter": {
    title: "Carpenter Needed",
    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    certs: ["White Card", "Trade Certificate"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "scaffolder": {
    title: "Scaffolder Required",
    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    rateMin: 48,
    rateMax: 68,
    rateType: "hourly",
  },
  "other": {
    title: "Construction Worker Needed",
    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    certs: ["White Card"],
    rateMin: 35,
    rateMax: 55,
    rateType: "hourly",
  },
};

export default function PostJobPage() {
  const router = useRouter();
  const supabase = createClient();

  // Ensure CSRF token is available (defensive — may have been missed during signup)
  useEffect(() => {
    if (!getCSRFCookie()) {
      initializeCSRF();
    }
  }, []);

  const [step, setStep] = useState(1);
  const [loading, setLoading] = useState(false);
  const [aiLoading, setAiLoading] = useState(false);
  const [aiLoadingMessage, setAiLoadingMessage] = useState("");

  // Voice recording state
  const [voiceRecording, setVoiceRecording] = useState(false);
  const [voiceStarting, setVoiceStarting] = useState(false);
  const [voiceError, setVoiceError] = useState<string | null>(null);
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const chunksRef = useRef<Blob[]>([]);

  // Step 1: Trade
  const [selectedTrade, setSelectedTrade] = useState("");

  // Step 2: AI-filled job details + location/date/workers (COMBINED)
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [certs, setCerts] = useState<string[]>([]);
  const [rateMin, setRateMin] = useState("");
  const [rateMax, setRateMax] = useState("");
  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
  const [suburb, setSuburb] = useState("");
  const [siteAddress, setSiteAddress] = useState("");
  const [startDate, setStartDate] = useState("");
  const [duration, setDuration] = useState("");
  const [workersNeeded, setWorkersNeeded] = useState("1");

  // Voice recording handlers
  const startVoiceRecording = useCallback(async () => {
    setVoiceError(null);
    setVoiceStarting(true);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mediaRecorder = new MediaRecorder(stream, {
        mimeType: MediaRecorder.isTypeSupported("audio/webm")
          ? "audio/webm"
          : "audio/mp4",
      });
      mediaRecorderRef.current = mediaRecorder;
      chunksRef.current = [];

      mediaRecorder.ondataavailable = (e) => {
        if (e.data.size > 0) {
          chunksRef.current.push(e.data);
        }
      };

      mediaRecorder.onstop = () => {
        const blob = new Blob(chunksRef.current, { type: mediaRecorder.mimeType });
        stream.getTracks().forEach((track) => track.stop());
        handleVoiceBlob(blob);
      };

      mediaRecorder.start();
      setVoiceRecording(true);
    } catch (err) {
      if (err instanceof DOMException) {
        switch (err.name) {
          case "NotAllowedError":
            setVoiceError("Microphone access denied. Please allow microphone access in your browser settings.");
            break;
          case "NotFoundError":
            setVoiceError("No microphone found. Please connect a microphone and try again.");
            break;
          case "AbortError":
            break;
          case "SecurityError":
            setVoiceError("Microphone access requires a secure connection (HTTPS).");
            break;
          default:
            setVoiceError("Could not access microphone. Please try again.");
        }
      } else {
        setVoiceError("Could not access microphone. Please try again.");
      }
    } finally {
      setVoiceStarting(false);
    }
  }, []);

  const stopVoiceRecording = useCallback(() => {
    if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
      mediaRecorderRef.current.stop();
      setVoiceRecording(false);
    }
  }, []);

  async function handleVoiceBlob(blob: Blob) {
    setAiLoading(true);
    setAiLoadingMessage("Listening to your voice...");
    setStep(2);

    try {
      const formData = new FormData();
      formData.append("audio", blob, "recording.webm");
      addCSRFTokenToFormData(formData);

      setAiLoadingMessage("AI writing your job post...");

      const res = await fetch("/api/ai/voice-to-job", withCSRFToken({
        method: "POST",
        body: formData,
      }));

      if (!res.ok) {
        const errData = await res.json().catch(() => ({}));
        throw new Error(errData.error || "Failed to process voice");
      }

      const data = await res.json();

      if (data.fallback) {
        toast.error("AI couldn\u2019t fully process your voice. Please fill in the details.");
      }

      // Auto-fill all form fields from voice
      if (data.trade) setSelectedTrade(data.trade);
      if (data.title) setTitle(data.title);
      if (data.description) setDescription(data.description);
      if (data.certifications) setCerts(data.certifications);
      if (data.rateMin != null) {
        setRateMin(String(data.rateMin));
        // Default max to min unless AI explicitly gave a different max
        setRateMax(String(data.rateMax ?? data.rateMin));
      }
      if (data.rateType) setRateType(data.rateType);
      if (data.suburb) setSuburb(data.suburb);
      if (data.siteAddress) setSiteAddress(data.siteAddress);
      if (data.startDate) setStartDate(data.startDate);
      if (data.duration) setDuration(data.duration);
      if (data.workersNeeded) setWorkersNeeded(String(data.workersNeeded));
    } catch (error) {
      logger.error("Voice-to-job error:", error);
      toast.error("Voice processing failed. Try again or type your job details.");
      setStep(1);
    } finally {
      setAiLoading(false);
      setAiLoadingMessage("");
    }
  }

  async function handleTradeSelect(tradeId: string) {
    setSelectedTrade(tradeId);
    setAiLoading(true);
    setStep(2);

    // Try real AI generation, fall back to hardcoded suggestions
    try {
      const res = await csrfFetch("/api/ai/generate-job", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ trade: tradeId }),
      });

      if (res.ok) {
        const data = await res.json();
        setTitle(data.title);
        setDescription(data.description);
        setCerts(data.certifications);
        setRateMin(String(data.rateMin));
        setRateMax(String(data.rateMax));
        setRateType(data.rateType);
        setAiLoading(false);
        return;
      }
    } catch {
      // Fall through to hardcoded fallback
    }

    // Fallback: use hardcoded suggestions
    const suggestion = AI_SUGGESTIONS[tradeId];
    if (suggestion) {
      setTitle(suggestion.title);
      setDescription(suggestion.description);
      setCerts(suggestion.certs);
      setRateMin(String(suggestion.rateMin));
      setRateMax(String(suggestion.rateMax));
      setRateType(suggestion.rateType);
    }
    setAiLoading(false);
  }

  async function handlePost() {
    // Validate required fields
    if (!title || !suburb || !description.trim()) {
      toast.error("Please fill in the title, description, and suburb.");
      return;
    }

    const rateMinNum = parseFloat(rateMin);
    const rateMaxNum = parseFloat(rateMax);

    // Validate rates are positive
    if (rateMin && rateMinNum <= 0) {
      toast.error("Minimum rate must be greater than $0.");
      return;
    }
    if (rateMax && rateMaxNum <= 0) {
      toast.error("Maximum rate must be greater than $0.");
      return;
    }

    // Validate rateMin <= rateMax
    if (rateMin && rateMax && rateMinNum > rateMaxNum) {
      toast.error("Minimum rate cannot be greater than maximum rate.");
      return;
    }

    setLoading(true);

    try {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) {
        toast.error("Session expired. Please log in again.");
        router.push("/auth/login");
        return;
      }

      // Get contractor's company
      const { data: company } = await supabase
        .from("companies")
        .select("id")
        .eq("profile_id", user.id)
        .single();

      if (!company) {
        toast.error("Please complete your company profile first.");
        setLoading(false);
        return;
      }

      // Geocode the suburb to get lat/lng coordinates
      let location: string | null = null;
      if (suburb) {
        try {
          const geocodeRes = await csrfFetch("/api/geocode", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ suburb }),
          });

          if (geocodeRes.ok) {
            const geocodeData = await geocodeRes.json();
            if (geocodeData.location) {
              location = jsonToPostgresPoint(geocodeData.location);
            }
          }
        } catch (geoError) {
          // Continue without location - suburb text will still be saved
        }
      }

      // Strip phone numbers and emails to prevent platform bypass
      const sanitizedTitle = stripContactInfo(title);
      const sanitizedDescription = stripContactInfo(description);

      const { error: insertError } = await supabase.from("jobs").insert({
        company_id: company.id,
        title: sanitizedTitle,
        description: sanitizedDescription,
        trade: selectedTrade,
        suburb,
        location,
        site_address: siteAddress,
        rate_min: parseFloat(rateMin) || null,
        rate_max: parseFloat(rateMax) || parseFloat(rateMin) || null,
        rate_type: rateType,
        start_date: startDate || null,
        duration: duration || null,
        workers_needed: parseInt(workersNeeded) || 1,
        certifications_required: certs,
        status: "active",
        ai_generated: true,
      });

      if (insertError) {
        toast.error(`Failed to post job: ${insertError.message}`);
        setLoading(false);
        return;
      }

      // Record first_job_posted milestone (fire-and-forget)
      fetch('/api/success-tracking/record', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ milestone_type: 'first_job_posted' }),
      }).catch(() => {});

      setStep(3); // Success (was step 5, now step 3)
    } catch (error) {
      logger.error("Job post error:", error);
      toast.error("Something went wrong. Please try again.");
      setLoading(false);
    }
  }

  return (

        {/* STEP 1: Select Trade or Voice */}
        {step === 1 && (

            {/* Voice Input — Primary CTA */}

Post a Job

                Tap the mic and describe what you need

              <button
                type="button"
                onClick={voiceRecording ? stopVoiceRecording : startVoiceRecording}
                disabled={voiceStarting}
                className={`mt-6 flex h-24 w-24 items-center justify-center rounded-full transition-all active:scale-95 ${
                  voiceRecording
                    ? "animate-pulse bg-red-500 text-white shadow-lg shadow-red-500/40"
                    : "bg-primary text-primary-foreground shadow-lg hover:shadow-xl"
                } ${voiceStarting ? "opacity-50 cursor-not-allowed" : ""}`}
                aria-label={voiceRecording ? "Stop recording" : "Start recording"}
              >
                {voiceStarting ? (
                  <Loader2 className="h-10 w-10 animate-spin" />
                ) : voiceRecording ? (
                  <Square className="h-10 w-10" />
                ) : (
                  <Mic className="h-10 w-10" />
                )}
              </button>

                {voiceStarting
                  ? "Starting..."
                  : voiceRecording
                    ? "Listening... tap to stop"
                    : "\"I need two steelfixers in Parramatta, 45 an hour\""}

              {voiceError && (

{voiceError}

              )}

            {/* Divider */}

              or pick a trade

            {/* Trade Selection Grid */}

              {TRADE_CATEGORIES.map((trade) => {
                const Icon = trade.icon;
                return (
                  <button
                    key={trade.id}
                    onClick={() => handleTradeSelect(trade.id)}
                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
                  >
                    <Icon className="h-10 w-10" />
                    {trade.label}
                  </button>
                );
              })}

        )}

        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
        {step === 2 && (

            {aiLoading ? (

                  <Sparkles className="h-12 w-12 text-primary animate-pulse" />

                  {aiLoadingMessage || "AI writing your job post..."}

                  Based on what you said and market rates

            ) : (

                  <Sparkles className="h-5 w-5 text-primary" />

Job Details

                  AI pre-filled these. Edit anything you like.

                  {/* Job Details Section */}

                    Job Title
                    <Input
                      id="title"
                      value={title}
                      onChange={(e) => setTitle(e.target.value)}
                      className="mt-2 h-14 text-base"
                    />

                    Description
                    <textarea
                      id="description"
                      value={description}
                      onChange={(e) => setDescription(e.target.value)}
                      rows={5}
                      maxLength={5000}
                      className="mt-2 flex w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
                    />

                    Pay Rate (AUD)

                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
                        <Input
                          type="number"
                          value={rateMin}
                          onChange={(e) => {
                            setRateMin(e.target.value);
                            // Keep max in sync unless user explicitly set a range
                            if (!rateMax || rateMax === rateMin) {
                              setRateMax(e.target.value);
                            }
                          }}
                          placeholder="e.g. 60"
                          className="h-14 pl-10 text-base"
                        />

                      {rateMin && rateMax && rateMin !== rateMax && (

                          to

                            <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
                            <Input
                              type="number"
                              value={rateMax}
                              onChange={(e) => setRateMax(e.target.value)}
                              placeholder="Max"
                              className="h-14 pl-10 text-base"
                            />

                          <Button
                            type="button"
                            variant="ghost"
                            size="sm"
                            onClick={() => setRateMax(rateMin)}
                            className="text-xs text-muted-foreground shrink-0"
                          >
                            Use exact rate
                          </Button>

                      )}

                      <Button
                        type="button"
                        variant={rateType === "hourly" ? "default" : "outline"}
                        onClick={() => setRateType("hourly")}
                        className="h-12 text-base font-semibold"
                      >
                        Per Hour
                      </Button>
                      <Button
                        type="button"
                        variant={rateType === "daily" ? "default" : "outline"}
                        onClick={() => setRateType("daily")}
                        className="h-12 text-base font-semibold"
                      >
                        Per Day
                      </Button>

                  {/* Location Section */}

                      <MapPin className="h-5 w-5" />
                      Location

                        Site Address
                        <Input
                          id="siteAddress"
                          value={siteAddress}
                          onChange={(e) => setSiteAddress(e.target.value)}
                          placeholder="e.g. 123 George St, Sydney"
                          className="mt-2 h-14 text-base"
                        />

                        Suburb
                        <Input
                          id="suburb"
                          value={suburb}
                          onChange={(e) => setSuburb(e.target.value)}
                          placeholder="e.g. Parramatta"
                          className="mt-2 h-14 text-base"
                        />

                  {/* Date & Workers Section */}

                      <Calendar className="h-5 w-5" />
                      Schedule

                        Start Date
                        <Input
                          id="startDate"
                          type="date"
                          value={startDate}
                          onChange={(e) => setStartDate(e.target.value)}
                          className="mt-2 h-14 text-base"
                        />

                        Duration
                        <Input
                          id="duration"
                          value={duration}
                          onChange={(e) => setDuration(e.target.value)}
                          placeholder="e.g. 2 weeks, ongoing"
                          className="mt-2 h-14 text-base"
                        />

                          <Users className="h-5 w-5" />
                          Workers Needed

                          <Button
                            type="button"
                            variant="outline"
                            onClick={() =>
                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
                            }
                            className="h-14 w-14 text-2xl font-bold"
                          >
                            -
                          </Button>

                            {workersNeeded}

                          <Button
                            type="button"
                            variant="outline"
                            onClick={() =>
                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
                            }
                            className="h-14 w-14 text-2xl font-bold"
                          >
                            +
                          </Button>

                  {/* Required Certifications */}
                  {certs.length > 0 && (

                      Required Certifications

                        {certs.map((cert) => (
                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
                            {cert}
                            <button
                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
                              className="ml-2 text-muted-foreground hover:text-foreground"
                            >
                              ×
                            </button>
                          </Badge>
                        ))}

                  )}

            )}

        )}

        {/* STEP 3: Success */}
        {step === 3 && (
          <Card className="mt-8">
            <CardContent className="flex flex-col items-center py-10 text-center">

                <Check className="h-10 w-10 text-green-600" />

Job Posted!

                We're matching you with the best workers now.
                You'll get notified when we find matches.

                <Button
                  onClick={() => router.push("/contractor/matches")}
                  className="w-full h-12 text-base font-semibold"
                >
                  View Matches
                </Button>
                <Button
                  onClick={() => router.push("/dashboard")}
                  variant="outline"
                  className="w-full h-12 text-base"
                >
                  Back to Dashboard
                </Button>

            </CardContent>
          </Card>
        )}

      {/* Sticky Bottom CTA (Step 2 only) */}
      {step === 2 && !aiLoading && (

            <Button
              onClick={handlePost}
              className="w-full h-12 text-base font-semibold"
              disabled={loading || !title || !suburb || !description.trim()}
            >
              {loading ? (
                <>
                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
                  Posting...
                </>
              ) : (
                <>
                  Post Job Now
                  <Check className="ml-2 h-6 w-6" />
                </>
              )}
            </Button>

              Free to post. You only pay $50 when you hire someone.

      )}

  );
}


Now let me check the key dependencies - the CSRF and sanitize modules, and look at the import for `withCSRFToken` and `addCSRFTokenToFormData` to understand the CSRF flow, plus check if there might be an issue with how FormData is sent.


     1→/**
     2→ * Client-side CSRF protection utilities
     3→ */
     4→
     5→import { logger } from './logger';
     6→
     7→const CSRF_HEADER_NAME = 'x-csrf-token';
     8→const CSRF_COOKIE_NAME = 'csrf-token';
     9→
    10→/**
    11→ * Get CSRF token from browser cookies
    12→ *
    13→ * @returns CSRF token value or null if not found
    14→ *
    15→ * @example
    16→ * const token = getCSRFCookie();
    17→ * if (token) {
    18→ *   // Use token in API requests
    19→ * }
    20→ */
    21→export function getCSRFCookie(): string | null {
    22→  if (typeof document === 'undefined') return null;
    23→
    24→  const cookies = document.cookie.split(';');
    25→  for (const cookie of cookies) {
    26→    const [name, value] = cookie.trim().split('=');
    27→    if (name === CSRF_COOKIE_NAME) {
    28→      return value;
    29→    }
    30→  }
    31→  return null;
    32→}
    33→
    34→/**
    35→ * Fetch a new CSRF token from the server
    36→ * This should be called after authentication to get a fresh token
    37→ *
    38→ * @returns Promise resolving to CSRF token or null if fetch failed
    39→ *
    40→ * @example
    41→ * const token = await fetchCSRFToken();
    42→ * if (token) {
    43→ *   // Store or use the new token
    44→ * }
    45→ */
    46→export async function fetchCSRFToken(): Promise<string | null> {
    47→  try {
    48→    const response = await fetch('/api/auth/csrf', {
    49→      method: 'GET',
    50→      credentials: 'include', // Include cookies
    51→    });
    52→
    53→    if (!response.ok) {
    54→      logger.error('Failed to fetch CSRF token:', response.status);
    55→      return null;
    56→    }
    57→
    58→    const data = await response.json();
    59→    return data.token || null;
    60→  } catch (error) {
    61→    logger.error('Error fetching CSRF token:', error);
    62→    return null;
    63→  }
    64→}
    65→
    66→/**
    67→ * Add CSRF token to fetch options
    68→ * Enhances existing request options with CSRF protection headers
    69→ *
    70→ * @param options - Existing fetch options to enhance
    71→ * @returns Enhanced fetch options with CSRF token header
    72→ *
    73→ * @example
    74→ * const response = await fetch('/api/data', withCSRFToken({
    75→ *   method: 'POST',
    76→ *   body: JSON.stringify(data)
    77→ * }));
    78→ */
    79→export function withCSRFToken(options: RequestInit = {}): RequestInit {
    80→  const token = getCSRFCookie();
    81→
    82→  if (!token) {
    83→    logger.warn('No CSRF token found in cookies');
    84→    return options;
    85→  }
    86→
    87→  return {
    88→    ...options,
    89→    headers: {
    90→      ...options.headers,
    91→      [CSRF_HEADER_NAME]: token,
    92→    },
    93→  };
    94→}
    95→
    96→/**
    97→ * Enhanced fetch with CSRF protection
    98→ * Automatically adds CSRF token to requests. If no token exists in cookies,
    99→ * fetches a fresh one before proceeding.
   100→ *
   101→ * @param input - URL or Request object
   102→ * @param options - Fetch options
   103→ * @returns Promise resolving to Response
   104→ *
   105→ * @example
   106→ * const response = await csrfFetch('/api/data', {
   107→ *   method: 'POST',
   108→ *   body: JSON.stringify(data)
   109→ * });
   110→ */
   111→export async function csrfFetch(input: RequestInfo | URL, options?: RequestInit): Promise<Response> {
   112→  // If no CSRF cookie exists, fetch one first
   113→  if (!getCSRFCookie()) {
   114→    await fetchCSRFToken();
   115→  }
   116→  const enhancedOptions = withCSRFToken(options);
   117→  return fetch(input, enhancedOptions);
   118→}
   119→
   120→/**
   121→ * Add CSRF token to FormData for multipart/form-data requests
   122→ * Appends the CSRF token as a form field
   123→ *
   124→ * @param formData - FormData object to enhance
   125→ * @returns The same FormData object with CSRF token added
   126→ *
   127→ * @example
   128→ * const formData = new FormData();
   129→ * formData.append('file', file);
   130→ * addCSRFTokenToFormData(formData);
   131→ * // formData now includes csrf_token field
   132→ */
   133→export function addCSRFTokenToFormData(formData: FormData): FormData {
   134→  const token = getCSRFCookie();
   135→
   136→  if (token) {
   137→    formData.append('csrf_token', token);
   138→  } else {
   139→    logger.warn('No CSRF token found in cookies');
   140→  }
   141→
   142→  return formData;
   143→}
   144→
   145→/**
   146→ * Initialize CSRF token after authentication
   147→ * Call this after successful login/signup to get a fresh CSRF token
   148→ *
   149→ * @returns Promise resolving to true if token was successfully fetched
   150→ *
   151→ * @example
   152→ * const success = await initializeCSRF();
   153→ * if (success) {
   154→ *   // CSRF token is now available for subsequent requests
   155→ * }
   156→ */
   157→export async function initializeCSRF(): Promise<boolean> {
   158→  const token = await fetchCSRFToken();
   159→  return token !== null;
   160→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Client-side CSRF protection utilities
 */

import { logger } from './logger';

const CSRF_HEADER_NAME = 'x-csrf-token';
const CSRF_COOKIE_NAME = 'csrf-token';

/**
 * Get CSRF token from browser cookies
 *
 * @returns CSRF token value or null if not found
 *
 * @example
 * const token = getCSRFCookie();
 * if (token) {
 *   // Use token in API requests
 * }
 */
export function getCSRFCookie(): string | null {
  if (typeof document === 'undefined') return null;

  const cookies = document.cookie.split(';');
  for (const cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === CSRF_COOKIE_NAME) {
      return value;
    }
  }
  return null;
}

/**
 * Fetch a new CSRF token from the server
 * This should be called after authentication to get a fresh token
 *
 * @returns Promise resolving to CSRF token or null if fetch failed
 *
 * @example
 * const token = await fetchCSRFToken();
 * if (token) {
 *   // Store or use the new token
 * }
 */
export async function fetchCSRFToken(): Promise<string | null> {
  try {
    const response = await fetch('/api/auth/csrf', {
      method: 'GET',
      credentials: 'include', // Include cookies
    });

    if (!response.ok) {
      logger.error('Failed to fetch CSRF token:', response.status);
      return null;
    }

    const data = await response.json();
    return data.token || null;
  } catch (error) {
    logger.error('Error fetching CSRF token:', error);
    return null;
  }
}

/**
 * Add CSRF token to fetch options
 * Enhances existing request options with CSRF protection headers
 *
 * @param options - Existing fetch options to enhance
 * @returns Enhanced fetch options with CSRF token header
 *
 * @example
 * const response = await fetch('/api/data', withCSRFToken({
 *   method: 'POST',
 *   body: JSON.stringify(data)
 * }));
 */
export function withCSRFToken(options: RequestInit = {}): RequestInit {
  const token = getCSRFCookie();

  if (!token) {
    logger.warn('No CSRF token found in cookies');
    return options;
  }

  return {
    ...options,
    headers: {
      ...options.headers,
      [CSRF_HEADER_NAME]: token,
    },
  };
}

/**
 * Enhanced fetch with CSRF protection
 * Automatically adds CSRF token to requests. If no token exists in cookies,
 * fetches a fresh one before proceeding.
 *
 * @param input - URL or Request object
 * @param options - Fetch options
 * @returns Promise resolving to Response
 *
 * @example
 * const response = await csrfFetch('/api/data', {
 *   method: 'POST',
 *   body: JSON.stringify(data)
 * });
 */
export async function csrfFetch(input: RequestInfo | URL, options?: RequestInit): Promise<Response> {
  // If no CSRF cookie exists, fetch one first
  if (!getCSRFCookie()) {
    await fetchCSRFToken();
  }
  const enhancedOptions = withCSRFToken(options);
  return fetch(input, enhancedOptions);
}

/**
 * Add CSRF token to FormData for multipart/form-data requests
 * Appends the CSRF token as a form field
 *
 * @param formData - FormData object to enhance
 * @returns The same FormData object with CSRF token added
 *
 * @example
 * const formData = new FormData();
 * formData.append('file', file);
 * addCSRFTokenToFormData(formData);
 * // formData now includes csrf_token field
 */
export function addCSRFTokenToFormData(formData: FormData): FormData {
  const token = getCSRFCookie();

  if (token) {
    formData.append('csrf_token', token);
  } else {
    logger.warn('No CSRF token found in cookies');
  }

  return formData;
}

/**
 * Initialize CSRF token after authentication
 * Call this after successful login/signup to get a fresh CSRF token
 *
 * @returns Promise resolving to true if token was successfully fetched
 *
 * @example
 * const success = await initializeCSRF();
 * if (success) {
 *   // CSRF token is now available for subsequent requests
 * }
 */
export async function initializeCSRF(): Promise<boolean> {
  const token = await fetchCSRFToken();
  return token !== null;
}


     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { randomBytes, timingSafeEqual as cryptoTimingSafeEqual } from 'crypto';
     3→import { logger } from './logger';
     4→
     5→// CSRF token configuration
     6→const CSRF_TOKEN_LENGTH = 32;
     7→const CSRF_COOKIE_NAME = 'csrf-token';
     8→const CSRF_HEADER_NAME = 'x-csrf-token';
     9→const CSRF_FORM_FIELD_NAME = 'csrf_token';
    10→
    11→/**
    12→ * Generate a cryptographically secure CSRF token
    13→ */
    14→export function generateCSRFToken(): string {
    15→  return randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
    16→}
    17→
    18→/**
    19→ * Create a CSRF token and set it in a cookie
    20→ */
    21→export function createCSRFToken(): { token: string; cookie: string } {
    22→  const token = generateCSRFToken();
    23→  let cookie = `${CSRF_COOKIE_NAME}=${token}; Path=/; SameSite=Strict; Max-Age=86400`; // 24 hours
    24→
    25→  // Add secure flag in production
    26→  if (process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://')) {
    27→    cookie += '; Secure';
    28→  }
    29→
    30→  return { token, cookie };
    31→}
    32→
    33→/**
    34→ * Get CSRF token from cookie
    35→ */
    36→export function getCSRFCookieToken(request: Request | NextRequest): string | null {
    37→  // Check if it's a NextRequest with cookies property
    38→  if ('cookies' in request && typeof request.cookies.get === 'function') {
    39→    const cookie = request.cookies.get(CSRF_COOKIE_NAME);
    40→    return cookie?.value || null;
    41→  }
    42→
    43→  // Fall back to parsing Cookie header for standard Request
    44→  const cookieHeader = request.headers.get('cookie');
    45→  if (!cookieHeader) return null;
    46→
    47→  const cookies = cookieHeader.split(';');
    48→  for (const cookie of cookies) {
    49→    const [name, value] = cookie.trim().split('=');
    50→    if (name === CSRF_COOKIE_NAME) {
    51→      return value;
    52→    }
    53→  }
    54→  return null;
    55→}
    56→
    57→/**
    58→ * Get CSRF token from request (header or form data)
    59→ */
    60→export function getCSRFRequestToken(request: Request | NextRequest): string | null {
    61→  // Check header first
    62→  const headerToken = request.headers.get(CSRF_HEADER_NAME);
    63→  if (headerToken) return headerToken;
    64→
    65→  // For form data, check in the request body (will need to parse appropriately)
    66→  // This is handled in the validation function for JSON requests
    67→  return null;
    68→}
    69→
    70→/**
    71→ * Validate CSRF token from request
    72→ * For JSON requests: checks x-csrf-token header
    73→ * For form requests: checks form field
    74→ */
    75→export async function validateCSRFToken(request: Request | NextRequest): Promise<boolean> {
    76→  const cookieToken = getCSRFCookieToken(request);
    77→
    78→  if (!cookieToken) {
    79→    logger.error('CSRF validation failed: No token in cookie');
    80→    return false;
    81→  }
    82→
    83→  let requestToken: string | null = null;
    84→
    85→  // For JSON requests
    86→  if (request.headers.get('content-type')?.includes('application/json')) {
    87→    requestToken = request.headers.get(CSRF_HEADER_NAME);
    88→
    89→    // If no header, check in JSON body
    90→    if (!requestToken) {
    91→      try {
    92→        const body = await request.clone().json();
    93→        requestToken = body[CSRF_FORM_FIELD_NAME] || null;
    94→      } catch {
    95→        // Ignore JSON parsing errors
    96→      }
    97→    }
    98→  }
    99→
   100→  // For form data requests
   101→  else if (request.headers.get('content-type')?.includes('multipart/form-data')) {
   102→    // Check header first (withCSRFToken adds it here)
   103→    requestToken = request.headers.get(CSRF_HEADER_NAME);
   104→
   105→    // Fall back to form field
   106→    if (!requestToken) {
   107→      try {
   108→        const formData = await request.clone().formData();
   109→        requestToken = formData.get(CSRF_FORM_FIELD_NAME) as string;
   110→      } catch {
   111→        // Ignore form parsing errors
   112→      }
   113→    }
   114→  }
   115→
   116→  if (!requestToken) {
   117→    logger.error('CSRF validation failed: No token in request');
   118→    return false;
   119→  }
   120→
   121→  // Constant-time comparison to prevent timing attacks
   122→  const tokensMatch = timingSafeEqual(cookieToken, requestToken);
   123→
   124→  if (!tokensMatch) {
   125→    logger.error('CSRF validation failed: Token mismatch');
   126→    return false;
   127→  }
   128→
   129→  return true;
   130→}
   131→
   132→/**
   133→ * Timing-safe string comparison using Node.js crypto.timingSafeEqual
   134→ * Prevents timing attacks by using constant-time comparison
   135→ */
   136→function timingSafeEqual(a: string, b: string): boolean {
   137→  // crypto.timingSafeEqual requires buffers of equal length
   138→  // Pad the shorter string to match lengths to avoid leaking length info
   139→  const maxLength = Math.max(a.length, b.length);
   140→  const bufferA = Buffer.alloc(maxLength);
   141→  const bufferB = Buffer.alloc(maxLength);
   142→
   143→  bufferA.write(a);
   144→  bufferB.write(b);
   145→
   146→  try {
   147→    // This performs constant-time comparison
   148→    return cryptoTimingSafeEqual(bufferA, bufferB) && a.length === b.length;
   149→  } catch {
   150→    // If timingSafeEqual throws (shouldn't happen with equal-length buffers), fail closed
   151→    return false;
   152→  }
   153→}
   154→
   155→/**
   156→ * Check if request is same-origin
   157→ */
   158→export function isSameOrigin(request: Request | NextRequest): boolean {
   159→  const origin = request.headers.get('origin');
   160→  const referer = request.headers.get('referer');
   161→
   162→  if (!origin && !referer) {
   163→    // No origin/referer headers - reject the request to prevent CSRF bypass
   164→    return false;
   165→  }
   166→
   167→  const checkUrl = origin || referer;
   168→  if (!checkUrl) return false;
   169→
   170→  try {
   171→    const parsedUrl = new URL(checkUrl);
   172→    const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000');
   173→
   174→    return parsedUrl.origin === appUrl.origin;
   175→  } catch {
   176→    return false;
   177→  }
   178→}
   179→
   180→/**
   181→ * CSRF protection middleware
   182→ * Use this for routes that need CSRF protection
   183→ */
   184→export function withCSRFProtection<T extends NextRequest>(
   185→  handler: (request: T) => Promise<NextResponse | Response>,
   186→  options?: {
   187→    requireSameOrigin?: boolean;
   188→    skipValidation?: boolean;
   189→  }
   190→): (request: T) => Promise<NextResponse | Response> {
   191→  return async (request: T) => {
   192→    // Skip CSRF for GET, HEAD, OPTIONS requests
   193→    if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
   194→      return handler(request);
   195→    }
   196→
   197→    // Check if request is same-origin (optional, but recommended)
   198→    if (options?.requireSameOrigin !== false && !isSameOrigin(request)) {
   199→      return NextResponse.json(
   200→        { error: 'Cross-origin request not allowed' },
   201→        { status: 403 }
   202→      );
   203→    }
   204→
   205→    // Validate CSRF token (unless explicitly skipped)
   206→    if (!options?.skipValidation) {
   207→      const isValid = await validateCSRFToken(request);
   208→      if (!isValid) {
   209→        return NextResponse.json(
   210→          { error: 'Invalid or missing CSRF token' },
   211→          { status: 403 }
   212→        );
   213→      }
   214→    }
   215→
   216→    return handler(request);
   217→  };
   218→}
   219→
   220→/**
   221→ * Create a CSRF token endpoint
   222→ * This should be called when a user session starts
   223→ */
   224→export async function createCSRFEndpoint(request: NextRequest): Promise<NextResponse> {
   225→  if (request.method !== 'GET') {
   226→    return NextResponse.json({ error: 'Method not allowed' }, { status: 405 });
   227→  }
   228→
   229→  const { token, cookie } = createCSRFToken();
   230→
   231→  const response = NextResponse.json({ token });
   232→  response.headers.append('Set-Cookie', cookie);
   233→
   234→  return response;
   235→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import { NextRequest, NextResponse } from 'next/server';
import { randomBytes, timingSafeEqual as cryptoTimingSafeEqual } from 'crypto';
import { logger } from './logger';

// CSRF token configuration
const CSRF_TOKEN_LENGTH = 32;
const CSRF_COOKIE_NAME = 'csrf-token';
const CSRF_HEADER_NAME = 'x-csrf-token';
const CSRF_FORM_FIELD_NAME = 'csrf_token';

/**
 * Generate a cryptographically secure CSRF token
 */
export function generateCSRFToken(): string {
  return randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
}

/**
 * Create a CSRF token and set it in a cookie
 */
export function createCSRFToken(): { token: string; cookie: string } {
  const token = generateCSRFToken();
  let cookie = `${CSRF_COOKIE_NAME}=${token}; Path=/; SameSite=Strict; Max-Age=86400`; // 24 hours

  // Add secure flag in production
  if (process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://')) {
    cookie += '; Secure';
  }

  return { token, cookie };
}

/**
 * Get CSRF token from cookie
 */
export function getCSRFCookieToken(request: Request | NextRequest): string | null {
  // Check if it's a NextRequest with cookies property
  if ('cookies' in request && typeof request.cookies.get === 'function') {
    const cookie = request.cookies.get(CSRF_COOKIE_NAME);
    return cookie?.value || null;
  }

  // Fall back to parsing Cookie header for standard Request
  const cookieHeader = request.headers.get('cookie');
  if (!cookieHeader) return null;

  const cookies = cookieHeader.split(';');
  for (const cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === CSRF_COOKIE_NAME) {
      return value;
    }
  }
  return null;
}

/**
 * Get CSRF token from request (header or form data)
 */
export function getCSRFRequestToken(request: Request | NextRequest): string | null {
  // Check header first
  const headerToken = request.headers.get(CSRF_HEADER_NAME);
  if (headerToken) return headerToken;

  // For form data, check in the request body (will need to parse appropriately)
  // This is handled in the validation function for JSON requests
  return null;
}

/**
 * Validate CSRF token from request
 * For JSON requests: checks x-csrf-token header
 * For form requests: checks form field
 */
export async function validateCSRFToken(request: Request | NextRequest): Promise<boolean> {
  const cookieToken = getCSRFCookieToken(request);

  if (!cookieToken) {
    logger.error('CSRF validation failed: No token in cookie');
    return false;
  }

  let requestToken: string | null = null;

  // For JSON requests
  if (request.headers.get('content-type')?.includes('application/json')) {
    requestToken = request.headers.get(CSRF_HEADER_NAME);

    // If no header, check in JSON body
    if (!requestToken) {
      try {
        const body = await request.clone().json();
        requestToken = body[CSRF_FORM_FIELD_NAME] || null;
      } catch {
        // Ignore JSON parsing errors
      }
    }
  }

  // For form data requests
  else if (request.headers.get('content-type')?.includes('multipart/form-data')) {
    // Check header first (withCSRFToken adds it here)
    requestToken = request.headers.get(CSRF_HEADER_NAME);

    // Fall back to form field
    if (!requestToken) {
      try {
        const formData = await request.clone().formData();
        requestToken = formData.get(CSRF_FORM_FIELD_NAME) as string;
      } catch {
        // Ignore form parsing errors
      }
    }
  }

  if (!requestToken) {
    logger.error('CSRF validation failed: No token in request');
    return false;
  }

  // Constant-time comparison to prevent timing attacks
  const tokensMatch = timingSafeEqual(cookieToken, requestToken);

  if (!tokensMatch) {
    logger.error('CSRF validation failed: Token mismatch');
    return false;
  }

  return true;
}

/**
 * Timing-safe string comparison using Node.js crypto.timingSafeEqual
 * Prevents timing attacks by using constant-time comparison
 */
function timingSafeEqual(a: string, b: string): boolean {
  // crypto.timingSafeEqual requires buffers of equal length
  // Pad the shorter string to match lengths to avoid leaking length info
  const maxLength = Math.max(a.length, b.length);
  const bufferA = Buffer.alloc(maxLength);
  const bufferB = Buffer.alloc(maxLength);

  bufferA.write(a);
  bufferB.write(b);

  try {
    // This performs constant-time comparison
    return cryptoTimingSafeEqual(bufferA, bufferB) && a.length === b.length;
  } catch {
    // If timingSafeEqual throws (shouldn't happen with equal-length buffers), fail closed
    return false;
  }
}

/**
 * Check if request is same-origin
 */
export function isSameOrigin(request: Request | NextRequest): boolean {
  const origin = request.headers.get('origin');
  const referer = request.headers.get('referer');

  if (!origin && !referer) {
    // No origin/referer headers - reject the request to prevent CSRF bypass
    return false;
  }

  const checkUrl = origin || referer;
  if (!checkUrl) return false;

  try {
    const parsedUrl = new URL(checkUrl);
    const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000');

    return parsedUrl.origin === appUrl.origin;
  } catch {
    return false;
  }
}

/**
 * CSRF protection middleware
 * Use this for routes that need CSRF protection
 */
export function withCSRFProtection<T extends NextRequest>(
  handler: (request: T) => Promise<NextResponse | Response>,
  options?: {
    requireSameOrigin?: boolean;
    skipValidation?: boolean;
  }
): (request: T) => Promise<NextResponse | Response> {
  return async (request: T) => {
    // Skip CSRF for GET, HEAD, OPTIONS requests
    if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
      return handler(request);
    }

    // Check if request is same-origin (optional, but recommended)
    if (options?.requireSameOrigin !== false && !isSameOrigin(request)) {
      return NextResponse.json(
        { error: 'Cross-origin request not allowed' },
        { status: 403 }
      );
    }

    // Validate CSRF token (unless explicitly skipped)
    if (!options?.skipValidation) {
      const isValid = await validateCSRFToken(request);
      if (!isValid) {
        return NextResponse.json(
          { error: 'Invalid or missing CSRF token' },
          { status: 403 }
        );
      }
    }

    return handler(request);
  };
}

/**
 * Create a CSRF token endpoint
 * This should be called when a user session starts
 */
export async function createCSRFEndpoint(request: NextRequest): Promise<NextResponse> {
  if (request.method !== 'GET') {
    return NextResponse.json({ error: 'Method not allowed' }, { status: 405 });
  }

  const { token, cookie } = createCSRFToken();

  const response = NextResponse.json({ token });
  response.headers.append('Set-Cookie', cookie);

  return response;
}


     1→/**
     2→ * Sanitizes user input to prevent prompt injection attacks
     3→ * Strips control characters, limits length, and escapes template injection
     4→ */
     5→
     6→/**
     7→ * Strip control characters (0x00-0x1F, 0x7F)
     8→ *
     9→ * @param str - Input string to clean
    10→ * @returns String with control characters removed
    11→ *
    12→ * @example
    13→ * stripControlChars('Hello\x00World\x1F')
    14→ * // Returns: 'HelloWorld'
    15→ */
    16→function stripControlChars(str: string): string {
    17→  return str.replace(/[\x00-\x1F\x7F]/g, '');
    18→}
    19→
    20→/**
    21→ * Escape template literals to prevent injection
    22→ * Prevents template literal injection by escaping backticks, $, and backslashes
    23→ *
    24→ * @param str - Input string to escape
    25→ * @returns Escaped string safe for template literals
    26→ *
    27→ * @example
    28→ * escapeTemplateLiterals('Hello ${user.name}')
    29→ * // Returns: 'Hello \${user.name}'
    30→ *
    31→ * @example
    32→ * escapeTemplateLiterals('Backtick: `code`')
    33→ * // Returns: 'Backtick: \`code\`'
    34→ */
    35→function escapeTemplateLiterals(str: string): string {
    36→  return str
    37→    .replace(/\\/g, '\\\\')  // Escape backslashes first
    38→    .replace(/\$/g, '\\$')   // Escape $ to prevent ${} injection
    39→    .replace(/`/g, '\\`');   // Escape backticks
    40→}
    41→
    42→/**
    43→ * Sanitize string input to prevent security issues and normalize data
    44→ * Strips control characters, escapes template literals, trims whitespace, and enforces length limits
    45→ *
    46→ * @param input - String input to sanitize
    47→ * @param options - Sanitization options
    48→ * @param options.maxLength - Maximum allowed length (truncates if longer)
    49→ * @param options.stripControls - Whether to strip control characters (default: true)
    50→ * @param options.escapeTemplates - Whether to escape template literals (default: true)
    51→ * @returns Sanitized string
    52→ *
    53→ * @example
    54→ * sanitizeInput('Hello\x00World')
    55→ * // Returns: 'HelloWorld'
    56→ *
    57→ * @example
    58→ * sanitizeInput('  Hello ${injection}  ', { maxLength: 10 })
    59→ * // Returns: 'Hello \${injection}'
    60→ *
    61→ * @example
    62→ * sanitizeInput('user input', { stripControls: false, escapeTemplates: false })
    63→ * // Returns: 'user input'
    64→ */
    65→export function sanitizeInput(
    66→  input: string,
    67→  options: {
    68→    maxLength?: number;
    69→    stripControls?: boolean;
    70→    escapeTemplates?: boolean;
    71→  } = {}
    72→): string {
    73→  if (typeof input !== 'string') {
    74→    return '';
    75→  }
    76→
    77→  let sanitized = input;
    78→
    79→  // Strip control characters by default
    80→  if (options.stripControls !== false) {
    81→    sanitized = stripControlChars(sanitized);
    82→  }
    83→
    84→  // Trim whitespace
    85→  sanitized = sanitized.trim();
    86→
    87→  // Limit length BEFORE escaping to avoid cutting in the middle of escape sequences
    88→  if (options.maxLength && sanitized.length > options.maxLength) {
    89→    sanitized = sanitized.substring(0, options.maxLength);
    90→  }
    91→
    92→  // Escape template literals by default (after truncation)
    93→  if (options.escapeTemplates !== false) {
    94→    sanitized = escapeTemplateLiterals(sanitized);
    95→  }
    96→
    97→  return sanitized;
    98→}
    99→
   100→/**
   101→ * Strip phone numbers and email addresses from text to prevent
   102→ * contractors and workers from bypassing the platform.
   103→ *
   104→ * Handles Australian phone formats: 04xx xxx xxx, +614xxxxxxxx, (02) xxxx xxxx, etc.
   105→ * Handles email addresses: user@domain.com
   106→ *
   107→ * @param input - String to strip contact info from
   108→ * @returns String with phone numbers and emails replaced with [removed]
   109→ */
   110→export function stripContactInfo(input: string): string {
   111→  if (typeof input !== 'string') return '';
   112→
   113→  let result = input;
   114→
   115→  // Australian mobile: 04xx xxx xxx, 04xxxxxxxx, +614xxxxxxxx
   116→  result = result.replace(/(\+?61\s?|0)4\d{2}[\s.-]?\d{3}[\s.-]?\d{3}/g, '[removed]');
   117→
   118→  // Australian landline: (0x) xxxx xxxx, 0x xxxx xxxx
   119→  result = result.replace(/\(?0[2-9]\)?[\s.-]?\d{4}[\s.-]?\d{4}/g, '[removed]');
   120→
   121→  // General international numbers: +xx xxxxxxxxxx (10+ digits with optional separators)
   122→  result = result.replace(/\+\d{1,3}[\s.-]?\d[\d\s.-]{8,}/g, '[removed]');
   123→
   124→  // Catch remaining digit sequences that look like phone numbers (8-12 digits with separators)
   125→  result = result.replace(/\b\d{4}[\s.-]\d{3}[\s.-]\d{3}\b/g, '[removed]');
   126→
   127→  // Email addresses
   128→  result = result.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[removed]');
   129→
   130→  // Clean up multiple consecutive [removed] markers
   131→  result = result.replace(/(\[removed\]\s*){2,}/g, '[removed] ');
   132→
   133→  return result;
   134→}
   135→
   136→/**
   137→ * Sanitize multiple string inputs at once with field-specific options
   138→ * Useful for form data where different fields have different validation rules
   139→ *
   140→ * @param inputs - Object with string values to sanitize
   141→ * @param fieldOptions - Per-field sanitization options (keyed by field name)
   142→ * @returns Object with sanitized values
   143→ *
   144→ * @example
   145→ * sanitizeInputs(
   146→ *   { name: 'John Doe', bio: 'Hello\x00World', email: '  john@example.com  ' },
   147→ *   { bio: { maxLength: 500 }, email: { maxLength: 100 } }
   148→ * )
   149→ * // Returns: { name: 'John Doe', bio: 'HelloWorld', email: 'john@example.com' }
   150→ */
   151→export function sanitizeInputs(
   152→  inputs: Record<string, string>,
   153→  fieldOptions: Record<string, { maxLength?: number }> = {}
   154→): Record<string, string> {
   155→  const sanitized: Record<string, string> = {};
   156→
   157→  for (const [key, value] of Object.entries(inputs)) {
   158→    const options = fieldOptions[key] || {};
   159→    sanitized[key] = sanitizeInput(value, options);
   160→  }
   161→
   162→  return sanitized;
   163→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Sanitizes user input to prevent prompt injection attacks
 * Strips control characters, limits length, and escapes template injection
 */

/**
 * Strip control characters (0x00-0x1F, 0x7F)
 *
 * @param str - Input string to clean
 * @returns String with control characters removed
 *
 * @example
 * stripControlChars('Hello\x00World\x1F')
 * // Returns: 'HelloWorld'
 */
function stripControlChars(str: string): string {
  return str.replace(/[\x00-\x1F\x7F]/g, '');
}

/**
 * Escape template literals to prevent injection
 * Prevents template literal injection by escaping backticks, $, and backslashes
 *
 * @param str - Input string to escape
 * @returns Escaped string safe for template literals
 *
 * @example
 * escapeTemplateLiterals('Hello ${user.name}')
 * // Returns: 'Hello \${user.name}'
 *
 * @example
 * escapeTemplateLiterals('Backtick: `code`')
 * // Returns: 'Backtick: \`code\`'
 */
function escapeTemplateLiterals(str: string): string {
  return str
    .replace(/\\/g, '\\\\')  // Escape backslashes first
    .replace(/\$/g, '\\$')   // Escape $ to prevent ${} injection
    .replace(/`/g, '\\`');   // Escape backticks
}

/**
 * Sanitize string input to prevent security issues and normalize data
 * Strips control characters, escapes template literals, trims whitespace, and enforces length limits
 *
 * @param input - String input to sanitize
 * @param options - Sanitization options
 * @param options.maxLength - Maximum allowed length (truncates if longer)
 * @param options.stripControls - Whether to strip control characters (default: true)
 * @param options.escapeTemplates - Whether to escape template literals (default: true)
 * @returns Sanitized string
 *
 * @example
 * sanitizeInput('Hello\x00World')
 * // Returns: 'HelloWorld'
 *
 * @example
 * sanitizeInput('  Hello ${injection}  ', { maxLength: 10 })
 * // Returns: 'Hello \${injection}'
 *
 * @example
 * sanitizeInput('user input', { stripControls: false, escapeTemplates: false })
 * // Returns: 'user input'
 */
export function sanitizeInput(
  input: string,
  options: {
    maxLength?: number;
    stripControls?: boolean;
    escapeTemplates?: boolean;
  } = {}
): string {
  if (typeof input !== 'string') {
    return '';
  }

  let sanitized = input;

  // Strip control characters by default
  if (options.stripControls !== false) {
    sanitized = stripControlChars(sanitized);
  }

  // Trim whitespace
  sanitized = sanitized.trim();

  // Limit length BEFORE escaping to avoid cutting in the middle of escape sequences
  if (options.maxLength && sanitized.length > options.maxLength) {
    sanitized = sanitized.substring(0, options.maxLength);
  }

  // Escape template literals by default (after truncation)
  if (options.escapeTemplates !== false) {
    sanitized = escapeTemplateLiterals(sanitized);
  }

  return sanitized;
}

/**
 * Strip phone numbers and email addresses from text to prevent
 * contractors and workers from bypassing the platform.
 *
 * Handles Australian phone formats: 04xx xxx xxx, +614xxxxxxxx, (02) xxxx xxxx, etc.
 * Handles email addresses: user@domain.com
 *
 * @param input - String to strip contact info from
 * @returns String with phone numbers and emails replaced with [removed]
 */
export function stripContactInfo(input: string): string {
  if (typeof input !== 'string') return '';

  let result = input;

  // Australian mobile: 04xx xxx xxx, 04xxxxxxxx, +614xxxxxxxx
  result = result.replace(/(\+?61\s?|0)4\d{2}[\s.-]?\d{3}[\s.-]?\d{3}/g, '[removed]');

  // Australian landline: (0x) xxxx xxxx, 0x xxxx xxxx
  result = result.replace(/\(?0[2-9]\)?[\s.-]?\d{4}[\s.-]?\d{4}/g, '[removed]');

  // General international numbers: +xx xxxxxxxxxx (10+ digits with optional separators)
  result = result.replace(/\+\d{1,3}[\s.-]?\d[\d\s.-]{8,}/g, '[removed]');

  // Catch remaining digit sequences that look like phone numbers (8-12 digits with separators)
  result = result.replace(/\b\d{4}[\s.-]\d{3}[\s.-]\d{3}\b/g, '[removed]');

  // Email addresses
  result = result.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[removed]');

  // Clean up multiple consecutive [removed] markers
  result = result.replace(/(\[removed\]\s*){2,}/g, '[removed] ');

  return result;
}

/**
 * Sanitize multiple string inputs at once with field-specific options
 * Useful for form data where different fields have different validation rules
 *
 * @param inputs - Object with string values to sanitize
 * @param fieldOptions - Per-field sanitization options (keyed by field name)
 * @returns Object with sanitized values
 *
 * @example
 * sanitizeInputs(
 *   { name: 'John Doe', bio: 'Hello\x00World', email: '  john@example.com  ' },
 *   { bio: { maxLength: 500 }, email: { maxLength: 100 } }
 * )
 * // Returns: { name: 'John Doe', bio: 'HelloWorld', email: 'john@example.com' }
 */
export function sanitizeInputs(
  inputs: Record<string, string>,
  fieldOptions: Record<string, { maxLength?: number }> = {}
): Record<string, string> {
  const sanitized: Record<string, string> = {};

  for (const [key, value] of Object.entries(inputs)) {
    const options = fieldOptions[key] || {};
    sanitized[key] = sanitizeInput(value, options);
  }

  return sanitized;
}


Now I see a critical issue. Let me look at the `handleVoiceBlob` function more carefully. The function calls `withCSRFToken` which sets an `x-csrf-token` header. But when sending `FormData`, the browser needs to automatically set the `Content-Type` header with the multipart boundary. Let me check if `withCSRFToken` is interfering with the Content-Type header by setting explicit headers.

Looking at `handleVoiceBlob` (line 218):
```js
const res = await fetch("/api/ai/voice-to-job", withCSRFToken({
  method: "POST",
  body: formData,
}));
```

The `withCSRFToken` function spreads `options.headers` and adds the CSRF header. Since `options.headers` is undefined here, it creates `{ 'x-csrf-token': token }` - that's fine, it doesn't set Content-Type, so the browser should auto-set multipart boundaries. This part looks okay.

Let me check if there's a `Content-Type` header being set by `withCSRFToken` or any middleware issue. Also, let me check the CSRF endpoint exists.