     1→import { formatABN } from "@/lib/abn-utils";
     2→import { NextRequest, NextResponse } from 'next/server';
     3→import { isValidABN, cleanABN } from '@/lib/abn-utils';
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     5→import { sanitizeInput } from '@/lib/sanitize';
     6→import { z } from 'zod';
     7→import {
     8→  MAX_LENGTH_EMAIL,
     9→  TIMEOUT_ABN_LOOKUP,
    10→  POSTCODE_SYDNEY,
    11→  POSTCODE_MELBOURNE
    12→} from '@/lib/constants';
    13→import { getCachedABN, cacheABNData } from '@/lib/abn-cache';
    14→import { logger } from '@/lib/logger';
    15→
    16→/**
    17→ * ABN Lookup API Route — GET /api/abn-lookup?abn=XXXXXXXXXXX
    18→ *
    19→ * Validates the ABN locally (checksum), then calls the ABR JSON endpoint
    20→ * to fetch entity details.
    21→ *
    22→ * ABR JSON docs: https://abr.business.gov.au/json/
    23→ * Registration:  https://abr.business.gov.au/Tools/WebServices
    24→ *
    25→ * Required env var:
    26→ *   ABR_GUID — the authentication GUID issued after registration
    27→ *
    28→ * Optional env var:
    29→ *   ABR_DEV_MODE — set to 'true' to enable development mode with mock data
    30→ */
    31→
    32→// ── ABR configuration ──────────────────────────────────────────────
    33→const ABR_JSON_URL = 'https://abr.business.gov.au/json/AbnDetails.aspx';
    34→const ABR_GUID = process.env.ABR_GUID ?? '';
    35→const ABR_DEV_MODE = process.env.ABR_DEV_MODE === 'true';
    36→
    37→// ── Types ──────────────────────────────────────────────────────────
    38→
    39→/** Shape returned by the ABR JSON endpoint (JSONP-unwrapped). */
    40→interface ABRJsonResponse {
    41→  Abn: string;
    42→  AbnStatus: string;
    43→  AbnStatusEffectiveFrom: string;
    44→  Acn: string;
    45→  AddressDate: string | null;
    46→  AddressPostcode: string;
    47→  AddressState: string;
    48→  BusinessName: Array<string | { Value: string; EffectiveFrom: string }>;
    49→  EntityName: string;
    50→  EntityTypeCode: string;
    51→  EntityTypeName: string;
    52→  Gst: string | null;
    53→  Message: string;
    54→}
    55→
    56→/** Director verification result */
    57→interface DirectorVerification {
    58→  status: 'pending' | 'verified' | 'mismatch' | 'not_available' | 'auto_verified' | 'manual_review';
    59→  message: string;
    60→  requiresManualReview?: boolean;
    61→  verificationMethod?: string;
    62→  riskScore?: number;
    63→  riskFactors?: string[];
    64→}
    65→
    66→/** Company data from ABR */
    67→interface CompanyData {
    68→  name: string;
    69→  abn: string;
    70→  status: string;
    71→  statusDate: string;
    72→  state: string;
    73→  postcode: string;
    74→  type: string;
    75→  typeCode: string;
    76→  gstRegistered: boolean;
    77→  acn?: string;
    78→  directorVerification?: DirectorVerification;
    79→}
    80→
    81→/** Our normalised response. */
    82→interface ABNLookupResult {
    83→  success: boolean;
    84→  data?: CompanyData;
    85→  error?: string;
    86→  devMode?: boolean;
    87→}
    88→
    89→// ── Development Mode Mock Data ─────────────────────────────────────
    90→
    91→/**
    92→ * Mock company data for development when ABR_GUID is not configured.
    93→ * Uses obviously fake ABNs to prevent confusion with real data.
    94→ */
    95→const MOCK_COMPANIES: Record<string, CompanyData> = {
    96→  // Test ABN for development - passes checksum but is obviously fake
    97→  '51824753556': {
    98→    name: 'TEST COMPANY PTY LTD (Dev Mode)',
    99→    abn: '51 824 753 556',
   100→    status: 'Active',
   101→    statusDate: '2020-01-01',
   102→    state: 'NSW',
   103→    postcode: POSTCODE_SYDNEY,
   104→    type: 'Australian Private Company',
   105→    typeCode: 'PRV',
   106→    gstRegistered: true,
   107→    directorVerification: {
   108→      status: 'auto_verified',
   109→      message: 'Development mode - auto-verified',
   110→      verificationMethod: 'dev_mode',
   111→      riskScore: 0,
   112→    },
   113→  },
   114→  // Another test ABN
   115→  '34241177887': {
   116→    name: 'EXAMPLE BUILDERS PTY LTD (Dev Mode)',
   117→    abn: '34 241 177 887',
   118→    status: 'Active',
   119→    statusDate: '2019-06-15',
   120→    state: 'VIC',
   121→    postcode: POSTCODE_MELBOURNE,
   122→    type: 'Australian Private Company',
   123→    typeCode: 'PRV',
   124→    gstRegistered: true,
   125→    directorVerification: {
   126→      status: 'auto_verified',
   127→      message: 'Development mode - auto-verified',
   128→      verificationMethod: 'dev_mode',
   129→      riskScore: 0,
   130→    },
   131→  },
   132→};
   133→
   134→/**
   135→ * Generate mock data for any valid ABN in development mode.
   136→ */
   137→function generateMockData(abn: string): CompanyData {
   138→  const formatted = formatABN(abn);
   139→  return {
   140→    name: `MOCK COMPANY ${abn.slice(-4)} PTY LTD (Dev Mode)`,
   141→    abn: formatted,
   142→    status: 'Active',
   143→    statusDate: new Date().toISOString().split('T')[0],
   144→    state: 'NSW',
   145→    postcode: POSTCODE_SYDNEY,
   146→    type: 'Australian Private Company',
   147→    typeCode: 'PRV',
   148→    gstRegistered: Math.random() > 0.3,
   149→    directorVerification: {
   150→      status: 'auto_verified',
   151→      message: 'Development mode - auto-verified',
   152→      verificationMethod: 'dev_mode',
   153→      riskScore: 0,
   154→    },
   155→  };
   156→}
   157→
   158→// ── Helpers ────────────────────────────────────────────────────────
   159→
   160→function buildABRUrl(abn: string): string {
   161→  const params = new URLSearchParams({
   162→    abn,
   163→    guid: ABR_GUID,
   164→    callback: 'cbr',
   165→  });
   166→  return `${ABR_JSON_URL}?${params.toString()}`;
   167→}
   168→
   169→function stripJSONP(jsonp: string): ABRJsonResponse {
   170→  const match = jsonp.match(/^cbr\((.*)\)$/);
   171→  if (!match) throw new Error('Unexpected ABR response format');
   172→  return JSON.parse(match[1]);
   173→}
   174→
   175→function normaliseABRResponse(abr: ABRJsonResponse): CompanyData {
   176→  const name = abr.EntityName ||
   177→    (Array.isArray(abr.BusinessName) && abr.BusinessName[0]) ||
   178→    'Unknown';
   179→
   180→  return {
   181→    name: typeof name === 'string' ? name : (name as any).Value || 'Unknown',
   182→    abn: formatABN(abr.Abn),
   183→    status: abr.AbnStatus,
   184→    statusDate: abr.AbnStatusEffectiveFrom,
   185→    state: abr.AddressState,
   186→    postcode: abr.AddressPostcode,
   187→    type: abr.EntityTypeName,
   188→    typeCode: abr.EntityTypeCode,
   189→    gstRegistered: abr.Gst !== null,
   190→    acn: abr.Acn || undefined,
   191→    // Director verification will be done by verifyDirector() function
   192→  };
   193→}
   194→
   195→/**
   196→ * Multi-Layer Director Verification
   197→ *
   198→ * Implements a practical 3-tier verification system:
   199→ * 1. Automated checks (email domain, GST status, entity type)
   200→ * 2. Risk scoring to flag suspicious registrations
   201→ * 3. Manual review fallback for high-risk cases
   202→ *
   203→ * Future: ASIC Connect API integration for real director lookup
   204→ * @see https://connectonline.asic.gov.au/
   205→ */
   206→async function verifyDirector(
   207→  abn: string,
   208→  companyData: CompanyData,
   209→  userEmail?: string
   210→): Promise<DirectorVerification> {
   211→  const riskFactors: string[] = [];
   212→  let riskScore = 0;
   213→
   214→  // --- Layer 1: Automated Verification Checks ---
   215→
   216→  // Check 1: GST Registration (legitimate businesses usually have GST)
   217→  if (!companyData.gstRegistered) {
   218→    riskScore += 15;
   219→    riskFactors.push('Not GST registered');
   220→  }
   221→
   222→  // Check 2: Entity Type (only allow private companies and sole traders)
   223→  const allowedEntityTypes = ['PRV', 'IND']; // Private Company, Individual/Sole Trader
   224→  if (!allowedEntityTypes.includes(companyData.typeCode)) {
   225→    riskScore += 30;
   226→    riskFactors.push(`Unusual entity type: ${companyData.type}`);
   227→  }
   228→
   229→  // Check 3: ABN Status (must be active)
   230→  if (companyData.status.toLowerCase() !== 'active') {
   231→    riskScore += 50;
   232→    riskFactors.push(`ABN status: ${companyData.status}`);
   233→  }
   234→
   235→  // Check 4: Email Domain Validation (if provided)
   236→  if (userEmail) {
   237→    const emailDomain = userEmail.split('@')[1]?.toLowerCase();
   238→    const companyName = companyData.name.toLowerCase();
   239→
   240→    // Check if email is free provider (gmail, outlook, etc.)
   241→    const freeEmailProviders = [
   242→      'gmail.com', 'outlook.com', 'hotmail.com', 'yahoo.com',
   243→      'icloud.com', 'live.com', 'msn.com', 'protonmail.com'
   244→    ];
   245→
   246→    if (freeEmailProviders.includes(emailDomain)) {
   247→      riskScore += 20;
   248→      riskFactors.push('Using free email provider (no company domain)');
   249→    } else {
   250→      // Check if email domain roughly matches company name
   251→      const domainParts = emailDomain.replace(/\.com\.au|\.com|\.net\.au|\.net|\.org\.au|\.org/g, '').split('.');
   252→      const companyParts = companyName.replace(/pty ltd|pty|ltd|limited|proprietary/gi, '').trim().split(/\s+/);
   253→
   254→      // Simple fuzzy match: check if any significant word from company name is in domain
   255→      const hasMatch = companyParts.some(part =>
   256→        part.length > 3 && domainParts.some(dp =>
   257→          dp.includes(part.toLowerCase()) || part.toLowerCase().includes(dp)
   258→        )
   259→      );
   260→
   261→      if (!hasMatch) {
   262→        riskScore += 25;
   263→        riskFactors.push('Email domain does not match company name');
   264→      }
   265→    }
   266→  } else {
   267→    // No email provided for verification
   268→    riskScore += 10;
   269→    riskFactors.push('No email provided for verification');
   270→  }
   271→
   272→  // Check 5: Company Age (ABN status date - newly registered companies are riskier)
   273→  const statusDate = new Date(companyData.statusDate);
   274→  const monthsOld = (Date.now() - statusDate.getTime()) / (1000 * 60 * 60 * 24 * 30);
   275→
   276→  if (monthsOld < 3) {
   277→    riskScore += 20;
   278→    riskFactors.push('Company registered less than 3 months ago');
   279→  } else if (monthsOld < 12) {
   280→    riskScore += 10;
   281→    riskFactors.push('Company registered less than 1 year ago');
   282→  }
   283→
   284→  // Check 6: Missing ACN (private companies should have ACN)
   285→  if (companyData.typeCode === 'PRV' && !companyData.acn) {
   286→    riskScore += 15;
   287→    riskFactors.push('Private company missing ACN');
   288→  }
   289→
   290→  // --- Layer 2: Risk Score Evaluation ---
   291→
   292→  // Low Risk (0-25): Auto-verify
   293→  if (riskScore <= 25 && companyData.status.toLowerCase() === 'active') {
   294→    return {
   295→      status: 'auto_verified',
   296→      message: 'Company automatically verified based on ABR data and basic checks',
   297→      verificationMethod: 'automated',
   298→      riskScore,
   299→      riskFactors: riskFactors.length > 0 ? riskFactors : undefined,
   300→      requiresManualReview: false,
   301→    };
   302→  }
   303→
   304→  // Medium Risk (26-50): Flag for manual review but allow signup
   305→  if (riskScore <= 50) {
   306→    return {
   307→      status: 'manual_review',
   308→      message: 'Company registration flagged for manual review. You can proceed with signup.',
   309→      verificationMethod: 'manual_review_required',
   310→      riskScore,
   311→      riskFactors,
   312→      requiresManualReview: true,
   313→    };
   314→  }
   315→
   316→  // High Risk (51+): Require manual approval before activation
   317→  return {
   318→    status: 'pending',
   319→    message: 'Company registration requires manual verification before activation. Our team will review within 24 hours.',
   320→    verificationMethod: 'manual_approval_required',
   321→    riskScore,
   322→    riskFactors,
   323→    requiresManualReview: true,
   324→  };
   325→}
   326→
   327→// ── Route Handler ──────────────────────────────────────────────────
   328→
   329→// Rate limit: 10 requests per 15 minutes per IP (external API call)
   330→const ABN_LOOKUP_RATE_LIMIT = 10;
   331→const ABN_LOOKUP_WINDOW_MS = 15 * 60 * 1000;
   332→
   333→// Email validation schema
   334→const emailSchema = z.string().email().max(254);
   335→
   336→export async function GET(request: NextRequest) {
   337→  try {
   338→    // --- 0. Rate limiting ---------------------------------------------------
   339→    const clientIP = getClientIP(request);
   340→    const rateLimit = await checkRateLimit(`abn-lookup:${clientIP}`, ABN_LOOKUP_RATE_LIMIT, ABN_LOOKUP_WINDOW_MS);
   341→
   342→    if (!rateLimit.allowed) {
   343→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
   344→      return createRateLimitResponse(clientIP, retryAfter);
   345→    }
   346→
   347→    const searchParams = request.nextUrl.searchParams;
   348→    const rawABN = searchParams.get('abn');
   349→    const rawEmail = searchParams.get('email');
   350→
   351→    // --- 1. Validate input --------------------------------------------------
   352→    if (!rawABN) {
   353→      return NextResponse.json({
   354→        success: false,
   355→        error: 'Missing query parameter: abn'
   356→      }, { status: 400 });
   357→    }
   358→
   359→    const abn = cleanABN(rawABN);
   360→    if (!isValidABN(abn)) {
   361→      return NextResponse.json({
   362→        success: false,
   363→        error: 'Invalid ABN format. Please check the number and try again.'
   364→      }, { status: 400 });
   365→    }
   366→
   367→    // Validate email format if provided
   368→    let userEmail: string | null = null;
   369→    if (rawEmail) {
   370→      const sanitizedEmail = sanitizeInput(rawEmail, { maxLength: MAX_LENGTH_EMAIL });
   371→      const emailValidation = emailSchema.safeParse(sanitizedEmail);
   372→
   373→      if (!emailValidation.success) {
   374→        return NextResponse.json({
   375→          success: false,
   376→          error: 'Invalid email format. Please provide a valid email address.'
   377→        }, { status: 400 });
   378→      }
   379→
   380→      userEmail = emailValidation.data;
   381→    }
   382→
   383→    // --- 2. Check cache first ----------------------------------------------
   384→    const cachedData = await getCachedABN(abn);
   385→    if (cachedData) {
   386→      logger.info(`[ABN Lookup] Cache hit for ABN: ${abn}`);
   387→
   388→      // Convert cached data to the expected format
   389→      const result: CompanyData & { directorVerification?: DirectorVerification } = {
   390→        name: cachedData.companyName,
   391→        abn: cachedData.abn,
   392→        status: cachedData.status,
   393→        statusDate: cachedData.statusDate || '',
   394→        state: cachedData.state || '',
   395→        postcode: cachedData.postcode || '',
   396→        type: cachedData.entityType || '',
   397→        typeCode: cachedData.entityTypeCode || '',
   398→        gstRegistered: cachedData.gstRegistered || false,
   399→        acn: cachedData.acn,
   400→      };
   401→
   402→      // Director verification (same as before)
   403→      result.directorVerification = await verifyDirector(
   404→        abn,
   405→        result,
   406→        userEmail || undefined
   407→      );
   408→
   409→      return NextResponse.json({
   410→        success: true,
   411→        data: result,
   412→        cached: true,
   413→        cachedAt: cachedData.cachedAt,
   414→      });
   415→    }
   416→
   417→    logger.info(`[ABN Lookup] Cache miss for ABN: ${abn}, calling ABR API`);
   418→
   419→    // --- 3. Check if ABR_GUID is configured --------------------------------
   420→    const isGuidConfigured = ABR_GUID &&
   421→      ABR_GUID !== 'REGISTER-TOMORROW-AT-ABR-BUSINESS-GOV-AU' &&
   422→      ABR_GUID !== 'temporary-abr-guid-for-build';
   423→
   424→    if (!isGuidConfigured) {
   425→      // Development mode: return mock data
   426→      if (ABR_DEV_MODE) {
   427→        logger.info(`[ABN Lookup] Dev mode - returning mock data for ${abn}`);
   428→        const mockData = MOCK_COMPANIES[abn] || generateMockData(abn);
   429→        return NextResponse.json({
   430→          success: true,
   431→          data: mockData,
   432→          devMode: true,
   433→        });
   434→      }
   435→
   436→      // Production without GUID: return error with validation info
   437→      return NextResponse.json({
   438→        success: false,
   439→        error: 'ABR service not configured',
   440→        devMode: false,
   441→      }, { status: 503 });
   442→    }
   443→
   444→    // --- 4. Call ABR web service -------------------------------------------
   445→    const url = buildABRUrl(abn);
   446→    const abrResponse = await fetch(url, {
   447→      method: 'GET',
   448→      headers: { 'User-Agent': 'RateRight/1.0' },
   449→      signal: AbortSignal.timeout(TIMEOUT_ABN_LOOKUP),
   450→    });
   451→
   452→    if (!abrResponse.ok) {
   453→      const text = await abrResponse.text();
   454→      logger.error(`[ABN Lookup] ABR error ${abrResponse.status}:`, text);
   455→      return NextResponse.json({
   456→        success: false,
   457→        error: `ABR service error: ${abrResponse.status}`,
   458→      }, { status: 502 });
   459→    }
   460→
   461→    const jsonpText = await abrResponse.text();
   462→    const abrData = stripJSONP(jsonpText);
   463→
   464→    // --- 5. Handle ABR business logic ---------------------------------------
   465→    if (abrData.Message) {
   466→      // Cache the "not found" result with shorter expiry
   467→      await cacheABNData({
   468→        abn: abn,
   469→        companyName: 'Not Found',
   470→        status: 'Not Found',
   471→        abrResponse: abrData,
   472→      });
   473→
   474→      return NextResponse.json({
   475→        success: false,
   476→        error: abrData.Message
   477→      }, { status: 404 });
   478→    }
   479→
   480→    const result: CompanyData & { directorVerification?: DirectorVerification } = normaliseABRResponse(abrData);
   481→
   482→    // Cache the successful result
   483→    await cacheABNData({
   484→      abn: result.abn,
   485→      companyName: result.name,
   486→      status: result.status,
   487→      statusDate: result.statusDate,
   488→      state: result.state,
   489→      postcode: result.postcode,
   490→      entityType: result.type,
   491→      entityTypeCode: result.typeCode,
   492→      gstRegistered: result.gstRegistered,
   493→      acn: result.acn,
   494→      abrResponse: abrData,
   495→    });
   496→
   497→    // --- 5. Director verification (multi-layer automated + risk scoring) ----
   498→    result.directorVerification = await verifyDirector(
   499→      abn,
   500→      result,
   501→      userEmail || undefined
   502→    );
   503→
   504→    return NextResponse.json({
   505→      success: true,
   506→      data: result,
   507→    });
   508→
   509→  } catch (err: unknown) {
   510→    logger.error('[ABN Lookup] Uncaught error:', err);
   511→    return NextResponse.json({
   512→      success: false,
   513→      error: 'Internal server error',
   514→    }, { status: 500 });
   515→  }
   516→}
   517→

