← 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 });
}
}