| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { z } from 'zod' |
|
|
| |
| |
| |
| |
| const VALID_CROPS = ['corn', 'rice', 'soybean', 'wheat', 'tomato'] as const |
|
|
| |
| |
| |
| |
| export const cropSchema = z.enum(VALID_CROPS, { |
| message: 'Invalid crop type. Must be one of: corn, rice, soybean, wheat, tomato', |
| }) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const imageUploadSchema = z.object({ |
| |
| |
| |
| |
| |
| |
| image: z |
| .instanceof(File, { message: 'Image file is required' }) |
| .refine((file) => file.size > 0, { message: 'Image file cannot be empty' }) |
| .refine( |
| (file) => file.size <= 10 * 1024 * 1024, |
| { message: 'Image file size must be less than 10MB' } |
| ) |
| .refine( |
| (file) => file.type.startsWith('image/'), |
| { message: 'File must be an image (JPEG, PNG, etc.)' } |
| ), |
| |
| |
| |
| |
| |
| crop: cropSchema, |
| }) |
|
|
| |
| |
| |
| |
| |
| |
| export const predictionRequestSchema = imageUploadSchema.strict() |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function sanitizeFilename(filename: string): string { |
| |
| let sanitized = filename |
| .replace(/[\/\\]/g, '') |
| .replace(/\0/g, '') |
| .replace(/\.\./g, '') |
| .trim() |
|
|
| |
| const MAX_FILENAME_LENGTH = 255 |
| if (sanitized.length > MAX_FILENAME_LENGTH) { |
| const ext = sanitized.substring(sanitized.lastIndexOf('.')) |
| sanitized = sanitized.substring(0, MAX_FILENAME_LENGTH - ext.length) + ext |
| } |
|
|
| |
| sanitized = sanitized.replace(/[<>:"|?*\x00-\x1f]/g, '') |
|
|
| |
| if (!sanitized || sanitized === '.') { |
| sanitized = `file-${Date.now()}` |
| } |
|
|
| return sanitized |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function validateCrop(crop: unknown): z.infer<typeof cropSchema> { |
| return cropSchema.parse(crop) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function validatePredictionRequest( |
| formData: FormData |
| ): Promise<{ image: File; crop: z.infer<typeof cropSchema> }> { |
| const image = formData.get('image') |
| const crop = formData.get('crop') |
|
|
| |
| const validated = predictionRequestSchema.parse({ |
| image, |
| crop, |
| }) |
|
|
| |
| const sanitizedFilename = sanitizeFilename(validated.image.name) |
| |
| |
| |
| const sanitizedFile = new File( |
| [validated.image], |
| sanitizedFilename, |
| { type: validated.image.type } |
| ) |
|
|
| return { |
| image: sanitizedFile, |
| crop: validated.crop, |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export const locationSchema = z.object({ |
| lat: z.number().min(-90).max(90), |
| lng: z.number().min(-180).max(180), |
| }) |
|
|
| |
| |
| |
| |
| export const outbreakReportSchema = z.object({ |
| lat: z.number().min(-90).max(90), |
| lng: z.number().min(-180).max(180), |
| crop: cropSchema, |
| disease: z.string().min(1).max(200), |
| severity: z.enum(['low', 'medium', 'high']), |
| description: z.string().max(1000).optional(), |
| }).strict() |
|
|
| |
| |
| |
| |
| export const farmerRegistrationSchema = z.object({ |
| name: z.string().min(1).max(200), |
| lat: z.number().min(-90).max(90), |
| lng: z.number().min(-180).max(180), |
| crops: z.array(cropSchema).min(1).max(10), |
| }).strict() |
|
|
| |
| |
| |
| const VALID_US_STATE_CODES = [ |
| 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', |
| 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', |
| 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', |
| 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', |
| 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', |
| ] as const |
|
|
| export const usStateCodeSchema = z.enum(VALID_US_STATE_CODES, { |
| message: 'Invalid state code.', |
| }) |
|
|
| const trimmedString = (min: number, max: number, label: string) => |
| z.string() |
| .transform((value) => value.trim()) |
| .pipe(z.string().min(min, `${label} is required`).max(max, `${label} is too long`)) |
|
|
| export const displayNameSchema = trimmedString(1, 100, 'Name') |
|
|
| export const createFarmSchema = z.object({ |
| name: trimmedString(1, 120, 'Farm name'), |
| address: trimmedString(1, 240, 'Address'), |
| stateCode: usStateCodeSchema, |
| crops: z.array(cropSchema).min(1, 'Select at least one crop').max(5, 'Too many crops selected'), |
| acreage: z.number().min(0).max(1_000_000).nullable().optional(), |
| lat: z.number().min(-90).max(90).nullable().optional(), |
| lng: z.number().min(-180).max(180).nullable().optional(), |
| }).strict() |
|
|
| export const joinCodeSchema = z.string() |
| .transform((value) => value.trim().toUpperCase()) |
| .pipe(z.string().regex(/^[A-Z2-9]{6}$/, 'Enter the six-character farm join code.')) |
|
|
| export const farmSearchSchema = z.object({ |
| search: trimmedString(2, 80, 'Farm name or location'), |
| stateCode: usStateCodeSchema, |
| }).strict() |
|
|
| export const farmIdSchema = trimmedString(8, 128, 'Farm ID') |
|
|
| export const accessRequestStatusSchema = z.enum(['pending', 'approved', 'denied', 'expired']) |
|
|