<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>


     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { logger } from '@/lib/logger';
     3→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     4→
     5→// Cache to store logo URLs (in production, use Redis or database)
     6→const logoCache = new Map<string, { logoUrl: string | null; source: string; timestamp: number }>();
     7→const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
     8→
     9→interface LogoResponse {
    10→  logoUrl: string | null;
    11→  source: 'clearbit' | 'google' | 'none';
    12→}
    13→
    14→// Rate limit: 30 requests per 15 minutes per IP
    15→const LOGO_RATE_LIMIT = 30;
    16→const LOGO_WINDOW_MS = 15 * 60 * 1000;
    17→
    18→async function checkLogoExists(url: string): Promise<boolean> {
    19→  try {
    20→    const response = await fetch(url, {
    21→      method: 'HEAD',
    22→      headers: {
    23→        'User-Agent': 'RateRight/1.0',
    24→      },
    25→    });
    26→    return response.ok;
    27→  } catch {
    28→    return false;
    29→  }
    30→}
    31→
    32→export async function GET(request: NextRequest) {
    33→  try {
    34→    // --- Rate limiting ---
    35→    const clientIP = getClientIP(request);
    36→    const rateLimit = await checkRateLimit(`company-logo:${clientIP}`, LOGO_RATE_LIMIT, LOGO_WINDOW_MS);
    37→
    38→    if (!rateLimit.allowed) {
    39→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    40→      return createRateLimitResponse(clientIP, retryAfter);
    41→    }
    42→
    43→    const searchParams = request.nextUrl.searchParams;
    44→    const domain = searchParams.get('domain');
    45→
    46→    if (!domain) {
    47→      return NextResponse.json<LogoResponse>(
    48→        { logoUrl: null, source: 'none' },
    49→        { status: 400, headers: getCORSHeaders(request) }
    50→      );
    51→    }
    52→
    53→    // Clean the domain (remove protocol if present)
    54→    const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
    55→
    56→    // Check cache first
    57→    const cacheKey = cleanDomain;
    58→    const cached = logoCache.get(cacheKey);
    59→    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    60→      return NextResponse.json<LogoResponse>({
    61→        logoUrl: cached.logoUrl,
    62→        source: cached.source as LogoResponse['source'],
    63→      }, {
    64→        headers: getCORSHeaders(request),
    65→      });
    66→    }
    67→
    68→    // Try Clearbit Logo API first
    69→    const clearbitUrl = `https://logo.clearbit.com/${cleanDomain}`;
    70→    const clearbitExists = await checkLogoExists(clearbitUrl);
    71→
    72→    if (clearbitExists) {
    73→      logoCache.set(cacheKey, {
    74→        logoUrl: clearbitUrl,
    75→        source: 'clearbit',
    76→        timestamp: Date.now(),
    77→      });
    78→
    79→      return NextResponse.json<LogoResponse>({
    80→        logoUrl: clearbitUrl,
    81→        source: 'clearbit',
    82→      }, {
    83→        headers: getCORSHeaders(request),
    84→      });
    85→    }
    86→
    87→    // Try Google Favicon as fallback
    88→    const googleUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=128`;
    89→    const googleExists = await checkLogoExists(googleUrl);
    90→
    91→    if (googleExists) {
    92→      logoCache.set(cacheKey, {
    93→        logoUrl: googleUrl,
    94→        source: 'google',
    95→        timestamp: Date.now(),
    96→      });
    97→
    98→      return NextResponse.json<LogoResponse>({
    99→        logoUrl: googleUrl,
   100→        source: 'google',
   101→      }, {
   102→        headers: getCORSHeaders(request),
   103→      });
   104→    }
   105→
   106→    // No logo found
   107→    logoCache.set(cacheKey, {
   108→      logoUrl: null,
   109→      source: 'none',
   110→      timestamp: Date.now(),
   111→    });
   112→
   113→    return NextResponse.json<LogoResponse>({
   114→      logoUrl: null,
   115→      source: 'none',
   116→    }, {
   117→      headers: getCORSHeaders(request),
   118→    });
   119→
   120→  } catch (error) {
   121→    logger.error('Logo fetch error:', error);
   122→
   123→    return NextResponse.json<LogoResponse>(
   124→      { logoUrl: null, source: 'none' },
   125→      { status: 500, headers: getCORSHeaders(request) }
   126→    );
   127→  }
   128→}
   129→
   130→// CORS configuration - restrict to known origins
   131→const ALLOWED_ORIGINS = [
   132→  process.env.NEXT_PUBLIC_APP_URL, // Production URL from env
   133→  ...(process.env.NODE_ENV === 'development'
   134→    ? ['http://localhost:3000', 'https://localhost:3000']
   135→    : []),
   136→].filter(Boolean) as string[];
   137→
   138→function getCORSHeaders(request: NextRequest): Record<string, string> {
   139→  const origin = request.headers.get('origin');
   140→
   141→  // Only allow specific origins, no wildcards (H5 fix)
   142→  const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin)
   143→    ? origin
   144→    : ALLOWED_ORIGINS[0] || 'null';
   145→
   146→  return {
   147→    'Access-Control-Allow-Origin': allowedOrigin,
   148→    'Access-Control-Allow-Methods': 'GET, OPTIONS',
   149→    'Access-Control-Allow-Headers': 'Content-Type',
   150→    'Vary': 'Origin',
   151→  };
   152→}
   153→
   154→// Add OPTIONS handler for CORS
   155→export async function OPTIONS(request: NextRequest) {
   156→  return new NextResponse(null, {
   157→    status: 200,
   158→    headers: getCORSHeaders(request),
   159→  });
   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>


     1→import { NextRequest, NextResponse } from "next/server";
     2→import { logger } from "@/lib/logger";
     3→
     4→/**
     5→ * Geocoding API endpoint
     6→ * Converts Australian suburb names to latitude/longitude coordinates
     7→ * Uses Nominatim (OpenStreetMap) - free, no API key required
     8→ */
     9→export async function POST(req: NextRequest) {
    10→  try {
    11→    const body = await req.json();
    12→    const { suburb } = body;
    13→
    14→    if (!suburb || typeof suburb !== "string") {
    15→      return NextResponse.json(
    16→        { error: "Suburb is required" },
    17→        { status: 400 }
    18→      );
    19→    }
    20→
    21→    // Add "Australia" to improve geocoding accuracy
    22→    const searchQuery = suburb.includes("Australia")
    23→      ? suburb
    24→      : `${suburb}, Australia`;
    25→
    26→    // Use Nominatim (OpenStreetMap) - free geocoding service
    27→    const response = await fetch(
    28→      `https://nominatim.openstreetmap.org/search?` +
    29→        new URLSearchParams({
    30→          q: searchQuery,
    31→          format: "json",
    32→          limit: "1",
    33→          countrycodes: "au", // Restrict to Australia
    34→        }),
    35→      {
    36→        headers: {
    37→          "User-Agent": "RateRight/2.0 (Construction Hiring Platform)",
    38→        },
    39→      }
    40→    );
    41→
    42→    if (!response.ok) {
    43→      logger.error("Nominatim API error:", response.statusText);
    44→      return NextResponse.json(
    45→        { error: "Geocoding service unavailable" },
    46→        { status: 503 }
    47→      );
    48→    }
    49→
    50→    const data = await response.json();
    51→
    52→    if (!data || data.length === 0) {
    53→      return NextResponse.json(
    54→        { error: "Location not found", suburb },
    55→        { status: 404 }
    56→      );
    57→    }
    58→
    59→    const location = data[0];
    60→    const lat = parseFloat(location.lat);
    61→    const lng = parseFloat(location.lon);
    62→
    63→    if (isNaN(lat) || isNaN(lng)) {
    64→      return NextResponse.json(
    65→        { error: "Invalid coordinates returned" },
    66→        { status: 500 }
    67→      );
    68→    }
    69→
    70→    return NextResponse.json({
    71→      suburb,
    72→      location: { lat, lng },
    73→      displayName: location.display_name,
    74→    });
    75→  } catch (error) {
    76→    logger.error("Geocoding error:", error);
    77→    return NextResponse.json(
    78→      { error: "Failed to geocode location" },
    79→      { status: 500 }
    80→    );
    81→  }
    82→}
    83→

