← back to Wine Finder Next

app/api/membership/votes/[id]/cast/route.ts

153 lines

import { NextRequest, NextResponse } from 'next/server';
import { governanceVoteDb } from '@/lib/membershipDatabase';
import type { ApiResponse } from '@/src/types/membership';
import { checkRateLimit, getClientIdentifier, getRateLimitConfig } from '@/lib/rateLimit';
import { validateVoteId, validateUserId, validateMembershipUnitId, validateVoteChoice, sanitizeString } from '@/lib/inputValidation';
import { logAudit } from '@/lib/auth';

export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const ipAddress = request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown';
  const userAgent = request.headers.get('user-agent') || 'unknown';

  try {
    const { id } = await params;

    // Rate limiting
    const identifier = getClientIdentifier(request);
    const rateLimit = checkRateLimit(identifier, getRateLimitConfig('/api/membership/votes/'));

    if (!rateLimit.success) {
      logAudit({
        action: 'RATE_LIMIT_EXCEEDED',
        resource: `vote/${id}`,
        ipAddress,
        userAgent,
        success: false
      });

      return NextResponse.json({
        success: false,
        error: 'Rate limit exceeded. Please try again later.'
      } as ApiResponse<null>, {
        status: 429,
        headers: {
          'X-RateLimit-Limit': rateLimit.limit.toString(),
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': new Date(rateLimit.reset).toISOString(),
          'Retry-After': Math.ceil((rateLimit.reset - Date.now()) / 1000).toString()
        }
      });
    }

    // Input validation
    if (!validateVoteId(id)) {
      logAudit({
        action: 'INVALID_VOTE_ID',
        resource: `vote/${id}`,
        ipAddress,
        userAgent,
        success: false
      });

      return NextResponse.json({
        success: false,
        error: 'Invalid vote ID format'
      } as ApiResponse<null>, { status: 400 });
    }

    const body = await request.json();
    const { userId, membershipUnitId, choice } = body;

    if (!userId || !membershipUnitId || !choice) {
      return NextResponse.json({
        success: false,
        error: 'Missing required fields: userId, membershipUnitId, choice'
      } as ApiResponse<null>, { status: 400 });
    }

    // Sanitize and validate inputs
    const sanitizedUserId = sanitizeString(userId, 100);
    const sanitizedUnitId = sanitizeString(membershipUnitId, 100);
    const sanitizedChoice = sanitizeString(choice, 20);

    if (!validateUserId(sanitizedUserId)) {
      return NextResponse.json({
        success: false,
        error: 'Invalid user ID format'
      } as ApiResponse<null>, { status: 400 });
    }

    if (!validateMembershipUnitId(sanitizedUnitId)) {
      return NextResponse.json({
        success: false,
        error: 'Invalid membership unit ID format'
      } as ApiResponse<null>, { status: 400 });
    }

    if (!validateVoteChoice(sanitizedChoice)) {
      return NextResponse.json({
        success: false,
        error: 'Invalid choice. Must be: for, against, or abstain'
      } as ApiResponse<null>, { status: 400 });
    }

    const result = governanceVoteDb.castVote(id, sanitizedUserId, sanitizedUnitId, sanitizedChoice);

    if (!result) {
      logAudit({
        userId: sanitizedUserId,
        action: 'VOTE_CAST_FAILED',
        resource: `vote/${id}`,
        ipAddress,
        userAgent,
        success: false,
        metadata: { membershipUnitId: sanitizedUnitId, choice: sanitizedChoice }
      });

      return NextResponse.json({
        success: false,
        error: 'Failed to cast vote. Either vote not found, unit invalid, or already voted.'
      } as ApiResponse<null>, { status: 400 });
    }

    logAudit({
      userId: sanitizedUserId,
      action: 'VOTE_CAST',
      resource: `vote/${id}`,
      ipAddress,
      userAgent,
      success: true,
      metadata: { membershipUnitId: sanitizedUnitId, choice: sanitizedChoice }
    });

    return NextResponse.json({
      success: true,
      data: result,
      message: 'Vote cast successfully'
    }, {
      headers: {
        'X-RateLimit-Limit': rateLimit.limit.toString(),
        'X-RateLimit-Remaining': rateLimit.remaining.toString(),
        'X-RateLimit-Reset': new Date(rateLimit.reset).toISOString()
      }
    });
  } catch (error) {
    logAudit({
      action: 'VOTE_CAST_ERROR',
      resource: 'vote/unknown',
      ipAddress,
      userAgent,
      success: false,
      metadata: { error: error instanceof Error ? error.message : 'Unknown error' }
    });

    return NextResponse.json({
      success: false,
      error: 'Failed to cast vote'
    } as ApiResponse<null>, { status: 500 });
  }
}