← back to Wine Finder Next
app/api/membership/bottles/[id]/route.ts
164 lines
import { NextRequest, NextResponse } from 'next/server';
import { bottleDb, membershipUnitDb, 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,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
// Rate limiting
const identifier = getClientIdentifier(request);
const rateLimit = checkRateLimit(identifier, getRateLimitConfig('/api/membership/bottles'));
if (!rateLimit.success) {
return NextResponse.json({
success: false,
error: 'Rate limit exceeded. Please try again later.'
} as ApiResponse<null>, { status: 429 });
}
// Validate bottle ID
const sanitizedId = sanitizeString(id, 100);
if (!validateBottleId(sanitizedId)) {
return NextResponse.json({
success: false,
error: 'Invalid bottle ID format'
} as ApiResponse<null>, { status: 400 });
}
const bottle = bottleDb.getById(sanitizedId);
if (!bottle) {
logAudit({
action: 'BOTTLE_VIEW_NOT_FOUND',
resource: `bottle/${sanitizedId}`,
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: false
});
return NextResponse.json({
success: false,
error: 'Bottle not found'
} as ApiResponse<null>, { status: 404 });
}
const membershipUnits = membershipUnitDb.getByBottleId(sanitizedId);
const votes = governanceVoteDb.getByBottleId(sanitizedId);
logAudit({
action: 'BOTTLE_VIEW',
resource: `bottle/${sanitizedId}`,
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: true,
metadata: { bottleId: sanitizedId, name: bottle.name }
});
return NextResponse.json({
success: true,
data: {
bottle,
membershipUnits,
votes
}
});
} catch (error) {
return NextResponse.json({
success: false,
error: 'Failed to fetch bottle details'
} as ApiResponse<null>, { status: 500 });
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
// Rate limiting
const identifier = getClientIdentifier(request);
const rateLimit = checkRateLimit(identifier, getRateLimitConfig('/api/membership/bottles'));
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,
params: { id },
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
const sanitizedId = sanitizeString(id, 100);
if (!validateBottleId(sanitizedId)) {
return NextResponse.json({
success: false,
error: 'Invalid bottle ID format'
} as ApiResponse<null>, { status: 400 });
}
const sanitizedBody = body.name ? { ...body, name: sanitizeString(body.name, 200) } : body;
const updated = bottleDb.update(sanitizedId, sanitizedBody);
if (!updated) {
logAudit({
action: 'BOTTLE_UPDATE_NOT_FOUND',
resource: `bottle/${sanitizedId}`,
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: false
});
return NextResponse.json({
success: false,
error: 'Bottle not found'
} as ApiResponse<null>, { status: 404 });
}
logAudit({
action: 'BOTTLE_UPDATE',
resource: `bottle/${sanitizedId}`,
ipAddress: identifier,
userAgent: request.headers.get('user-agent') || 'unknown',
success: true,
metadata: { bottleId: sanitizedId, changes: Object.keys(sanitizedBody) }
});
return NextResponse.json({
success: true,
data: updated,
message: 'Bottle updated successfully'
});
} catch (error) {
return NextResponse.json({
success: false,
error: 'Failed to update bottle'
} as ApiResponse<null>, { status: 500 });
}
}