<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>


     1→import { NextRequest, NextResponse } from "next/server";
     2→import { createClient } from "@/lib/supabase/server";
     3→import { logger } from "@/lib/logger";
     4→
     5→export async function GET(request: NextRequest) {
     6→  try {
     7→    const supabase = await createClient();
     8→
     9→    const { data: { user } } = await supabase.auth.getUser();
    10→
    11→    if (!user) {
    12→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    13→    }
    14→
    15→    const { searchParams } = new URL(request.url);
    16→    const userId = searchParams.get("userId");
    17→
    18→    if (!userId) {
    19→      return NextResponse.json({ error: "userId is required" }, { status: 400 });
    20→    }
    21→
    22→    // Fetch ratings with profile information
    23→    const { data: ratings, error } = await supabase
    24→      .from("ratings")
    25→      .select(`
    26→        *,
    27→        from_profile:profiles!ratings_from_id_fkey(
    28→          id,
    29→          name,
    30→          avatar_url
    31→        )
    32→      `)
    33→      .eq("to_id", userId)
    34→      .order("created_at", { ascending: false });
    35→
    36→    if (error) {
    37→      logger.error("[ratings-api] Error fetching ratings:", error);
    38→      return NextResponse.json({ error: error.message }, { status: 500 });
    39→    }
    40→
    41→    // Calculate average rating
    42→    const averageRating = ratings?.length
    43→      ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    44→      : 0;
    45→
    46→    return NextResponse.json({
    47→      ratings: ratings || [],
    48→      averageRating,
    49→      totalRatings: ratings?.length || 0
    50→    });
    51→  } catch (error) {
    52→    logger.error("[ratings-api] Unexpected error in ratings API:", error);
    53→    return NextResponse.json(
    54→      { error: "Internal server error" },
    55→      { status: 500 }
    56→    );
    57→  }
    58→}

