Find all files related to the messages/start API route and messaging system. Search for:
1. Any file at app/api/messages/start or similar path
2. Any route.ts files related to messages
3. Any conversation or messaging related API routes
4. The matches table schema or types
5. Payment verification logic

Also find all files related to photo upload - search for upload, photo, avatar, image, multer, formdata, 2MB, file size limits.

Also find files related to job posting/creation - search for job post, job create, job description, sanitize.

Also find worker profile pages and worker signup flow.

Be very thorough - check src/app, app, pages directories. Return the full paths and key code snippets.


I'll conduct a thorough search of the codebase to find all the files you mentioned. Let me start with multiple parallel searches.


/home/ccuser/the-50-dollar-app/node_modules/@stripe/stripe-js/dist/stripe-js/elements/payment-method-messaging.d.ts
/home/ccuser/the-50-dollar-app/src/lib/messaging.ts


No files found


Now let me search for more specific files related to uploads, photo handling, job creation, and worker profiles.


Found 22 files
src/app/page.tsx
src/app/(authenticated)/contractor/profile/page.tsx
src/app/(authenticated)/ratings/[userId]/page.tsx
src/app/api/ai/voice-complete/route.ts
src/app/api/ratings/route.ts
src/app/(authenticated)/worker/signup/page.tsx
src/app/(authenticated)/contractor/matches/page.tsx
src/app/api/messages/[id]/route.ts
src/app/(authenticated)/contractor/signup/page.tsx
src/app/(authenticated)/messages/page.tsx
src/app/(authenticated)/messages/[conversationId]/page.tsx
src/app/(authenticated)/worker/profile/error.tsx
src/app/(authenticated)/worker/profile/page.tsx
src/app/faq/page.tsx
src/components/avatar-upload.tsx
src/components/ratings-list.tsx
src/proxy.ts
src/components/ConversationPreview.tsx
src/lib/types.ts
src/app/layout.tsx
src/app/(authenticated)/layout.tsx
src/app/offline/page.tsx


Now let me read the specific source files I found:


     1→import { createClient } from "@/lib/supabase/server";
     2→import { NextRequest, NextResponse } from "next/server";
     3→import { logger } from "@/lib/logger";
     4→import { withCSRFProtection } from "@/lib/csrf";
     5→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     6→
     7→// 20 conversation starts per 15 min window per IP (prevents spam conversation creation)
     8→const START_RATE_LIMIT = 20;
     9→const START_WINDOW_MS = 15 * 60 * 1000;
    10→
    11→const handler = async (request: NextRequest) => {
    12→  try {
    13→    const clientIP = getClientIP(request);
    14→    const rateLimit = checkRateLimit(`messages-start:${clientIP}`, START_RATE_LIMIT, START_WINDOW_MS);
    15→
    16→    if (!rateLimit.allowed) {
    17→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    18→      return createRateLimitResponse(clientIP, retryAfter);
    19→    }
    20→
    21→    const supabase = await createClient();
    22→
    23→    // Check if user is authenticated
    24→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    25→
    26→    if (authError || !user) {
    27→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    28→    }
    29→
    30→    const { recipientId, initialMessage } = await request.json();
    31→
    32→    if (!recipientId) {
    33→      return NextResponse.json(
    34→        { error: "Recipient ID is required" },
    35→        { status: 400 }
    36→      );
    37→    }
    38→
    39→    // Verify recipient exists
    40→    const { data: recipient, error: recipientError } = await supabase
    41→      .from('profiles')
    42→      .select('id, name')
    43→      .eq('id', recipientId)
    44→      .single();
    45→
    46→    if (recipientError || !recipient) {
    47→      return NextResponse.json(
    48→        { error: "Recipient not found" },
    49→        { status: 404 }
    50→      );
    51→    }
    52→
    53→    // Get or create conversation using the database function
    54→    const { data: conversationId, error: conversationError } = await supabase
    55→      .rpc('get_or_create_conversation', {
    56→        user1_id: user.id,
    57→        user2_id: recipientId
    58→      });
    59→
    60→    if (conversationError) {
    61→      logger.error("Error creating conversation:", conversationError);
    62→      return NextResponse.json(
    63→        { error: "Failed to create conversation" },
    64→        { status: 500 }
    65→      );
    66→    }
    67→
    68→    // If there's an initial message, send it
    69→    if (initialMessage?.trim()) {
    70→      const { error: messageError } = await supabase
    71→        .from('messages')
    72→        .insert({
    73→          conversation_id: conversationId,
    74→          sender_id: user.id,
    75→          content: initialMessage.trim()
    76→        });
    77→
    78→      if (messageError) {
    79→        logger.error("Error sending initial message:", messageError);
    80→        // Don't fail the whole request if message fails
    81→      }
    82→    }
    83→
    84→    return NextResponse.json({
    85→      conversationId,
    86→      message: "Conversation created successfully"
    87→    });
    88→  } catch (error) {
    89→    logger.error("Error in start conversation API:", error);
    90→    return NextResponse.json(
    91→      { error: "Internal server error" },
    92→      { status: 500 }
    93→    );
    94→  }
    95→}
    96→
    97→// Apply CSRF protection to the handler
    98→export const POST = withCSRFProtection(handler);
    99→

