veza/apps/web/src/schemas/apiRequestSchemas.ts

477 lines
12 KiB
TypeScript
Raw Normal View History

/**
* 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<typeof loginRequestSchema>;
/**
* 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<typeof registerRequestSchema>;
/**
* 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<typeof verify2FARequestSchema>;
/**
* Disable 2FA request schema
* POST /auth/2fa/disable
*/
export const disable2FARequestSchema = z.object({
password: passwordSchema,
});
export type Disable2FARequest = z.infer<typeof disable2FARequestSchema>;
/**
* 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<typeof createUserRequestSchema>;
/**
* Update user request schema
*/
export const updateUserRequestSchema = z.object({
username: usernameSchema.optional(),
email: emailSchema.optional(),
password: passwordSchema.optional(),
});
export type UpdateUserRequest = z.infer<typeof updateUserRequestSchema>;
/**
* 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<typeof updateProfileRequestSchema>;
/**
* 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<typeof sendMessageRequestSchema>;
/**
* Update message request schema
*/
export const updateMessageRequestSchema = z.object({
content: z.string().min(1, 'Message content is required').optional(),
});
export type UpdateMessageRequest = z.infer<typeof updateMessageRequestSchema>;
/**
* 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<typeof uploadChunkRequestSchema>;
/**
* 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<typeof recordEventRequestSchema>;
/**
* 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<typeof createWebhookRequestSchema>;
/**
* 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<typeof frontendLogRequestSchema>;
/**
* 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<typeof uploadTrackRequestSchema>;
/**
* 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<typeof updateTrackRequestSchema>;
/**
* 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<typeof paginationParamsSchema>;
/**
* List users request schema
*/
export const listUsersRequestSchema = paginationParamsSchema.extend({
query: z.string().optional(),
});
export type ListUsersRequest = z.infer<typeof listUsersRequestSchema>;
/**
* List messages request schema
*/
export const listMessagesRequestSchema = paginationParamsSchema.extend({
conversation_id: uuidSchema,
});
export type ListMessagesRequest = z.infer<typeof listMessagesRequestSchema>;
/**
* 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<typeof listTracksRequestSchema>;
/**
* Search tracks request schema
*/
export const searchTracksRequestSchema = paginationParamsSchema.extend({
query: z.string().min(1, 'Search query is required'),
});
export type SearchTracksRequest = z.infer<typeof searchTracksRequestSchema>;
/**
* 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<typeof uploadFileRequestSchema>;
/**
* 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<T>(
schema: z.ZodSchema<T>,
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<T>(
schema: z.ZodSchema<T>,
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<T>(
schema: z.ZodSchema<T>,
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;
}
}