/** * API Request Schemas * FE-TYPE-003: Create Zod schemas to validate API requests before sending * * Provides Zod schemas for all API request types to ensure type safety * and runtime validation of API requests before they are sent to the backend. */ import { z } from 'zod'; import { uuidSchema } from './apiSchemas'; /** * Email schema */ export const emailSchema = z.string().email('Invalid email format'); /** * Password schema (min 8 characters) */ export const passwordSchema = z .string() .min(8, 'Password must be at least 8 characters'); /** * Username schema */ export const usernameSchema = z .string() .min(3, 'Username must be at least 3 characters') .max(30, 'Username must be at most 30 characters') .regex( /^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores', ); /** * Login request schema */ export const loginRequestSchema = z.object({ email: emailSchema, password: z.string().min(1, 'Password is required'), }); export type LoginRequest = z.infer; /** * Register request schema */ export const registerRequestSchema = z.object({ username: usernameSchema, email: emailSchema, password: passwordSchema, first_name: z.string().max(100).optional(), last_name: z.string().max(100).optional(), }); export type RegisterRequest = z.infer; /** * Two-Factor Authentication (2FA) Request Schemas * HIGH PRIORITY: Security feature - validation critical */ /** * Verify 2FA code request schema * POST /auth/2fa/verify */ export const verify2FARequestSchema = z.object({ code: z .string() .min(6, 'TOTP code must be at least 6 characters') .max(6, 'TOTP code must be exactly 6 characters'), secret: z.string().min(1, 'Secret is required'), }); export type Verify2FARequest = z.infer; /** * Disable 2FA request schema * POST /auth/2fa/disable */ export const disable2FARequestSchema = z.object({ password: passwordSchema, }); export type Disable2FARequest = z.infer; /** * Note: POST /auth/2fa/setup does not require a request body * It generates a secret and QR code for the authenticated user */ /** * Create user request schema */ export const createUserRequestSchema = z.object({ username: usernameSchema, email: emailSchema, password: passwordSchema, }); export type CreateUserRequest = z.infer; /** * Update user request schema */ export const updateUserRequestSchema = z.object({ username: usernameSchema.optional(), email: emailSchema.optional(), password: passwordSchema.optional(), }); export type UpdateUserRequest = z.infer; /** * Update profile request schema */ export const updateProfileRequestSchema = z.object({ first_name: z.string().max(100).optional(), last_name: z.string().max(100).optional(), username: usernameSchema.optional(), bio: z.string().max(500).optional(), location: z.string().max(100).optional(), birthdate: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format. Use YYYY-MM-DD') .optional(), gender: z.enum(['Male', 'Female', 'Other', 'Prefer not to say']).optional(), }); export type UpdateProfileRequest = z.infer; /** * Send message request schema */ export const sendMessageRequestSchema = z.object({ conversation_id: uuidSchema, content: z.string().min(1, 'Message content is required'), message_type: z.enum(['text', 'image', 'audio', 'file']).optional(), attachment_url: z.string().url().optional(), }); export type SendMessageRequest = z.infer; /** * Update message request schema */ export const updateMessageRequestSchema = z.object({ content: z.string().min(1, 'Message content is required').optional(), }); export type UpdateMessageRequest = z.infer; /** * Create conversation request schema */ export const createConversationRequestSchema = z.object({ name: z.string().min(1, 'Conversation name is required'), type: z.enum(['direct', 'group']), participant_ids: z .array(uuidSchema) .min(1, 'At least one participant is required'), }); export type CreateConversationRequest = z.infer< typeof createConversationRequestSchema >; /** * Update conversation request schema */ export const updateConversationRequestSchema = z.object({ name: z.string().min(1, 'Conversation name is required').optional(), }); export type UpdateConversationRequest = z.infer< typeof updateConversationRequestSchema >; /** * Batch Operations Request Schemas * MEDIUM PRIORITY: Core functionality */ /** * Batch delete tracks request schema * POST /tracks/batch/delete */ export const batchDeleteTracksRequestSchema = z.object({ track_ids: z.array(uuidSchema).min(1, 'At least one track ID is required'), }); export type BatchDeleteTracksRequest = z.infer< typeof batchDeleteTracksRequestSchema >; /** * Upload Chunking Request Schemas * MEDIUM PRIORITY: Core functionality for large file uploads */ /** * Initiate chunked upload request schema * POST /tracks/initiate */ export const initiateChunkedUploadRequestSchema = z.object({ filename: z.string().min(1, 'Filename is required'), total_chunks: z.number().int().min(1, 'Total chunks must be at least 1'), total_size: z.number().int().min(1, 'Total size must be at least 1'), }); export type InitiateChunkedUploadRequest = z.infer< typeof initiateChunkedUploadRequestSchema >; /** * Complete chunked upload request schema * POST /tracks/complete */ export const completeChunkedUploadRequestSchema = z.object({ upload_id: z.string().min(1, 'Upload ID is required'), }); export type CompleteChunkedUploadRequest = z.infer< typeof completeChunkedUploadRequestSchema >; /** * Upload chunk request schema * POST /tracks/chunk * Note: This endpoint uses formData (multipart/form-data), not JSON * The schema validates the form fields, but chunk file is validated separately */ export const uploadChunkRequestSchema = z.object({ upload_id: z.string().min(1, 'Upload ID is required'), chunk_number: z.number().int().min(0, 'Chunk number must be non-negative'), total_chunks: z.number().int().min(1, 'Total chunks must be at least 1'), total_size: z.number().int().min(1, 'Total size must be at least 1'), filename: z.string().min(1, 'Filename is required'), // chunk file is sent as FormData file field, validated separately }); export type UploadChunkRequest = z.infer; /** * Analytics Request Schemas * MEDIUM PRIORITY: Analytics tracking */ /** * Record analytics event request schema * POST /analytics/events */ export const recordEventRequestSchema = z.object({ event_name: z .string() .min(1, 'Event name is required') .max(100, 'Event name must be at most 100 characters'), payload: z.record(z.any()).optional(), // Additional properties allowed }); export type RecordEventRequest = z.infer; /** * Webhook Request Schemas * MEDIUM PRIORITY: Webhook management */ /** * Create webhook request schema * POST /webhooks * Note: Schema inferred from endpoint usage - webhook creation typically requires url, events, secret */ export const createWebhookRequestSchema = z.object({ url: z.string().url('Invalid webhook URL'), events: z.array(z.string()).min(1, 'At least one event is required'), secret: z.string().min(1, 'Secret is required').optional(), }); export type CreateWebhookRequest = z.infer; /** * Frontend Logging Request Schemas * LOW PRIORITY: Development/debugging feature */ /** * Frontend log request schema * POST /api/v1/logs/frontend */ export const frontendLogRequestSchema = z.object({ level: z.string().optional(), message: z.string().optional(), context: z.record(z.any()).optional(), timestamp: z.string().optional(), data: z.any().optional(), }); export type FrontendLogRequest = z.infer; /** * Email Verification Request Schemas * LOW PRIORITY: User account management */ /** * Resend verification email request schema * POST /auth/resend-verification */ export const resendVerificationRequestSchema = z.object({ email: emailSchema, }); export type ResendVerificationRequest = z.infer< typeof resendVerificationRequestSchema >; /** * Upload track request schema * Note: File objects cannot be validated with Zod, so we validate metadata only */ export const uploadTrackRequestSchema = z.object({ title: z.string().min(1, 'Track title is required'), artist_id: uuidSchema, album_id: uuidSchema.optional(), genre: z.string().min(1, 'Genre is required'), // file and cover_art are File objects, validated separately }); export type UploadTrackRequest = z.infer; /** * Update track request schema */ export const updateTrackRequestSchema = z.object({ title: z.string().min(1, 'Track title is required').optional(), artist_id: uuidSchema.optional(), album_id: uuidSchema.optional(), genre: z.string().min(1, 'Genre is required').optional(), }); export type UpdateTrackRequest = z.infer; /** * Pagination params schema */ export const paginationParamsSchema = z.object({ page: z.number().int().positive().optional(), limit: z.number().int().positive().max(100).optional(), cursor: z.string().optional(), }); export type PaginationParams = z.infer; /** * List users request schema */ export const listUsersRequestSchema = paginationParamsSchema.extend({ query: z.string().optional(), }); export type ListUsersRequest = z.infer; /** * List messages request schema */ export const listMessagesRequestSchema = paginationParamsSchema.extend({ conversation_id: uuidSchema, }); export type ListMessagesRequest = z.infer; /** * List conversations request schema */ export const listConversationsRequestSchema = paginationParamsSchema.extend({ query: z.string().optional(), }); export type ListConversationsRequest = z.infer< typeof listConversationsRequestSchema >; /** * List tracks request schema */ export const listTracksRequestSchema = paginationParamsSchema.extend({ artist: z.string().optional(), genre: z.string().optional(), }); export type ListTracksRequest = z.infer; /** * Search tracks request schema */ export const searchTracksRequestSchema = paginationParamsSchema.extend({ query: z.string().min(1, 'Search query is required'), }); export type SearchTracksRequest = z.infer; /** * Upload file request schema * Note: File object cannot be validated with Zod */ export const uploadFileRequestSchema = z.object({ type: z.enum(['image', 'audio', 'document']), // file is a File object, validated separately }); export type UploadFileRequest = z.infer; /** * Validate API request data * * @param schema - Zod schema to validate against * @param data - Data to validate * @returns Validated data * @throws ZodError if validation fails */ export function validateApiRequest( schema: z.ZodSchema, data: unknown, ): T { return schema.parse(data); } /** * Safe validate API request (returns result instead of throwing) * * @param schema - Zod schema to validate against * @param data - Data to validate * @returns Validation result */ export function safeValidateApiRequest( schema: z.ZodSchema, data: unknown, ): { success: boolean; data?: T; error?: z.ZodError; } { try { const validated = validateApiRequest(schema, data); return { success: true, data: validated }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, error }; } throw error; } } /** * Validate request with custom error handling * * @param schema - Zod schema to validate against * @param data - Data to validate * @param onError - Optional error handler * @returns Validated data or throws */ export function validateApiRequestWithError( schema: z.ZodSchema, data: unknown, onError?: (error: z.ZodError) => void, ): T { try { return schema.parse(data); } catch (error) { if (error instanceof z.ZodError && onError) { onError(error); } throw error; } }