<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 { 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→
     6→// 100 conversation list fetches per 15 min window per IP
     7→const MESSAGE_RATE_LIMIT = 100;
     8→const MESSAGE_WINDOW_MS = 15 * 60 * 1000;
     9→
    10→export async function GET(request: NextRequest) {
    11→  try {
    12→    const clientIP = getClientIP(request);
    13→    const rateLimit = checkRateLimit(`messages-list:${clientIP}`, MESSAGE_RATE_LIMIT, MESSAGE_WINDOW_MS);
    14→
    15→    if (!rateLimit.allowed) {
    16→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    17→      return createRateLimitResponse(clientIP, retryAfter);
    18→    }
    19→
    20→    const supabase = await createClient();
    21→
    22→    // Check if user is authenticated
    23→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    24→
    25→    if (authError || !user) {
    26→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    27→    }
    28→
    29→    // Update last_seen_at
    30→    await supabase
    31→      .from('profiles')
    32→      .update({ last_seen_at: new Date().toISOString() })
    33→      .eq('id', user.id);
    34→
    35→    // Get conversation previews using the database function
    36→    const { data: conversations, error } = await supabase
    37→      .rpc('get_conversation_previews', { user_id: user.id });
    38→
    39→    if (error) {
    40→      logger.error("Error fetching conversations:", error);
    41→      return NextResponse.json(
    42→        { error: "Failed to fetch conversations" },
    43→        { status: 500 }
    44→      );
    45→    }
    46→
    47→    return NextResponse.json({ conversations: conversations || [] });
    48→  } catch (error) {
    49→    logger.error("Error in messages API:", error);
    50→    return NextResponse.json(
    51→      { error: "Internal server error" },
    52→      { status: 500 }
    53→    );
    54→  }
    55→}

<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 { createClient } from "@/lib/supabase/server";
     2→import { NextRequest, NextResponse } from "next/server";
     3→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     4→import { logger } from "@/lib/logger";
     5→
     6→// 200 message fetches per 15 min window per IP (higher limit for active conversations)
     7→const GET_MESSAGES_RATE_LIMIT = 200;
     8→const GET_MESSAGES_WINDOW_MS = 15 * 60 * 1000;
     9→
    10→export async function GET(
    11→  request: NextRequest,
    12→  { params }: { params: Promise<{ id: string }> }
    13→) {
    14→  try {
    15→    const clientIP = getClientIP(request);
    16→    const rateLimit = checkRateLimit(`messages-get:${clientIP}`, GET_MESSAGES_RATE_LIMIT, GET_MESSAGES_WINDOW_MS);
    17→
    18→    if (!rateLimit.allowed) {
    19→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    20→      return createRateLimitResponse(clientIP, retryAfter);
    21→    }
    22→
    23→    const { id: conversationId } = await params;
    24→    const supabase = await createClient();
    25→
    26→    // Parse pagination params from URL
    27→    const { searchParams } = new URL(request.url);
    28→    const cursor = searchParams.get('cursor'); // message ID to fetch before
    29→    const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 100); // Max 100 messages per request
    30→
    31→    // Check if user is authenticated
    32→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    33→
    34→    if (authError || !user) {
    35→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    36→    }
    37→
    38→    // Verify user is a participant in this conversation
    39→    const { data: participant, error: participantError } = await supabase
    40→      .from('conversation_participants')
    41→      .select('id')
    42→      .eq('conversation_id', conversationId)
    43→      .eq('profile_id', user.id)
    44→      .single();
    45→
    46→    if (participantError || !participant) {
    47→      return NextResponse.json({ error: "Conversation not found" }, { status: 404 });
    48→    }
    49→
    50→    // Get conversation details with other participant info
    51→    const { data: conversationData, error: conversationError } = await supabase
    52→      .from('conversation_participants')
    53→      .select(`
    54→        profile_id,
    55→        profiles!inner(
    56→          id,
    57→          name,
    58→          avatar_url,
    59→          type
    60→        )
    61→      `)
    62→      .eq('conversation_id', conversationId)
    63→      .neq('profile_id', user.id)
    64→      .single();
    65→
    66→    if (conversationError || !conversationData) {
    67→      return NextResponse.json(
    68→        { error: "Failed to fetch conversation details" },
    69→        { status: 500 }
    70→      );
    71→    }
    72→
    73→    // Get messages in this conversation with cursor pagination
    74→    let query = supabase
    75→      .from('messages')
    76→      .select(`
    77→        id,
    78→        content,
    79→        sender_id,
    80→        status,
    81→        created_at,
    82→        attachments
    83→      `)
    84→      .eq('conversation_id', conversationId)
    85→      .order('created_at', { ascending: true })
    86→      .limit(limit);
    87→
    88→    // If cursor provided, fetch messages after this cursor (chronologically)
    89→    if (cursor) {
    90→      // Get the timestamp of the cursor message
    91→      const { data: cursorMessage } = await supabase
    92→        .from('messages')
    93→        .select('created_at')
    94→        .eq('id', cursor)
    95→        .single();
    96→
    97→      if (cursorMessage) {
    98→        query = query.gt('created_at', cursorMessage.created_at);
    99→      }
   100→    }
   101→
   102→    const { data: messages, error: messagesError } = await query;
   103→
   104→    if (messagesError) {
   105→      return NextResponse.json(
   106→        { error: "Failed to fetch messages" },
   107→        { status: 500 }
   108→      );
   109→    }
   110→
   111→    // Update last_read_at and last_seen_at
   112→    const now = new Date().toISOString();
   113→    await supabase
   114→      .from('conversation_participants')
   115→      .update({ last_read_at: now })
   116→      .eq('conversation_id', conversationId)
   117→      .eq('profile_id', user.id);
   118→
   119→    await supabase
   120→      .from('profiles')
   121→      .update({ last_seen_at: now })
   122→      .eq('id', user.id);
   123→
   124→    // Cast profiles to single object (inner join guarantees single row)
   125→    const profile = conversationData.profiles as unknown as {
   126→      id: string;
   127→      name: string;
   128→      avatar_url: string | null;
   129→      type: string;
   130→    };
   131→
   132→    // Check if other user was active in the last 5 minutes
   133→    const { data: otherProfile } = await supabase
   134→      .from('profiles')
   135→      .select('last_seen_at')
   136→      .eq('id', profile.id)
   137→      .single();
   138→
   139→    const fiveMinAgo = Date.now() - 5 * 60 * 1000;
   140→    const isOnline = otherProfile?.last_seen_at
   141→      ? new Date(otherProfile.last_seen_at).getTime() > fiveMinAgo
   142→      : false;
   143→
   144→    // Determine if there are more messages (next cursor)
   145→    const hasMore = messages && messages.length === limit;
   146→    const nextCursor = hasMore && messages.length > 0
   147→      ? messages[messages.length - 1].id
   148→      : null;
   149→
   150→    return NextResponse.json({
   151→      conversation: {
   152→        id: conversationId,
   153→        otherParticipant: {
   154→          id: profile.id,
   155→          name: profile.name,
   156→          avatar: profile.avatar_url,
   157→          type: profile.type,
   158→          isOnline,
   159→        },
   160→        messages: messages?.map(msg => ({
   161→          id: msg.id,
   162→          content: msg.content,
   163→          isOwn: msg.sender_id === user.id,
   164→          timestamp: msg.created_at,
   165→          status: msg.status,
   166→          attachments: msg.attachments
   167→        })) || [],
   168→        pagination: {
   169→          nextCursor,
   170→          hasMore,
   171→          limit
   172→        }
   173→      }
   174→    });
   175→  } catch (error) {
   176→    logger.error("Error in conversation API:", error);
   177→    return NextResponse.json(
   178→      { error: "Internal server error" },
   179→      { status: 500 }
   180→    );
   181→  }
   182→}

<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 { 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,
    31→  { params }: { params: Promise<{ id: string }> }
    32→) {
    33→  try {
    34→    const clientIP = getClientIP(request);
    35→    const rateLimit = checkRateLimit(`messages-send:${clientIP}`, SEND_MESSAGE_RATE_LIMIT, SEND_MESSAGE_WINDOW_MS);
    36→
    37→    if (!rateLimit.allowed) {
    38→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    39→      return createRateLimitResponse(clientIP, retryAfter);
    40→    }
    41→
    42→    // CSRF protection: validate same-origin and token
    43→    if (!isSameOrigin(request)) {
    44→      return NextResponse.json({ error: 'Cross-origin request not allowed' }, { status: 403 });
    45→    }
    46→    const csrfValid = await validateCSRFToken(request);
    47→    if (!csrfValid) {
    48→      return NextResponse.json({ error: 'Invalid or missing CSRF token' }, { status: 403 });
    49→    }
    50→
    51→    const { id: conversationId } = await params;
    52→    const supabase = await createClient();
    53→
    54→    // Check if user is authenticated
    55→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    56→
    57→    if (authError || !user) {
    58→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    59→    }
    60→
    61→    // Parse and validate request body
    62→    const body = await request.json();
    63→    const validationResult = sendMessageSchema.safeParse(body);
    64→
    65→    if (!validationResult.success) {
    66→      return NextResponse.json(
    67→        {
    68→          error: "Invalid request data",
    69→          details: validationResult.error.issues.map(e => ({
    70→            field: e.path.join('.'),
    71→            message: e.message
    72→          }))
    73→        },
    74→        { status: 400 }
    75→      );
    76→    }
    77→
    78→    const { content, attachments } = validationResult.data;
    79→    const sanitizedContent = sanitizeInput(content, { maxLength: MAX_LENGTH_MESSAGE_CONTENT });
    80→
    81→    if (!sanitizedContent) {
    82→      return NextResponse.json(
    83→        { error: "Message content is required" },
    84→        { status: 400 }
    85→      );
    86→    }
    87→
    88→    // Verify user is a participant in this conversation
    89→    const { data: participant, error: participantError } = await supabase
    90→      .from('conversation_participants')
    91→      .select('id')
    92→      .eq('conversation_id', conversationId)
    93→      .eq('profile_id', user.id)
    94→      .single();
    95→
    96→    if (participantError || !participant) {
    97→      return NextResponse.json({ error: "Conversation not found" }, { status: 404 });
    98→    }
    99→
   100→    // Insert the message
   101→    const { data: message, error: messageError } = await supabase
   102→      .from('messages')
   103→      .insert({
   104→        conversation_id: conversationId,
   105→        sender_id: user.id,
   106→        content: sanitizedContent,
   107→        attachments: attachments
   108→      })
   109→      .select(`
   110→        id,
   111→        content,
   112→        sender_id,
   113→        status,
   114→        created_at,
   115→        attachments
   116→      `)
   117→      .single();
   118→
   119→    if (messageError) {
   120→      logger.error("Error sending message:", messageError);
   121→      return NextResponse.json(
   122→        { error: "Failed to send message" },
   123→        { status: 500 }
   124→      );
   125→    }
   126→
   127→    // Get the other participant to send notification
   128→    const { data: otherParticipant } = await supabase
   129→      .from('conversation_participants')
   130→      .select('profile_id')
   131→      .eq('conversation_id', conversationId)
   132→      .neq('profile_id', user.id)
   133→      .single();
   134→
   135→    if (otherParticipant) {
   136→      // Create notification for the other participant
   137→      const { data: senderProfile } = await supabase
   138→        .from('profiles')
   139→        .select('name')
   140→        .eq('id', user.id)
   141→        .single();
   142→
   143→      const { error: notifError } = await supabase.from('notifications').insert({
   144→        profile_id: otherParticipant.profile_id,
   145→        type: 'system',
   146→        title: 'New message',
   147→        body: `${senderProfile?.name || 'Someone'} sent you a message`,
   148→        data: {
   149→          conversationId: conversationId,
   150→          senderId: user.id
   151→        }
   152→      });
   153→      if (notifError) {
   154→        logger.error('Failed to create message notification:', notifError);
   155→      }
   156→    }
   157→
   158→    return NextResponse.json({
   159→      message: {
   160→        id: message.id,
   161→        content: message.content,
   162→        isOwn: true,
   163→        timestamp: message.created_at,
   164→        status: message.status,
   165→        attachments: message.attachments
   166→      }
   167→    });
   168→  } catch (error) {
   169→    logger.error("Error in send message API:", error);
   170→    return NextResponse.json(
   171→      { error: "Internal server error" },
   172→      { status: 500 }
   173→    );
   174→  }
   175→}