<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>


     1→import { NextRequest, NextResponse } from "next/server";
     2→import { createClient } from "@/lib/supabase/server";
     3→
     4→// Simple logger fallback
     5→const logger = {
     6→  error: (...args: any[]) => console.error("[ratings-api]", ...args),
     7→  warn: (...args: any[]) => console.warn("[ratings-api]", ...args),
     8→  info: (...args: any[]) => console.info("[ratings-api]", ...args),
     9→};
    10→
    11→export async function GET(request: NextRequest) {
    12→  try {
    13→    const supabase = await createClient();
    14→
    15→    const { data: { user } } = await supabase.auth.getUser();
    16→
    17→    if (!user) {
    18→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    19→    }
    20→
    21→    const { searchParams } = new URL(request.url);
    22→    const userId = searchParams.get("userId");
    23→
    24→    if (!userId) {
    25→      return NextResponse.json({ error: "userId is required" }, { status: 400 });
    26→    }
    27→
    28→    // Fetch ratings with profile information
    29→    const { data: ratings, error } = await supabase
    30→      .from("ratings")
    31→      .select(`
    32→        *,
    33→        from_profile:profiles!ratings_from_id_fkey(
    34→          id,
    35→          name,
    36→          avatar_url
    37→        )
    38→      `)
    39→      .eq("to_id", userId)
    40→      .order("created_at", { ascending: false });
    41→
    42→    if (error) {
    43→      console.error("Error fetching ratings:", error);
    44→      return NextResponse.json({ error: error.message }, { status: 500 });
    45→    }
    46→
    47→    // Calculate average rating
    48→    const averageRating = ratings?.length
    49→      ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    50→      : 0;
    51→
    52→    return NextResponse.json({
    53→      ratings: ratings || [],
    54→      averageRating,
    55→      totalRatings: ratings?.length || 0
    56→    });
    57→  } catch (error) {
    58→    console.error("Unexpected error in ratings API:", error);
    59→    return NextResponse.json(
    60→      { error: "Internal server error" },
    61→      { status: 500 }
    62→    );
    63→  }
    64→}

