← back to Wine Finder Next
app/api/membership/votes/route.ts
132 lines
import { NextRequest, NextResponse } from 'next/server';
import { governanceVoteDb } from '@/lib/membershipDatabase';
import type { ApiResponse } from '@/src/types/membership';
import { checkRateLimit, getRateLimitConfig, getClientIdentifier } from '@/lib/rateLimit';
import { logAudit } from '@/lib/auth';
import { sanitizeString, validateBottleId } from '@/lib/inputValidation';
import { analyzeRequest } from '@/lib/securityMonitoring';
export async function GET(request: NextRequest) {
try {
// Rate limiting
const identifier = getClientIdentifier(request);
const rateLimit = checkRateLimit(identifier, getRateLimitConfig('/api/membership/votes'));
if (!rateLimit.success) {
return NextResponse.json({
success: false,
error: 'Rate limit exceeded. Please try again later.'
} as ApiResponse<null>, { status: 429 });
}
const { searchParams } = new URL(request.url);
const bottleId = searchParams.get('bottleId');
const activeOnly = searchParams.get('active') === 'true';
// Validate bottleId if provided
if (bottleId) {
const sanitizedBottleId = sanitizeString(bottleId, 100);
if (!validateBottleId(sanitizedBottleId)) {
return NextResponse.json({
success: false,
error: 'Invalid bottle ID format'
} as ApiResponse<null>, { status: 400 });
}
}
let votes;
if (activeOnly) {
votes = governanceVoteDb.getActive();
} else if (bottleId) {
votes = governanceVoteDb.getByBottleId(sanitizeString(bottleId, 100));
} else {
votes = governanceVoteDb.getAll();
}
logAudit({
action: 'VOTES_LIST',
resource: 'votes',
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: true,
metadata: { count: votes.length, bottleId, activeOnly }
});
return NextResponse.json({
success: true,
data: votes
} as ApiResponse<typeof votes>);
} catch (error) {
return NextResponse.json({
success: false,
error: 'Failed to fetch votes'
} as ApiResponse<null>, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
// Rate limiting
const identifier = getClientIdentifier(request);
const rateLimit = checkRateLimit(identifier, getRateLimitConfig('/api/membership/votes'));
if (!rateLimit.success) {
return NextResponse.json({
success: false,
error: 'Rate limit exceeded. Please try again later.'
} as ApiResponse<null>, { status: 429 });
}
// Threat detection
const body = await request.json();
const analysis = analyzeRequest({
body,
headers: Object.fromEntries(request.headers),
ip: identifier
});
if (!analysis.safe) {
return NextResponse.json({
success: false,
error: 'Security threat detected'
} as ApiResponse<null>, { status: 403 });
}
// Validate and sanitize
if (body.title) {
body.title = sanitizeString(body.title, 200);
}
if (body.description) {
body.description = sanitizeString(body.description, 1000);
}
if (body.bottleId && !validateBottleId(body.bottleId)) {
return NextResponse.json({
success: false,
error: 'Invalid bottle ID format'
} as ApiResponse<null>, { status: 400 });
}
const newVote = governanceVoteDb.create(body);
logAudit({
action: 'VOTE_CREATE',
resource: `vote/${newVote.id}`,
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: true,
metadata: { voteId: newVote.id, bottleId: newVote.bottleId }
});
return NextResponse.json({
success: true,
data: newVote,
message: 'Vote created successfully'
} as ApiResponse<typeof newVote>);
} catch (error) {
return NextResponse.json({
success: false,
error: 'Failed to create vote'
} as ApiResponse<null>, { status: 500 });
}
}