<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
    31→      .from('payments')
    32→      .select('match_id, company_id, companies!inner(profile_id)')
    33→      .eq('stripe_payment_id', paymentIntentId)
    34→      .single();
    35→
    36→    if (!payment || (payment.companies as { profileId?: string }).profileId !== user.id) {
    37→      return NextResponse.json({ error: 'Payment not found or unauthorized' }, { status: 403 });
    38→    }
    39→
    40→    // Return payment intent status
    41→    return NextResponse.json({
    42→      status: paymentIntent.status,
    43→      amount: paymentIntent.amount,
    44→      currency: paymentIntent.currency,
    45→    });
    46→  } catch (error) {
    47→    logger.error('Payment verification error:', error);
    48→    if (error instanceof Error && error.message.includes('No such payment_intent')) {
    49→      return NextResponse.json({ error: 'Payment not found' }, { status: 404 });
    50→    }
    51→    return NextResponse.json({ error: 'Failed to verify payment' }, { status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
    52→  }
    53→}
    54→

<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 let me search for avatar-upload, job creation, and worker profile files:


1→"use client";
     2→
     3→import { useState, useRef } from "react";
     4→import { logger } from "@/lib/logger";
     5→import { Camera, Loader2, User } from "lucide-react";
     6→import { createClient } from "@/lib/supabase/client";
     7→import toast from "react-hot-toast";
     8→
     9→interface AvatarUploadProps {
    10→  currentUrl: string | null;
    11→  userName: string;
    12→}
    13→
    14→export function AvatarUpload({ currentUrl, userName }: AvatarUploadProps) {
    15→  const [avatarUrl, setAvatarUrl] = useState<string | null>(currentUrl);
    16→  const [uploading, setUploading] = useState(false);
    17→  const fileInputRef = useRef<HTMLInputElement>(null);
    18→
    19→  const initials = userName
    20→    .split(" ")
    21→    .map((n) => n[0])
    22→    .join("")
    23→    .toUpperCase()
    24→    .slice(0, 2);
    25→
    26→  async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
    27→    const file = e.target.files?.[0];
    28→    if (!file) return;
    29→
    30→    // Validate file
    31→    if (!file.type.startsWith("image/")) {
    32→      toast.error("Please select an image file");
    33→      return;
    34→    }
    35→    if (file.size > 2 * 1024 * 1024) {
    36→      toast.error("Image must be under 2MB");
    37→      return;
    38→    }
    39→
    40→    setUploading(true);
    41→
    42→    try {
    43→      const supabase = createClient();
    44→      const { data: { user } } = await supabase.auth.getUser();
    45→      if (!user) {
    46→        toast.error("Not logged in");
    47→        return;
    48→      }
    49→
    50→      const ext = file.name.split(".").pop() || "jpg";
    51→      const filePath = `${user.id}/avatar.${ext}`;
    52→
    53→      // Upload to Supabase Storage
    54→      const { error: uploadError } = await supabase.storage
    55→        .from("avatars")
    56→        .upload(filePath, file, {
    57→          cacheControl: "3600",
    58→          upsert: true,
    59→        });
    60→
    61→      if (uploadError) {
    62→        // If bucket doesn't exist, show helpful message
    63→        if (uploadError.message?.includes("not found") || uploadError.message?.includes("Bucket")) {
    64→          toast.error("Avatar storage not set up yet. Ask admin to create 'avatars' bucket in Supabase.");
    65→          return;
    66→        }
    67→        throw uploadError;
    68→      }
    69→
    70→      // Get public URL
    71→      const { data: { publicUrl } } = supabase.storage
    72→        .from("avatars")
    73→        .getPublicUrl(filePath);
    74→
    75→      // Update profile
    76→      const { error: updateError } = await supabase
    77→        .from("profiles")
    78→        .update({ avatar_url: publicUrl })
    79→        .eq("id", user.id);
    80→
    81→      if (updateError) throw updateError;
    82→
    83→      setAvatarUrl(publicUrl + "?t=" + Date.now()); // cache bust
    84→      toast.success("Profile photo updated!");
    85→    } catch (error) {
    86→      logger.error("Upload error:", error);
    87→      toast.error("Failed to upload photo. Please try again.");
    88→    } finally {
    89→      setUploading(false);
    90→      // Reset input so same file can be selected again
    91→      if (fileInputRef.current) fileInputRef.current.value = "";
    92→    }
    93→  }
    94→
    95→  return (
    96→

    97→      <button
    98→        onClick={() => fileInputRef.current?.click()}
    99→        disabled={uploading}
   100→        className="group relative h-24 w-24 overflow-hidden rounded-full border-4 border-background shadow-lg"
   101→      >
   102→        {avatarUrl ? (
   103→
   108→        ) : (
   109→

   110→            {initials || <User className="h-10 w-10" />}
   111→

   112→        )}
   113→
   114→        {/* Overlay */}
   115→

   116→          {uploading ? (
   117→            <Loader2 className="h-6 w-6 animate-spin text-white" />
   118→          ) : (
   119→            <Camera className="h-6 w-6 text-white" />
   120→          )}
   121→

   122→      </button>
   123→
   124→      <input
   125→        ref={fileInputRef}
   126→        type="file"
   127→        accept="image/*"
   128→        onChange={handleUpload}
   129→        className="hidden"
   130→      />
   131→
   132→      {/* Small camera badge */}
   133→

   134→        <Camera className="h-4 w-4" />
   135→

   136→

   137→  );
   138→}
   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→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import Image from "next/image";
     7→import { Loader2, HardHat } from "lucide-react";
     8→import { useWorkerAuth } from "./hooks/useWorkerAuth";
     9→import { useWorkerProfile } from "./hooks/useWorkerProfile";
    10→import { useVoiceProcessing } from "./hooks/useVoiceProcessing";
    11→import { AuthStep } from "./components/AuthStep";
    12→import { MethodStep } from "./components/MethodStep";
    13→import { VoiceStep } from "./components/VoiceStep";
    14→import { TradeStep } from "./components/TradeStep";
    15→import { ExperienceStep } from "./components/ExperienceStep";
    16→import { LocationStep } from "./components/LocationStep";
    17→import { AvailabilityStep } from "./components/AvailabilityStep";
    18→import { CertificationsStep } from "./components/CertificationsStep";
    19→import { ReviewStep } from "./components/ReviewStep";
    20→import { ProgressBar } from "./components/ProgressBar";
    21→import type { Step } from "./types";
    22→
    23→export default function WorkerSignupPage() {
    24→  const [step, setStep] = useState<Step>("auth");
    25→
    26→  const {
    27→    isAuthenticated,
    28→    loading: authLoading,
    29→    email,
    30→    setEmail,
    31→    password,
    32→    setPassword,
    33→    authError,
    34→    authLoading: isAuthLoading,
    35→    handleAuth,
    36→  } = useWorkerAuth();
    37→
    38→  const {
    39→    selectedTrades,
    40→    setSelectedTrades,
    41→    experience,
    42→    setExperience,
    43→    suburb,
    44→    setSuburb,
    45→    availability,
    46→    setAvailability,
    47→    certifications,
    48→    setCertifications,
    49→    saving,
    50→    toggleTrade,
    51→    toggleCert,
    52→    saveWorkerProfile,
    53→    workerData,
    54→  } = useWorkerProfile();
    55→
    56→  const {
    57→    voiceState,
    58→    handleVoiceComplete,
    59→  } = useVoiceProcessing(setSelectedTrades, setExperience, setSuburb, setAvailability, setStep);
    60→
    61→  useEffect(() => {
    62→    if (isAuthenticated && step === "auth") {
    63→      setStep("method");
    64→    }
    65→  }, [isAuthenticated, step]);
    66→
    67→  const stepOrder: Step[] = ["auth", "method", "voice", "trade", "experience", "location", "availability", "certs", "review"];
    68→  const progressSteps = ["auth", "method", "trade", "experience", "location", "availability", "certs", "review"]; // Exclude voice from progress bar
    69→  const activeSteps = isAuthenticated ? stepOrder.slice(1) : stepOrder;
    70→  const activeProgressSteps = isAuthenticated ? progressSteps.slice(1) : progressSteps;
    71→  const currentIndex = activeProgressSteps.indexOf(step === "voice" ? "method" : step); // Map voice to method for progress
    72→  const totalSteps = activeProgressSteps.length;
    73→
    74→  if (authLoading) {
    75→    return (
    76→

    77→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
    78→

    79→    );
    80→  }
    81→
    82→  return (
    83→

    84→      {/* Header */}
    85→
    86→

    87→          <Link href="/" className="flex items-center gap-2">
    88→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
    89→          </Link>
    90→

    91→
    92→
    93→
    94→        {/* Progress bar */}
    95→        <ProgressBar currentStep={currentIndex} totalSteps={totalSteps} />
    96→
    97→        {/* Step: Auth */}
    98→        {step === "auth" && (
    99→          <AuthStep
   100→            email={email}
   101→            setEmail={setEmail}
   102→            password={password}
   103→            setPassword={setPassword}
   104→            authError={authError}
   105→            authLoading={isAuthLoading}
   106→            onSubmit={handleAuth}
   107→          />
   108→        )}
   109→
   110→        {/* Step: Choose method */}
   111→        {step === "method" && (
   112→          <MethodStep
   113→            onTypeSelect={() => setStep("trade")}
   114→            onVoiceSelect={() => setStep("voice")}
   115→          />
   116→        )}
   117→
   118→        {/* Step: Voice recording */}
   119→        {step === "voice" && (
   120→          <VoiceStep
   121→            voiceProcessing={voiceState.processing}
   122→            voiceStatus={voiceState.status}
   123→            voiceError={voiceState.error}
   124→            onRecordingComplete={handleVoiceComplete}
   125→            onBack={() => setStep("method")}
   126→          />
   127→        )}
   128→
   129→        {/* Step: Select trade */}
   130→        {step === "trade" && (
   131→          <TradeStep
   132→            selectedTrades={selectedTrades}
   133→            onTradeToggle={toggleTrade}
   134→            onContinue={() => setStep("experience")}
   135→            onBack={() => setStep(isAuthenticated ? "method" : "auth")}
   136→          />
   137→        )}
   138→
   139→        {/* Step: Experience */}
   140→        {step === "experience" && (
   141→          <ExperienceStep
   142→            experience={experience}
   143→            onExperienceSelect={(exp) => {
   144→              setExperience(exp);
   145→              setStep("location");
   146→            }}
   147→            onBack={() => setStep("trade")}
   148→          />
   149→        )}
   150→
   151→        {/* Step: Location */}
   152→        {step === "location" && (
   153→          <LocationStep
   154→            suburb={suburb}
   155→            setSuburb={setSuburb}
   156→            onContinue={() => setStep("availability")}
   157→            onBack={() => setStep("experience")}
   158→          />
   159→        )}
   160→
   161→        {/* Step: Availability */}
   162→        {step === "availability" && (
   163→          <AvailabilityStep
   164→            availability={availability}
   165→            onAvailabilitySelect={(avail) => {
   166→              setAvailability(avail);
   167→              setStep("certs");
   168→            }}
   169→            onBack={() => setStep("location")}
   170→          />
   171→        )}
   172→
   173→        {/* Step: Certifications */}
   174→        {step === "certs" && (
   175→          <CertificationsStep
   176→            certifications={certifications}
   177→            onCertToggle={toggleCert}
   178→            onContinue={() => setStep("review")}
   179→            onBack={() => setStep("availability")}
   180→          />
   181→        )}
   182→
   183→        {/* Step: Review */}
   184→        {step === "review" && workerData && (
   185→          <ReviewStep
   186→            workerData={workerData}
   187→            onEdit={setStep}
   188→            onConfirm={saveWorkerProfile}
   189→            saving={saving}
   190→            onBack={() => setStep("certs")}
   191→          />
   192→        )}
   193→
   194→

   195→  );
   196→}