<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>


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/build/page.tsx:22:import { csrfFetch, addCSRFTokenToFormData, initializeCSRF } from "@/lib/csrf-client";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/build/page.tsx:121:      await initializeCSRF();
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts:41: * const token = await fetchCSRFToken();
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts:46:export async function fetchCSRFToken(): Promise<string | null> {
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts:147: * const success = await initializeCSRF();
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts:152:export async function initializeCSRF(): Promise<boolean> {
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts:153:  const token = await fetchCSRFToken();


Now let me check for the next.config and any security headers configuration, and look at a few more API routes for completeness.


/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts:46:  // Do NOT use getSession() here — it reads unverified cookie data.


     1→import { getSupabaseAdmin, createClient } from '@/lib/supabase/server';
     2→import { NextRequest, NextResponse } from 'next/server';
     3→import { logger } from '@/lib/logger';
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     5→import { createPaymentIntent } from '@/lib/stripe/server';
     6→import { withCSRFProtection } from '@/lib/csrf';
     7→import {
     8→  PAYMENT_AMOUNT,
     9→  RATE_LIMIT_REQUESTS_PER_USER as PAYMENT_RATE_LIMIT,
    10→  RATE_LIMIT_WINDOW_MS as PAYMENT_WINDOW_MS,
    11→  HTTP_STATUS_INTERNAL_SERVER_ERROR
    12→} from '@/lib/constants';
    13→
    14→const handler = async (request: NextRequest) => {
    15→  try {
    16→    // --- Rate limiting (by IP as initial check) ---
    17→    const clientIP = getClientIP(request);
    18→    const ipRateLimit = await checkRateLimit(`payment-ip:${clientIP}`, PAYMENT_RATE_LIMIT, PAYMENT_WINDOW_MS);
    19→
    20→    if (!ipRateLimit.allowed) {
    21→      const retryAfter = Math.ceil((ipRateLimit.resetTime - Date.now()) / 1000);
    22→      return createRateLimitResponse(clientIP, retryAfter);
    23→    }
    24→
    25→    const supabase = await createClient();
    26→    const {
    27→      data: { user },
    28→    } = await supabase.auth.getUser();
    29→
    30→    if (!user) {
    31→      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    32→    }
    33→
    34→    // --- Stricter rate limiting by user ID ---
    35→    const userRateLimit = await checkRateLimit(`payment-user:${user.id}`, PAYMENT_RATE_LIMIT, PAYMENT_WINDOW_MS);
    36→
    37→    if (!userRateLimit.allowed) {
    38→      const retryAfter = Math.ceil((userRateLimit.resetTime - Date.now()) / 1000);
    39→      return createRateLimitResponse(user.id, retryAfter);
    40→    }
    41→
    42→    const body = await request.json();
    43→    const { match_id: matchId, company_id: companyId } = body;
    44→    const amount = PAYMENT_AMOUNT; // Hardcoded amount to prevent client override
    45→
    46→    if (!matchId || !companyId) {
    47→      return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
    48→    }
    49→
    50→    // Verify user owns the company
    51→    const { data: company } = await supabase
    52→      .from('companies')
    53→      .select('id, name')
    54→      .eq('id', companyId)
    55→      .eq('profile_id', user.id)
    56→      .single();
    57→
    58→    if (!company) {
    59→      return NextResponse.json({ error: 'Company not found or unauthorized' }, { status: 403 });
    60→    }
    61→
    62→    // Verify match exists and belongs to company's job (can be in any status except rejected)
    63→    const { data: match } = await supabase
    64→      .from('matches')
    65→      .select('status, jobs!inner(company_id, title)')
    66→      .eq('id', matchId)
    67→      .neq('status', 'rejected')
    68→      .eq('jobs.company_id', companyId)
    69→      .single();
    70→
    71→    if (!match) {
    72→      return NextResponse.json({ error: 'Match not found, already rejected, or unauthorized' }, { status: 403 });
    73→    }
    74→
    75→    // Check if payment already exists and is charged (prevent duplicate charges)
    76→    const { data: existingPayment } = await supabase
    77→      .from('payments')
    78→      .select('id, status')
    79→      .eq('match_id', matchId)
    80→      .eq('status', 'charged')
    81→      .maybeSingle();
    82→
    83→    if (existingPayment) {
    84→      return NextResponse.json({ error: 'Payment already completed for this hire' }, { status: 400 });
    85→    }
    86→
    87→    // Create Stripe PaymentIntent
    88→    const jobTitle = (match.jobs as { title?: string }).title || 'Job';
    89→    const { clientSecret, paymentIntentId } = await createPaymentIntent({
    90→      amount,
    91→      matchId: matchId,
    92→      companyId: companyId,
    93→      companyName: company.name,
    94→      description: `Hire for ${jobTitle}`,
    95→    });
    96→
    97→    // Atomic upsert: insert or update if conflict on match_id
    98→    // This prevents race conditions from concurrent payment creation requests
    99→    const supabaseService = getSupabaseAdmin();
   100→
   101→    const { data: payment, error } = await supabaseService
   102→      .from('payments')
   103→      .upsert(
   104→        {
   105→          match_id: matchId,
   106→          company_id: companyId,
   107→          amount,
   108→          status: 'pending',
   109→          stripe_payment_id: paymentIntentId,
   110→        },
   111→        {
   112→          onConflict: 'match_id',
   113→          ignoreDuplicates: false,
   114→        }
   115→      )
   116→      .select("id, match_id, company_id, amount, status, stripe_payment_id, created_at")
   117→      .single();
   118→
   119→    if (error) {
   120→      return NextResponse.json({ error: error.message }, { status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
   121→    }
   122→
   123→    return NextResponse.json({
   124→      payment,
   125→      clientSecret,
   126→      publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
   127→    });
   128→  } catch (error) {
   129→    logger.error('Payment creation error:', error);
   130→    if (error instanceof Error && error.message.includes('Stripe')) {
   131→      return NextResponse.json({ error: 'Payment service unavailable' }, { status: 503 });
   132→    }
   133→    return NextResponse.json({ error: 'Internal server error' }, { status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
   134→  }
   135→}
   136→
   137→// Apply CSRF protection to the handler
   138→export const POST = withCSRFProtection(handler);
   139→

<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>


     1→import type { NextConfig } from "next";
     2→
     3→const securityHeaders = [
     4→  { key: 'X-Frame-Options', value: 'DENY' },
     5→  { key: 'X-Content-Type-Options', value: 'nosniff' },
     6→  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
     7→  { key: 'X-DNS-Prefetch-Control', value: 'on' },
     8→  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
     9→  { key: 'Permissions-Policy', value: 'camera=(), microphone=(self), geolocation=(self)' },
    10→];
    11→
    12→const nextConfig: NextConfig = {
    13→  reactStrictMode: true,
    14→  async headers() {
    15→    return [
    16→      // Global security headers on all routes
    17→      {
    18→        source: '/(.*)',
    19→        headers: [
    20→          ...securityHeaders,
    21→          {
    22→            key: 'Content-Security-Policy',
    23→            value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'self' https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-src https://js.stripe.com https://hooks.stripe.com"
    24→          }
    25→        ],
    26→      },
    27→      // PWA manifest
    28→      {
    29→        source: '/manifest.json',
    30→        headers: [
    31→          { key: 'Content-Type', value: 'application/json' },
    32→          { key: 'Cache-Control', value: 'public, max-age=3600, immutable' },
    33→        ],
    34→      },
    35→      // Service worker — no cache
    36→      {
    37→        source: '/sw.js',
    38→        headers: [
    39→          { key: 'Content-Type', value: 'application/javascript; charset=utf-8' },
    40→          { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
    41→        ],
    42→      },
    43→    ];
    44→  },
    45→};
    46→
    47→export default nextConfig;
    48→

<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>


/home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts:4:import { withCSRFProtection } from "@/lib/csrf";
/home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts:154:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches/route.ts:5:import { withCSRFProtection } from "@/lib/csrf";
/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches/route.ts:157:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/auth/verify-email/route.ts:9:import { withCSRFProtection } from '@/lib/csrf'
/home/ccuser/the-50-dollar-app/src/app/api/auth/verify-email/route.ts:51:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts:6:import { withCSRFProtection } from '@/lib/csrf';
/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts:138:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-job/route.ts:7:import { withCSRFProtection } from "@/lib/csrf";
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-job/route.ts:141:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/ai/voice-complete/route.ts:24:import { withCSRFProtection } from "@/lib/csrf";
/home/ccuser/the-50-dollar-app/src/app/api/ai/voice-complete/route.ts:280:export const POST = withCSRFProtection(handler);
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-profile/route.ts:7:import { withCSRFProtection } from "@/lib/csrf";
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-profile/route.ts:174:export const POST = withCSRFProtection(handler);


Now let me check the remaining API routes that do NOT have CSRF protection to see if they should.


     1→import { createClient } from "@/lib/supabase/server";
     2→import { NextRequest, NextResponse } from "next/server";
     3→import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";
     4→import { sanitizeInput } from "@/lib/sanitize";
     5→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     6→import { logger } from "@/lib/logger";
     7→import { z } from "zod";
     8→import {
     9→  SEND_MESSAGE_RATE_LIMIT,
    10→  RATE_LIMIT_WINDOW_MS as SEND_MESSAGE_WINDOW_MS,
    11→  MAX_LENGTH_MESSAGE_CONTENT
    12→} from '@/lib/constants';
    13→
    14→// 60 messages per 15 min window per IP (prevents message spam)
    15→
    16→// Attachment validation schema
    17→const attachmentSchema = z.object({
    18→  url: z.string().url().max(2048),
    19→  filename: z.string().min(1).max(255),
    20→  contentType: z.string().regex(/^[a-z]+\/[a-z0-9\-\+\.]+$/i).max(127),
    21→  size: z.number().int().min(1).max(10 * 1024 * 1024), // 10MB max
    22→});
    23→
    24→const sendMessageSchema = z.object({
    25→  content: z.string().max(MAX_LENGTH_MESSAGE_CONTENT),
    26→  attachments: z.array(attachmentSchema).max(5).optional().default([]),
    27→});
    28→
    29→export async function POST(
    30→  request: NextRequest,

<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>


     1→/**
     2→ * Voice Transcription Endpoint
     3→ *
     4→ * DEPRECATED: This endpoint is maintained for backward compatibility.
     5→ * For new implementations, use /api/ai/voice-complete which provides
     6→ * consolidated transcription + parsing + generation for better performance.
     7→ *
     8→ * Performance: Use voice-complete for 30-50% faster processing
     9→ */
    10→
    11→import { NextRequest, NextResponse } from "next/server";
    12→import { logger } from "@/lib/logger";
    13→import OpenAI from "openai";
    14→import { createClient } from "@/lib/supabase/server";
    15→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
    16→import { withErrorHandler, APIError, ErrorCodes } from "@/lib/api-errors";
    17→import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";
    18→import {
    19→  AI_RATE_LIMIT,
    20→  AI_WINDOW_MS
    21→} from "@/lib/constants";
    22→
    23→if (!process.env.OPENAI_API_KEY) {
    24→  throw new Error("OPENAI_API_KEY environment variable is not set");
    25→}
    26→
    27→const openai = new OpenAI({
    28→  apiKey: process.env.OPENAI_API_KEY,
    29→});
    30→

<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>


     1→/**
     2→ * Voice Profile Parsing Endpoint
     3→ *
     4→ * DEPRECATED: This endpoint is maintained for backward compatibility.
     5→ * For new implementations, use /api/ai/voice-complete which provides
     6→ * consolidated transcription + parsing + generation for better performance.
     7→ *
     8→ * Performance: Use voice-complete for 30-50% faster processing
     9→ */
    10→
    11→import { NextRequest, NextResponse } from "next/server";
    12→import { logger } from "@/lib/logger";
    13→import OpenAI from "openai";
    14→import { createClient } from "@/lib/supabase/server";
    15→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
    16→import { withErrorHandler, APIError, ErrorCodes } from "@/lib/api-errors";
    17→import { sanitizeInput } from "@/lib/sanitize";
    18→import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";
    19→import { TRADES } from "@/lib/constants";
    20→import {
    21→  AI_RATE_LIMIT,
    22→  RATE_LIMIT_WINDOW_MS as AI_WINDOW_MS,
    23→  MAX_LENGTH_TRANSCRIPT
    24→} from "@/lib/constants";
    25→
    26→// Cast helper to use NextRequest methods while satisfying withErrorHandler signature
    27→type Handler = (request: Request) => Promise<Response>;
    28→
    29→if (!process.env.OPENAI_API_KEY) {
    30→  throw new Error("OPENAI_API_KEY environment variable is not set");

<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>


     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { logger } from '@/lib/logger';
     3→import { retrievePaymentIntent } from '@/lib/stripe/server';
     4→import { createClient } from '@/lib/supabase/server';
     5→import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '@/lib/constants';
     6→
     7→export async function GET(request: NextRequest) {
     8→  try {
     9→    const searchParams = request.nextUrl.searchParams;
    10→    const paymentIntentId = searchParams.get('payment_intent');
    11→
    12→    if (!paymentIntentId) {
    13→      return NextResponse.json({ error: 'Missing payment_intent parameter' }, { status: 400 });
    14→    }
    15→
    16→    // Authenticate user
    17→    const supabase = await createClient();
    18→    const {
    19→      data: { user },
    20→    } = await supabase.auth.getUser();
    21→
    22→    if (!user) {
    23→      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    24→    }
    25→
    26→    // Retrieve payment intent from Stripe
    27→    const paymentIntent = await retrievePaymentIntent(paymentIntentId);
    28→
    29→    // Check if user owns the payment by verifying the match belongs to their company
    30→    const { data: payment } = await supabase

<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>


Now I have a comprehensive picture of the authentication system. Here is the detailed review.

---

## Authentication System Review: `/home/ccuser/the-50-dollar-app`

### Files Reviewed

- `/home/ccuser/the-50-dollar-app/middleware.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts`
- `/home/ccuser/the-50-dollar-app/src/app/auth/callback/route.ts`
- `/home/ccuser/the-50-dollar-app/src/app/auth/login/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/auth/forgot-password/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/auth/verify-email/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/auth/reset-password/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/layout.tsx`
- `/home/ccuser/the-50-dollar-app/src/hooks/use-notifications.ts`
- `/home/ccuser/the-50-dollar-app/src/components/sign-out-button.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/api/auth/csrf/route.ts`
- `/home/ccuser/the-50-dollar-app/src/app/api/auth/verify-email/route.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/csrf.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts`
- `/home/ccuser/the-50-dollar-app/src/lib/constants.ts`
- `/home/ccuser/the-50-dollar-app/next.config.ts`
- All API route files under `/home/ccuser/the-50-dollar-app/src/app/api/`

---

### THINGS DONE WELL

Before listing issues, the codebase has several security strengths worth noting:

1. **Uses `getUser()` instead of `getSession()`** everywhere -- the middleware explicitly notes not to use `getSession()` because it reads unverified cookie data. This is correct.
2. **Open redirect prevention** in the auth callback (line 10 of callback/route.ts).
3. **CSRF token system** exists with timing-safe comparison, same-origin checks, and a `withCSRFProtection` wrapper.
4. **Rate limiting** is implemented on most API routes using Upstash Redis with in-memory fallback.
5. **Security headers** are configured in `next.config.ts` (HSTS, X-Frame-Options DENY, CSP, nosniff, etc.).
6. **Admin client** (`getSupabaseAdmin`) disables `autoRefreshToken` and `persistSession`.
7. **Non-singleton browser client** -- `createClient()` avoids stale credentials.

---

### ISSUES FOUND

#### CRITICAL

**1. Email verification API is a no-op placeholder**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/auth/verify-email/route.ts`, lines 33-38
- The endpoint accepts `userId` and `token`, but the comment on line 33 says "In production, implement proper token validation" and line 36 just returns `{ success: true }`. This means any request with any `userId` and any `token` value will return success. The endpoint does nothing.
- **Impact:** The email verification API endpoint is non-functional. However, actual email verification appears to rely on Supabase's built-in email verification flow (via the callback route and `email_confirmed_at`), so this may be dead code. If anything calls this endpoint expecting it to actually verify an email, it would accept anything.

**2. Reset password page has no server-side session verification**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/reset-password/page.tsx`
- The page is a client component that calls `supabase.auth.updateUser({ password })` (line 39). It does not verify that the user arrived via a valid password reset link. If a user has an active session (e.g., they are already logged in), they could navigate directly to `/auth/reset-password` and change their password without going through the reset email flow.
- The middleware in `/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts` (lines 82-88) redirects verified logged-in users from auth pages to the dashboard, but `/auth/reset-password` is not explicitly listed as an auth page. The check on line 58 is `pathname.startsWith("/auth/login") || pathname.startsWith("/auth/signup")`. The `/auth/reset-password` path does NOT match either of those, so it will NOT be redirected. Verified, logged-in users can reach the password reset form.
- **Impact:** Medium-high. This is technically acceptable behavior (a logged-in user changing their own password), but there is no confirmation of the current password before allowing the change. An attacker with brief physical access to an unlocked session could change the password.

**3. Middleware protected paths list is incomplete**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts`, line 54
- Protected paths: `["/dashboard", "/contractor", "/worker", "/messages", "/profile"]`
- Any new routes added outside these prefixes would NOT be protected by middleware. For example, if someone adds `/settings`, `/admin`, `/account`, or any other top-level route, it would be accessible without authentication unless it has its own auth check.
- The `(authenticated)` layout group does check for a user on line 14, but that only covers pages nested under that route group. API routes under `/api/` are not protected by middleware auth logic -- they rely on individual route-level auth checks.
- **Impact:** Future routes could accidentally be unprotected. This is a maintenance risk rather than a current vulnerability.

#### HIGH

**4. No rate limiting on login endpoint**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/login/page.tsx`
- The login form calls `supabase.auth.signInWithPassword()` directly from the client (line 25). There is no server-side rate limiting wrapper around the login attempt. Supabase has its own built-in rate limiting, but the application does not enforce its own limits.
- **Impact:** Brute force password attacks are only mitigated by Supabase's internal rate limits, which may be generous (typically ~30 per hour per email by default). The application should add its own rate limiting on login, especially since it has a rate-limit infrastructure already built.

**5. No rate limiting on signup endpoint**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`
- Similar to login, `supabase.auth.signUp()` is called directly from the client (line 37) with no application-level rate limiting.
- **Impact:** An attacker could create many accounts in rapid succession, limited only by Supabase's internal throttling.

**6. No rate limiting on forgot-password endpoint**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/forgot-password/page.tsx`
- `supabase.auth.resetPasswordForEmail()` is called from the client (line 24) with no rate limiting.
- **Impact:** An attacker could trigger mass password reset emails to any email address, potentially for email bombing or to enumerate valid accounts (if error messages differ for existing vs non-existing emails).

**7. No rate limiting on password change**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/reset-password/page.tsx`
- `supabase.auth.updateUser({ password })` on line 39 has no rate limiting.
- **Impact:** Moderate. Combined with issue #2, this could be used to rapidly try password changes.

**8. CSRF cookie is not HttpOnly**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/csrf.ts`, line 23
- The CSRF cookie is set as: `${CSRF_COOKIE_NAME}=${token}; Path=/; SameSite=Strict; Max-Age=86400`
- It deliberately omits `HttpOnly` because the client-side JavaScript in `/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts` needs to read it via `document.cookie` (line 24 of csrf-client.ts).
- This is actually the standard double-submit cookie pattern and is correct for this approach. However, the cookie IS readable by any XSS payload. If an XSS vulnerability exists anywhere in the app, the CSRF protection can be fully bypassed.
- **Impact:** Medium. This is an inherent limitation of the double-submit cookie pattern. A server-side session-bound CSRF token would be more robust.

**9. CSRF token not initialized on login or signup**
- **Files:** `/home/ccuser/the-50-dollar-app/src/app/auth/login/page.tsx`, `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`
- Neither the login nor signup pages call `initializeCSRF()` after successful authentication. A grep shows that only the worker profile build page (`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/build/page.tsx`, line 121) calls `initializeCSRF()`.
- **Impact:** If a user logs in and immediately navigates to a CSRF-protected endpoint, the CSRF token may not exist yet, causing the request to fail. Alternatively, if an old token exists from a previous session, it would still work (which is a session fixation risk for CSRF tokens).

#### MEDIUM

**10. Geocode API has no authentication or rate limiting**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/geocode/route.ts`
- The POST handler has no auth check and no rate limiting. Any unauthenticated user can call it.
- **Impact:** Could be abused to proxy geocoding requests through the server, potentially hitting Nominatim rate limits and getting the server's IP banned from Nominatim.

**11. ABN Lookup API has no authentication**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts`
- Has rate limiting (10/15min per IP, line 330) but no authentication check. Any unauthenticated visitor can look up ABN data.
- **Impact:** Exposes the ABR lookup functionality publicly. If the ABR GUID is rate-limited or has usage quotas, an attacker could exhaust them.

**12. Company Logo API has no authentication**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/company-logo/route.ts`
- Has rate limiting (30/15min per IP) but no auth check.
- **Impact:** Lower severity. Could be used as a proxy to check domain existence, but limited practical impact.

**13. Real-time notification subscription is not filtered by user**
- **File:** `/home/ccuser/the-50-dollar-app/src/hooks/use-notifications.ts`, lines 149-168
- The Supabase Realtime subscription listens for all INSERT/UPDATE/DELETE events on the `notifications` table without a filter on `profile_id`. Line 156 shows `table: "notifications"` with no filter clause.
- The code does check for duplicates on the client side (line 163), but every connected client will receive every notification event for all users.
- **Impact:** This is a data leak. Depending on RLS policies on the Supabase side, it may or may not expose other users' notification content. Even if RLS prevents data exposure in the payload, the event itself (that a notification was created/updated/deleted) leaks to all clients. The subscription should use a filter like `.eq('profile_id', user.id)`.

**14. `useUnreadCount` subscription also has no user filter**
- **File:** `/home/ccuser/the-50-dollar-app/src/hooks/use-notifications.ts`, lines 276-295
- Same issue as #13. The subscription triggers `fetchCount()` on any INSERT or UPDATE to the notifications table, regardless of which user the notification belongs to.
- **Impact:** Every notification created for any user will trigger unnecessary `fetchCount()` calls for all connected clients.

**15. `verifyEmail()` admin helper has no authorization check**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts`, lines 96-105
- The `verifyEmail(userId)` function accepts any `userId` and marks it as verified using the admin client. It has no authorization check on who is calling it.
- **Impact:** If any route handler calls this function with user-controlled input without proper validation, any user's email could be marked as verified. Currently, this function does not appear to be called from any route handler (only the placeholder verify-email route exists), so the risk is theoretical.

**16. Weak password requirements**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`, line 90 and `/home/ccuser/the-50-dollar-app/src/app/auth/reset-password/page.tsx`, line 27
- The minimum password length is 6 characters. There are no requirements for complexity (uppercase, lowercase, numbers, special characters).
- **Impact:** Weak passwords are more susceptible to brute force and credential stuffing attacks.

**17. Supabase admin client is a module-level singleton**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts`, line 42
- `_supabaseAdmin` is stored as a module-level variable. In a serverless environment (like Vercel), this means it persists for the lifetime of the function instance, which is fine. However, if the service key is rotated, the old key will persist until all function instances are recycled.
- **Impact:** Low. Key rotation would require a redeployment to take effect for all instances.

**18. Error messages may leak information**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/auth/login/page.tsx`, line 31
- The login page displays `error.message` directly from Supabase. Supabase typically returns messages like "Invalid login credentials" which is fine, but other error messages could potentially leak implementation details.
- **Impact:** Low. Supabase's default error messages are generally safe, but custom error handling would be more robust.

#### LOW

**19. Missing `forgot-password` and `reset-password` from middleware auth page classification**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts`, line 58
- `isAuthPage` only matches `/auth/login` and `/auth/signup`. It does not include `/auth/forgot-password` or `/auth/reset-password`.
- This means a verified, logged-in user can access `/auth/forgot-password` and `/auth/reset-password` without being redirected to the dashboard (the redirect on lines 83-88 only triggers for `isAuthPage || isVerifyPage`).
- **Impact:** Low for forgot-password (harmless). Medium for reset-password (see issue #2 above).

**20. Duplicate ratings API route**
- **Files:** `/home/ccuser/the-50-dollar-app/src/app/api/ratings/route.ts` and `/home/ccuser/the-50-dollar-app/src/app/api/ratings-rootfix/route.ts`
- Two nearly identical ratings endpoints exist. Both have auth checks, but maintaining duplicate code increases the risk of one falling out of sync.
- **Impact:** Maintenance risk. If a security fix is applied to one, the other could be missed.

**21. Sign-out does not invalidate server-side session**
- **File:** `/home/ccuser/the-50-dollar-app/src/components/sign-out-button.tsx`
- The sign-out calls `supabase.auth.signOut()` on the client side (line 18), which clears cookies locally. This relies on the Supabase JS client to also invalidate the session on the server. If the sign-out request fails (caught on line 21), the user's session remains valid server-side but the UI navigates to `/`.
- **Impact:** Low. The error is caught and loading state is reset, but the user may believe they are signed out when they are not.

**22. No Content-Security-Policy `'unsafe-eval'` restriction tested, but `'unsafe-inline'` is present**
- **File:** `/home/ccuser/the-50-dollar-app/next.config.ts`, line 23
- The CSP includes `script-src 'self' 'unsafe-inline'`. While `'unsafe-eval'` is correctly absent, `'unsafe-inline'` weakens the XSS protection that CSP provides. Combined with issue #8, an XSS vulnerability could bypass both CSP and CSRF protections.
- **Impact:** Reduced effectiveness of CSP as an XSS mitigation. This is common in Next.js apps due to inline scripts, but it would be better to use nonces.

**23. Cookie `setAll` silently swallows errors in server client**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts`, line 32
- The `catch {}` block silently ignores cookie-setting failures. This is documented as expected ("Cookies can only be set in Server Actions or Route Handlers"), but it means that in Server Components, token refresh will silently fail.
- **Impact:** Low. This is standard practice for the Supabase SSR library, but it means the middleware's `updateSession` function is the primary place where token refresh actually works.

---

### SUMMARY TABLE

| # | Severity | Issue |
|---|----------|-------|
| 1 | CRITICAL | Email verification API endpoint is a no-op placeholder (always returns success) |
| 2 | CRITICAL | Reset password page accessible without reset flow; no current-password confirmation |
| 3 | CRITICAL | Middleware protected paths is a hardcoded allowlist -- new routes may be unprotected |
| 4 | HIGH | No rate limiting on login (brute force risk) |
| 5 | HIGH | No rate limiting on signup (mass account creation) |
| 6 | HIGH | No rate limiting on forgot-password (email bombing, account enumeration) |
| 7 | HIGH | No rate limiting on password change |
| 8 | HIGH | CSRF cookie is not HttpOnly (required for double-submit pattern, but XSS can bypass) |
| 9 | HIGH | CSRF token not initialized after login/signup |
| 10 | MEDIUM | Geocode API has no auth or rate limiting |
| 11 | MEDIUM | ABN Lookup API has no auth check |
| 12 | MEDIUM | Company Logo API has no auth check |
| 13 | MEDIUM | Realtime notification subscription is unfiltered (potential data leak) |
| 14 | MEDIUM | Unread count subscription is unfiltered (unnecessary server load) |
| 15 | MEDIUM | `verifyEmail()` admin helper has no authorization gate |
| 16 | MEDIUM | Weak password requirements (min 6 chars, no complexity) |
| 17 | MEDIUM | Admin client singleton persists stale keys until redeployment |
| 18 | MEDIUM | Raw Supabase error messages shown to users |
| 19 | LOW | forgot-password and reset-password not classified as auth pages in middleware |
| 20 | LOW | Duplicate ratings API routes (maintenance risk) |
| 21 | LOW | Sign-out error handling does not guarantee server-side session invalidation |
| 22 | LOW | CSP uses `'unsafe-inline'` for scripts, weakening XSS protection |
| 23 | LOW | Silent cookie-setting failures in Server Components |

The highest priority fixes should be items 4, 5, and 6 (adding rate limiting to the auth forms), item 2 (protecting the reset-password route), item 9 (initializing CSRF after login), and item 13 (adding a user filter to the realtime subscription).