<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 { redirect } from "next/navigation";
     2→import Link from "next/link";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import {
     8→  HardHat,
     9→  MapPin,
    10→  Clock,
    11→  Shield,
    12→  Zap,
    13→  Edit3,
    14→  Wrench,
    15→  Calendar,
    16→  Sparkles,
    17→  Camera,
    18→  Star,
    19→} from "lucide-react";
    20→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    21→import { AvatarUpload } from "@/components/avatar-upload";
    22→import { RatingsSummary } from "@/components/ratings-summary";
    23→import { RatingsList } from "@/components/ratings-list";
    24→
    25→export default async function WorkerProfilePage() {
    26→  const supabase = await createClient();
    27→
    28→  const {
    29→    data: { user },
    30→  } = await supabase.auth.getUser();
    31→
    32→  if (!user) {
    33→    redirect("/auth/login");
    34→  }
    35→
    36→  const { data: profile } = await supabase
    37→    .from("profiles")
    38→    .select("id, name, avatar_url, suburb, onboarding_completed")
    39→    .eq("id", user.id)
    40→    .single();
    41→
    42→  // If onboarding not completed, send to build
    43→  if (!profile?.onboarding_completed) {
    44→    redirect("/worker/profile/build");
    45→  }
    46→
    47→  const { data: workerProfile } = await supabase
    48→    .from("worker_profiles")
    49→    .select("profile_id, trades, experience_years, certifications, bio, ai_extracted, availability")
    50→    .eq("profile_id", user.id)
    51→    .single();
    52→
    53→  if (!workerProfile) {
    54→    redirect("/worker/profile/build");
    55→  }
    56→
    57→  const trades = workerProfile.trades || [];
    58→  const skills = (workerProfile.ai_extracted as { skills?: string[] })?.skills || [];
    59→  const certs = workerProfile.certifications || [];
    60→  const expLabel = getExperienceLabel(workerProfile.experience_years);
    61→
    62→  // Fetch ratings with profile information
    63→  const { data: ratings } = await supabase
    64→    .from("ratings")
    65→    .select(`
    66→      *,
    67→      from_profile:profiles!ratings_from_id_fkey(
    68→        id,
    69→        name,
    70→        avatar_url
    71→      )
    72→    `)
    73→    .eq("to_id", user.id)
    74→    .order("created_at", { ascending: false })
    75→    .limit(3); // Show latest 3 ratings
    76→
    77→  const avgRating = ratings && ratings.length > 0
    78→    ? (ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length).toFixed(1)
    79→    : null;
    80→
    81→  const totalRatings = ratings?.length || 0;
    82→
    83→  return (
    84→

    85→
    86→        {/* Profile header */}
    87→

    88→          <AvatarUpload
    89→            currentUrl={profile.avatar_url}
    90→            userName={profile.name || user.email || ""}
    91→          />
    92→
    93→
{profile.name || "Worker"}

    94→
    95→

    96→            {trades.map((t: string) => (
    97→              <Badge key={t} variant="secondary" className="text-sm">
    98→                <Wrench className="mr-1 h-3 w-3" />
    99→                {t}
   100→              </Badge>
   101→            ))}
   102→

   103→
   104→

   105→            {profile.suburb && (
   106→
   107→                <MapPin className="h-3.5 w-3.5" />
   108→                {profile.suburb}
   109→
   110→            )}
   111→            {expLabel && (
   112→
   113→                <Clock className="h-3.5 w-3.5" />
   114→                {expLabel}
   115→
   116→            )}
   117→            {avgRating && (
   118→
   119→                ⭐ {avgRating} ({ratings!.length})
   120→
   121→            )}
   122→

   123→
   124→          {workerProfile.availability && (
   125→            <Badge variant="outline" className="mt-2">
   126→              <Calendar className="mr-1 h-3 w-3" />
   127→              {workerProfile.availability}
   128→            </Badge>
   129→          )}
   130→

   131→
   132→        {/* Bio */}
   133→        {workerProfile.bio && (
   134→          <Card className="mt-6">
   135→            <CardContent className="pt-5">
   136→

   137→                <Sparkles className="h-4 w-4" />
   138→                About me
   139→

   140→
{workerProfile.bio}

   141→            </CardContent>
   142→          </Card>
   143→        )}
   144→
   145→        {/* Skills */}
   146→        {skills.length > 0 && (
   147→          <Card className="mt-3">
   148→            <CardContent className="pt-5">
   149→

   150→                <Zap className="h-4 w-4" />
   151→                Skills
   152→

   153→

   154→                {skills.map((skill: string) => (
   155→                  <Badge key={skill} className="text-sm">
   156→                    {skill}
   157→                  </Badge>
   158→                ))}
   159→

   160→            </CardContent>
   161→          </Card>
   162→        )}
   163→
   164→        {/* Certifications */}
   165→        {certs.length > 0 && (
   166→          <Card className="mt-3">
   167→            <CardContent className="pt-5">
   168→

   169→                <Shield className="h-4 w-4" />
   170→                Certifications
   171→

   172→

   173→                {certs.map((cert: string) => (
   174→

   178→                    <Shield className="h-4 w-4 text-primary" />
   179→                    {cert}
   180→

   181→                ))}
   182→

   183→            </CardContent>
   184→          </Card>
   185→        )}
   186→
   187→        {/* Ratings */}
   188→        {(ratings && ratings.length > 0) && (
   189→

   190→            <RatingsSummary
   191→              averageRating={parseFloat(avgRating || "0")}
   192→              totalRatings={totalRatings}
   193→            />
   194→
   195→

   196→              <RatingsList
   197→                ratings={ratings}
   198→                title="Recent Reviews"
   199→              />
   200→
   201→              {totalRatings > 3 && (
   202→                <Button asChild variant="outline" className="w-full mt-4">
   203→                  <Link href={`/ratings/${user.id}`}>
   204→                    <Star className="h-4 w-4 mr-2" />
   205→                    View All Reviews ({totalRatings})
   206→                  </Link>
   207→                </Button>
   208→              )}
   209→

   210→

   211→        )}
   212→
   213→        {/* Edit button */}
   214→

   215→          <Button asChild variant="outline" className="w-full h-12 text-base gap-2">
   216→            <Link href="/worker/profile/build">
   217→              <Edit3 className="h-5 w-5" />
   218→              Edit Profile
   219→            </Link>
   220→          </Button>
   221→

   222→
   223→
   224→      <BottomNavWrapper userType="worker" />
   225→

   226→  );
   227→}
   228→
   229→function getExperienceLabel(years: number | null): string | null {
   230→  if (years === null || years === undefined) return null;
   231→  if (years === 0) return "< 1 year";
   232→  if (years <= 3) return `${years} years`;
   233→  if (years <= 5) return "3-5 years";
   234→  if (years <= 10) return "5-10 years";
   235→  return "10+ years";
   236→}
   237→

<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→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import Link from "next/link";
     7→import Image from "next/image";
     8→import toast from "react-hot-toast";
     9→import { createClient } from "@/lib/supabase/client";
    10→import { Button } from "@/components/ui/button";
    11→import { Input } from "@/components/ui/input";
    12→import { Label } from "@/components/ui/label";
    13→import { Card, CardContent } from "@/components/ui/card";
    14→import {
    15→  Building2,
    16→  Search,
    17→  Sparkles,
    18→  CheckCircle2,
    19→  ArrowLeft,
    20→  ArrowRight,
    21→  Loader2,
    22→  Edit3,
    23→  Globe,
    24→  MapPin,
    25→  Users,
    26→  FileText,
    27→} from "lucide-react";
    28→import { guessCompanyDomain } from "@/lib/company-utils";
    29→import { validateABN, formatABN } from "@/lib/abn-utils";
    30→
    31→type Step = "auth" | "company" | "researching" | "review";
    32→
    33→interface CompanyData {
    34→  name: string;
    35→  abn: string;
    36→  address: string;
    37→  industry: string;
    38→  size: string;
    39→  website: string;
    40→  logoUrl?: string | null;
    41→  logoSource?: 'clearbit' | 'google' | 'none';
    42→}
    43→
    44→async function lookupABN(abn: string): Promise<CompanyData> {
    45→  // Clean the ABN - remove spaces
    46→  const cleanAbn = abn.replace(/\s/g, '');
    47→
    48→  // Call the ABN lookup API
    49→  const response = await fetch(`/api/abn-lookup?abn=${cleanAbn}`);
    50→
    51→  if (!response.ok) {
    52→    const errData = await response.json().catch(() => null);
    53→    throw new Error(errData?.error || 'Failed to lookup ABN');
    54→  }
    55→
    56→  const result = await response.json();
    57→
    58→  if (!result.success || !result.data) {
    59→    throw new Error(result.error || 'Unable to lookup ABN');
    60→  }
    61→
    62→  const data = result.data;
    63→
    64→  // Map the ABR data to our CompanyData format
    65→  const companyData: CompanyData = {
    66→    name: data.name || 'Unknown Company',
    67→    abn: formatABN(data.abn || cleanAbn),
    68→    address: [data.postcode, data.state].filter(Boolean).join(' ') || 'Address not available',
    69→    industry: 'Construction',
    70→    size: 'Unknown',
    71→    website: '',
    72→  };
    73→
    74→  // Try to find company logo
    75→  await fetchCompanyLogo(companyData);
    76→
    77→  return companyData;
    78→}
    79→
    80→async function fetchCompanyLogo(companyData: CompanyData): Promise<void> {
    81→  try {
    82→    // If we have an explicit website, try that first
    83→    if (companyData.website) {
    84→      const domain = extractDomainFromUrl(companyData.website);
    85→      if (domain) {
    86→        const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
    87→        if (logoResponse.ok) {
    88→          const logoData = await logoResponse.json();
    89→          companyData.logoUrl = logoData.logoUrl;
    90→          companyData.logoSource = logoData.source;
    91→          return;
    92→        }
    93→      }
    94→    }
    95→
    96→    // Otherwise, try to guess domains from company name
    97→    const possibleDomains = guessCompanyDomain(companyData.name);
    98→
    99→    // Try each domain until we find a logo
   100→    for (const domain of possibleDomains) {
   101→      const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
   102→      if (logoResponse.ok) {
   103→        const logoData = await logoResponse.json();
   104→        if (logoData.logoUrl) {
   105→          companyData.logoUrl = logoData.logoUrl;
   106→          companyData.logoSource = logoData.source;
   107→          return;
   108→        }
   109→      }
   110→    }
   111→
   112→    // No logo found
   113→    companyData.logoUrl = null;
   114→    companyData.logoSource = 'none';
   115→  } catch (error) {
   116→    logger.error('Logo fetch failed:', error);
   117→    companyData.logoUrl = null;
   118→    companyData.logoSource = 'none';
   119→  }
   120→}
   121→
   122→function extractDomainFromUrl(url: string): string | null {
   123→  try {
   124→    // Add protocol if missing
   125→    if (!url.match(/^https?:\/\//)) {
   126→      url = 'https://' + url;
   127→    }
   128→
   129→    const urlObj = new URL(url);
   130→    return urlObj.hostname.replace(/^www\./, '');
   131→  } catch {
   132→    return null;
   133→  }
   134→}
   135→
   136→export default function ContractorSignupPage() {
   137→  const [step, setStep] = useState<Step>("auth");
   138→  const [isAuthenticated, setIsAuthenticated] = useState(false);
   139→  const [loading, setLoading] = useState(true);
   140→
   141→  // Auth state
   142→  const [email, setEmail] = useState("");
   143→  const [password, setPassword] = useState("");
   144→  const [authLoading, setAuthLoading] = useState(false);
   145→
   146→  // Company state
   147→  const [companyInput, setCompanyInput] = useState("");
   148→  const [companyData, setCompanyData] = useState<CompanyData | null>(null);
   149→  const [editField, setEditField] = useState<string | null>(null);
   150→  const [saving, setSaving] = useState(false);
   151→
   152→  const router = useRouter();
   153→  const supabase = createClient();
   154→
   155→  useEffect(() => {
   156→    async function checkAuth() {
   157→      const {
   158→        data: { user },
   159→      } = await supabase.auth.getUser();
   160→      if (user) {
   161→        setIsAuthenticated(true);
   162→        setStep("company");
   163→      }
   164→      setLoading(false);
   165→    }
   166→    checkAuth();
   167→  }, [supabase.auth]);
   168→
   169→  async function handleAuth(e: React.FormEvent) {
   170→    e.preventDefault();
   171→    setAuthLoading(true);
   172→
   173→    const { error } = await supabase.auth.signUp({
   174→      email,
   175→      password,
   176→      options: {
   177→        data: { type: "contractor" },
   178→        emailRedirectTo: `${window.location.origin}/auth/callback`,
   179→      },
   180→    });
   181→
   182→    if (error) {
   183→      if (error.message.includes("already registered")) {
   184→        const { error: loginError } = await supabase.auth.signInWithPassword({
   185→          email,
   186→          password,
   187→        });
   188→        if (loginError) {
   189→          toast.error(loginError.message);
   190→          setAuthLoading(false);
   191→          return;
   192→        }
   193→        // If login successful, check if email is verified
   194→        const { data: { user } } = await supabase.auth.getUser();
   195→        if (user && !user.email_confirmed_at) {
   196→          // Email not verified, redirect to verification page
   197→          router.push("/auth/verify-email");
   198→          return;
   199→        }
   200→      } else {
   201→        toast.error(error.message);
   202→        setAuthLoading(false);
   203→        return;
   204→      }
   205→    }
   206→
   207→    // Check if email verification is required
   208→    const { data: { user } } = await supabase.auth.getUser();
   209→    if (user && !user.email_confirmed_at) {
   210→      // Email not verified, redirect to verification page
   211→      router.push("/auth/verify-email");
   212→      return;
   213→    }
   214→
   215→    setIsAuthenticated(true);
   216→    setStep("company");
   217→    setAuthLoading(false);
   218→  }
   219→
   220→  async function handleCompanySearch(e: React.FormEvent) {
   221→    e.preventDefault();
   222→    if (!companyInput.trim()) return;
   223→
   224→    const input = companyInput.trim();
   225→
   226→    // Validate ABN checksum using the shared utility
   227→    if (!validateABN(input)) {
   228→      toast.error("Please enter a valid 11-digit ABN. Check the number and try again.");
   229→      return;
   230→    }
   231→
   232→    setStep("researching");
   233→
   234→    try {
   235→      const data = await lookupABN(input);
   236→      setCompanyData(data);
   237→      setStep("review");
   238→    } catch (error) {
   239→      const message = error instanceof Error ? error.message : 'Failed to lookup ABN';
   240→      toast.error(message);
   241→      setStep("company");
   242→    }
   243→  }
   244→
   245→  async function handleConfirm() {
   246→    if (!companyData) return;
   247→    setSaving(true);
   248→
   249→    try {
   250→      const {
   251→        data: { user },
   252→      } = await supabase.auth.getUser();
   253→      if (!user) {
   254→        toast.error("Session expired. Please log in again.");
   255→        router.push("/auth/login");
   256→        return;
   257→      }
   258→
   259→      const profileResult = await supabase.from("profiles").update({
   260→        name: companyData.name,
   261→        onboarding_completed: true,
   262→      }).eq("id", user.id);
   263→
   264→      if (profileResult.error) {
   265→        toast.error(`Failed to update profile: ${profileResult.error.message}`);
   266→        setSaving(false);
   267→        return;
   268→      }
   269→
   270→      const companyResult = await supabase.from("companies").insert({
   271→        profile_id: user.id,
   272→        name: companyData.name,
   273→        abn: companyData.abn,
   274→        address: companyData.address,
   275→        industry: companyData.industry,
   276→        size: companyData.size,
   277→        website: companyData.website,
   278→        ai_research: {
   279→          abr_verified: true,
   280→          abn: companyData.abn,
   281→          verified_at: new Date().toISOString(),
   282→          source: "abr",
   283→        },
   284→      });
   285→
   286→      if (companyResult.error) {
   287→        toast.error(`Failed to create company: ${companyResult.error.message}`);
   288→        setSaving(false);
   289→        return;
   290→      }
   291→
   292→      router.push("/dashboard");
   293→      router.refresh();
   294→    } catch (error) {
   295→      logger.error("Signup error:", error);
   296→      toast.error("Something went wrong. Please try again.");
   297→      setSaving(false);
   298→    }
   299→  }
   300→
   301→  function updateCompanyField(field: keyof CompanyData, value: string) {
   302→    if (!companyData) return;
   303→    setCompanyData({ ...companyData, [field]: value });
   304→    setEditField(null);
   305→  }
   306→
   307→  const totalSteps = isAuthenticated ? 3 : 4;
   308→  const currentStep =
   309→    step === "auth" ? 0 : step === "company" ? 1 : step === "researching" ? 2 : 3;
   310→
   311→  if (loading) {
   312→    return (
   313→

   314→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   315→

   316→    );
   317→  }
   318→
   319→  return (
   320→

   321→      {/* Header */}
   322→
   323→

   324→          <Link href="/" className="flex items-center gap-2">
   325→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
   326→          </Link>
   327→

   328→
   329→
   330→
   331→        {/* Progress bar */}
   332→

   333→          {Array.from({ length: totalSteps }, (_, i) => (
   334→            <div
   335→              key={i}
   336→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   337→                i <= currentStep - (isAuthenticated ? 1 : 0)
   338→                  ? "bg-primary"
   339→                  : "bg-muted"
   340→              }`}
   341→            />
   342→          ))}
   343→

   344→
   345→        {/* Step: Auth */}
   346→        {step === "auth" && (
   347→

   348→

   349→

   350→                <Building2 className="h-5 w-5" />
   351→

   352→

   353→
Create your account

   354→

   355→                  Quick signup to get started
   356→

   357→

   358→

   359→
   360→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   361→

   362→                Email
   363→                <Input
   364→                  id="email"
   365→                  type="email"
   366→                  placeholder="you@company.com.au"
   367→                  value={email}
   368→                  onChange={(e) => setEmail(e.target.value)}
   369→                  required
   370→                  autoComplete="email"
   371→                  className="h-12 text-base"
   372→                />
   373→

   374→

   375→                Password
   376→                <Input
   377→                  id="password"
   378→                  type="password"
   379→                  placeholder="Create a password"
   380→                  value={password}
   381→                  onChange={(e) => setPassword(e.target.value)}
   382→                  required
   383→                  minLength={6}
   384→                  autoComplete="new-password"
   385→                  className="h-12 text-base"
   386→                />
   387→

   388→
   389→              <Button
   390→                type="submit"
   391→                className="h-12 w-full gap-2 text-base font-semibold"
   392→                disabled={authLoading}
   393→              >
   394→                {authLoading ? (
   395→                  <>
   396→                    <Loader2 className="h-5 w-5 animate-spin" />
   397→                    Creating account...
   398→                  </>
   399→                ) : (
   400→                  <>
   401→                    Continue
   402→                    <ArrowRight className="h-5 w-5" />
   403→                  </>
   404→                )}
   405→              </Button>
   406→            </form>
   407→
   408→

   409→              Already have an account?{" "}
   410→              <Link
   411→                href="/auth/login"
   412→                className="text-primary underline hover:no-underline"
   413→              >
   414→                Sign in
   415→              </Link>
   416→

   417→

   418→        )}
   419→
   420→        {/* Step: Enter company name/ABN */}
   421→        {step === "company" && (
   422→

   423→

   424→

   425→                <Search className="h-5 w-5" />
   426→

   427→

   428→
Your company

   429→

   430→                  We'll verify it via ABR
   431→

   432→

   433→

   434→
   435→            <form onSubmit={handleCompanySearch} className="mt-6 space-y-4">
   436→

   437→                ABN
   438→                <Input
   439→                  id="company"
   440→                  type="text"
   441→                  placeholder="Enter your 11-digit ABN"
   442→                  value={companyInput}
   443→                  onChange={(e) => setCompanyInput(e.target.value)}
   444→                  required
   445→                  autoFocus
   446→                  className="h-12 text-base"
   447→                />
   448→

   449→                  Enter your 11-digit ABN for verification via the Australian Business Register.
   450→

   451→

   452→
   453→              <Button
   454→                type="submit"
   455→                className="h-12 w-full gap-2 text-base font-semibold"
   456→              >
   457→                <Search className="h-5 w-5" />
   458→                Verify my ABN
   459→              </Button>
   460→            </form>
   461→
   462→            {!isAuthenticated && (
   463→              <button
   464→                onClick={() => setStep("auth")}
   465→                className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   466→              >
   467→                <ArrowLeft className="h-4 w-4" />
   468→                Back
   469→              </button>
   470→            )}
   471→

   472→        )}
   473→
   474→        {/* Step: ABR Verification */}
   475→        {step === "researching" && (
   476→

   477→

   478→              <Sparkles className="h-10 w-10 animate-pulse text-primary" />
   479→

   480→
Verifying your business

   481→

   482→              Checking ABR for your business details...
   483→

   484→
   485→

   486→              <ResearchStep label="Checking ABR..." />
   487→              <ResearchStep label="Verifying company..." />
   488→              <ResearchStep label="Done" />
   489→

   490→

   491→        )}
   492→
   493→        {/* Step: Review profile */}
   494→        {step === "review" && companyData && (
   495→

   496→

   497→

   498→                <CheckCircle2 className="h-5 w-5" />
   499→

   500→

   501→
Confirm your profile

   502→

   503→                  Tap any field to edit
   504→

   505→

   506→

   507→
   508→            <Card className="mt-6">
   509→              <CardContent className="space-y-4 pt-6">
   510→                <EditableField
   511→                  icon={<Building2 className="h-4 w-4" />}
   512→                  label="Company name"
   513→                  value={companyData.name}
   514→                  fieldKey="name"
   515→                  editField={editField}
   516→                  setEditField={setEditField}
   517→                  onSave={(v) => updateCompanyField("name", v)}
   518→                  logoUrl={companyData.logoUrl}
   519→                />
   520→                <EditableField
   521→                  icon={<FileText className="h-4 w-4" />}
   522→                  label="ABN"
   523→                  value={companyData.abn}
   524→                  fieldKey="abn"
   525→                  editField={editField}
   526→                  setEditField={setEditField}
   527→                  onSave={(v) => updateCompanyField("abn", v)}
   528→                />
   529→                <EditableField
   530→                  icon={<MapPin className="h-4 w-4" />}
   531→                  label="Address"
   532→                  value={companyData.address}
   533→                  fieldKey="address"
   534→                  editField={editField}
   535→                  setEditField={setEditField}
   536→                  onSave={(v) => updateCompanyField("address", v)}
   537→                />
   538→                <EditableField
   539→                  icon={<Building2 className="h-4 w-4" />}
   540→                  label="Industry"
   541→                  value={companyData.industry}
   542→                  fieldKey="industry"
   543→                  editField={editField}
   544→                  setEditField={setEditField}
   545→                  onSave={(v) => updateCompanyField("industry", v)}
   546→                />
   547→                <EditableField
   548→                  icon={<Users className="h-4 w-4" />}
   549→                  label="Company size"
   550→                  value={companyData.size}
   551→                  fieldKey="size"
   552→                  editField={editField}
   553→                  setEditField={setEditField}
   554→                  onSave={(v) => updateCompanyField("size", v)}
   555→                />
   556→                <EditableField
   557→                  icon={<Globe className="h-4 w-4" />}
   558→                  label="Website"
   559→                  value={companyData.website}
   560→                  fieldKey="website"
   561→                  editField={editField}
   562→                  setEditField={setEditField}
   563→                  onSave={(v) => updateCompanyField("website", v)}
   564→                />
   565→              </CardContent>
   566→            </Card>
   567→
   568→

   569→

   570→                <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-600" />
   571→

   572→

   573→                    Verified via Australian Business Register
   574→

   575→

   576→                    Your ABN has been verified. Tap any field to correct it.
   577→

   578→

   579→

   580→

   581→
   582→            <Button
   583→              onClick={handleConfirm}
   584→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   585→              disabled={saving}
   586→            >
   587→              {saving ? (
   588→                <>
   589→                  <Loader2 className="h-5 w-5 animate-spin" />
   590→                  Saving...
   591→                </>
   592→              ) : (
   593→                <>
   594→                  <CheckCircle2 className="h-5 w-5" />
   595→                  Looks good — create my profile
   596→                </>
   597→              )}
   598→            </Button>
   599→
   600→            <button
   601→              onClick={() => setStep("company")}
   602→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   603→            >
   604→              <ArrowLeft className="h-4 w-4" />
   605→              Search again
   606→            </button>
   607→

   608→        )}
   609→
   610→

   611→  );
   612→}
   613→
   614→function ResearchStep({ label }: { label: string }) {
   615→  return (
   616→

   617→      <Loader2 className="h-4 w-4 animate-spin text-primary" />
   618→      {label}
   619→

   620→  );
   621→}
   622→
   623→function EditableField({
   624→  icon,
   625→  label,
   626→  value,
   627→  fieldKey,
   628→  editField,
   629→  setEditField,
   630→  onSave,
   631→  logoUrl,
   632→}: {
   633→  icon: React.ReactNode;
   634→  label: string;
   635→  value: string;
   636→  fieldKey: string;
   637→  editField: string | null;
   638→  setEditField: (f: string | null) => void;
   639→  onSave: (value: string) => void;
   640→  logoUrl?: string | null;
   641→}) {
   642→  const [editValue, setEditValue] = useState(value);
   643→  const isEditing = editField === fieldKey;
   644→
   645→  if (isEditing) {
   646→    return (
   647→

   648→        {label}
   649→

   650→          {fieldKey === 'name' && logoUrl ? (
   651→

   652→              <img
   653→                src={logoUrl}
   654→                alt={`${value} logo`}
   655→                className="h-8 w-8 rounded-full object-cover"
   656→                onError={(e) => {
   657→                  e.currentTarget.style.display = 'none';
   658→                }}
   659→              />
   660→              <Input
   661→                value={editValue}
   662→                onChange={(e) => setEditValue(e.target.value)}
   663→                className="h-10 text-base"
   664→                autoFocus
   665→              />
   666→

   667→          ) : (
   668→            <Input
   669→              value={editValue}
   670→              onChange={(e) => setEditValue(e.target.value)}
   671→              className="h-10 text-base"
   672→              autoFocus
   673→            />
   674→          )}
   675→          <Button
   676→            size="sm"
   677→            onClick={() => onSave(editValue)}
   678→            className="h-10 shrink-0"
   679→          >
   680→            Save
   681→          </Button>
   682→

   683→

   684→    );
   685→  }
   686→
   687→  return (
   688→    <button
   689→      onClick={() => {
   690→        setEditValue(value);
   691→        setEditField(fieldKey);
   692→      }}
   693→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   694→    >
   695→      {fieldKey === 'name' && logoUrl ? (
   696→

   697→          <img
   698→            src={logoUrl}
   699→            alt={`${value} logo`}
   700→            className="h-6 w-6 rounded-full object-cover"
   701→            onError={(e) => {
   702→              // If image fails to load, fall back to icon
   703→              e.currentTarget.style.display = 'none';
   704→            }}
   705→          />
   706→

   707→      ) : (
   708→
{icon}

   709→      )}
   710→

   711→
{label}

   712→
{value}

   713→

   714→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   715→    </button>
   716→  );
   717→}
   718→

<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→ * 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→  // Escape template literals by default
    85→  if (options.escapeTemplates !== false) {
    86→    sanitized = escapeTemplateLiterals(sanitized);
    87→  }
    88→
    89→  // Trim whitespace
    90→  sanitized = sanitized.trim();
    91→
    92→  // Limit length if specified
    93→  if (options.maxLength && sanitized.length > options.maxLength) {
    94→    sanitized = sanitized.substring(0, options.maxLength);
    95→  }
    96→
    97→  return sanitized;
    98→}
    99→
   100→/**
   101→ * Sanitize multiple string inputs at once with field-specific options
   102→ * Useful for form data where different fields have different validation rules
   103→ *
   104→ * @param inputs - Object with string values to sanitize
   105→ * @param fieldOptions - Per-field sanitization options (keyed by field name)
   106→ * @returns Object with sanitized values
   107→ *
   108→ * @example
   109→ * sanitizeInputs(
   110→ *   { name: 'John Doe', bio: 'Hello\x00World', email: '  john@example.com  ' },
   111→ *   { bio: { maxLength: 500 }, email: { maxLength: 100 } }
   112→ * )
   113→ * // Returns: { name: 'John Doe', bio: 'HelloWorld', email: 'john@example.com' }
   114→ */
   115→export function sanitizeInputs(
   116→  inputs: Record<string, string>,
   117→  fieldOptions: Record<string, { maxLength?: number }> = {}
   118→): Record<string, string> {
   119→  const sanitized: Record<string, string> = {};
   120→
   121→  for (const [key, value] of Object.entries(inputs)) {
   122→    const options = fieldOptions[key] || {};
   123→    sanitized[key] = sanitizeInput(value, options);
   124→  }
   125→
   126→  return sanitized;
   127→}

<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 let me search for job-related files and get more information about matches and other APIs:


Found 4 files
src/app/page.tsx
src/lib/constants.ts
src/app/(authenticated)/contractor/post-job/page.tsx
src/app/api/ai/generate-job/route.ts


No files found


     1→import { NextRequest, NextResponse } from "next/server";
     2→import { logger } from "@/lib/logger";
     3→import OpenAI from "openai";
     4→import { createClient } from "@/lib/supabase/server";
     5→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     6→import { sanitizeInput } from "@/lib/sanitize";
     7→import { withCSRFProtection } from "@/lib/csrf";
     8→import {
     9→  AI_RATE_LIMIT,
    10→  RATE_LIMIT_WINDOW_MS as AI_WINDOW_MS,
    11→  MAX_LENGTH_TRADE
    12→} from "@/lib/constants";
    13→
    14→if (!process.env.OPENAI_API_KEY) {
    15→  throw new Error("OPENAI_API_KEY environment variable is not set");
    16→}
    17→
    18→const openai = new OpenAI({
    19→  apiKey: process.env.OPENAI_API_KEY,
    20→});
    21→
    22→// Rate limit: 10 requests per 15 minutes per user
    23→
    24→const handler = async (request: NextRequest) => {
    25→  try {
    26→    const clientIP = getClientIP(request);
    27→    const rateLimit = checkRateLimit(`ai-job:${clientIP}`, AI_RATE_LIMIT, AI_WINDOW_MS);
    28→
    29→    if (!rateLimit.allowed) {
    30→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    31→      return createRateLimitResponse(clientIP, retryAfter);
    32→    }
    33→
    34→    const supabase = await createClient();
    35→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    36→    if (authError || !user) {
    37→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    38→    }
    39→
    40→    const { trade } = await request.json();
    41→
    42→    if (!trade) {
    43→      return NextResponse.json({ error: "Missing trade" }, { status: 400 });
    44→    }
    45→
    46→    const sanitizedTrade = sanitizeInput(trade, { maxLength: MAX_LENGTH_TRADE });
    47→
    48→    const completion = await openai.chat.completions.create({
    49→      model: "gpt-4o-mini",
    50→      messages: [
    51→        {
    52→          role: "system",
    53→          content: "You are an Australian construction industry expert. Always respond with valid JSON only.",
    54→        },
    55→        {
    56→          role: "user",
    57→          content: `Generate a job posting for a ${sanitizedTrade} in the Australian construction industry.
    58→
    59→Return JSON with:
    60→1. title: A concise job title (e.g. "Experienced Steel Fixer Needed")
    61→2. description: A 3-4 sentence job description mentioning key duties, site expectations, and any specific requirements. Use Australian English. Mention PPE, early starts if relevant.
    62→3. certifications: Array of required certifications (e.g. "White Card", "Working at Heights", "Trade Certificate", "Scaffolding Licence"). Always include "White Card".
    63→4. rateMin: Minimum hourly rate in AUD (realistic 2026 Australian market rate)
    64→5. rateMax: Maximum hourly rate in AUD
    65→6. rateType: "hourly" or "daily"
    66→
    67→Return ONLY valid JSON:
    68→{
    69→  "title": "string",
    70→  "description": "string",
    71→  "certifications": ["string"],
    72→  "rateMin": number,
    73→  "rateMax": number,
    74→  "rateType": "hourly"
    75→}`,
    76→        },
    77→      ],
    78→      temperature: 0.7,
    79→      response_format: { type: "json_object" },
    80→    });
    81→
    82→    const content = completion.choices[0]?.message?.content;
    83→    if (!content) {
    84→      throw new Error("No response from AI");
    85→    }
    86→
    87→    const generated = JSON.parse(content);
    88→
    89→    // Validate and sanitize rate values
    90→    const MIN_HOURLY_RATE = 25; // Australian minimum for construction
    91→    const MAX_HOURLY_RATE = 200; // Upper bound for specialist trades
    92→    const DEFAULT_MIN_RATE = 35;
    93→    const DEFAULT_MAX_RATE = 55;
    94→
    95→    let rateMin = typeof generated.rateMin === 'number' ? generated.rateMin : DEFAULT_MIN_RATE;
    96→    let rateMax = typeof generated.rateMax === 'number' ? generated.rateMax : DEFAULT_MAX_RATE;
    97→
    98→    // Clamp rates to reasonable bounds
    99→    rateMin = Math.max(MIN_HOURLY_RATE, Math.min(rateMin, MAX_HOURLY_RATE));
   100→    rateMax = Math.max(MIN_HOURLY_RATE, Math.min(rateMax, MAX_HOURLY_RATE));
   101→
   102→    // Ensure rateMax >= rateMin
   103→    if (rateMax < rateMin) {
   104→      [rateMin, rateMax] = [rateMax, rateMin]; // Swap if reversed
   105→    }
   106→
   107→    // Ensure meaningful range (at least $5/hr difference)
   108→    if (rateMax - rateMin < 5) {
   109→      rateMax = rateMin + 10;
   110→      // Re-clamp if we exceeded max
   111→      if (rateMax > MAX_HOURLY_RATE) {
   112→        rateMax = MAX_HOURLY_RATE;
   113→        rateMin = Math.max(MIN_HOURLY_RATE, rateMax - 10);
   114→      }
   115→    }
   116→
   117→    return NextResponse.json({
   118→      title: generated.title || "",
   119→      description: generated.description || "",
   120→      certifications: generated.certifications || ["White Card"],
   121→      rateMin,
   122→      rateMax,
   123→      rateType: generated.rateType || "hourly",
   124→    });
   125→  } catch (error) {
   126→    logger.error("AI job generation error:", error);
   127→
   128→    if (error instanceof Error) {
   129→      if (error.message.includes("API key")) {
   130→        return NextResponse.json({ error: "AI service configuration error" }, { status: 503 });
   131→      }
   132→      if (error.message.includes("rate limit") || error.message.includes("429")) {
   133→        return NextResponse.json({ error: "AI service is busy. Please try again." }, { status: 429 });
   134→      }
   135→    }
   136→
   137→    return NextResponse.json({ error: "Failed to generate job details" }, { status: 500 });
   138→  }
   139→};
   140→
   141→export const POST = withCSRFProtection(handler);
   142→

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


Let me continue reading more key files: