diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts new file mode 100644 index 00000000..89899725 --- /dev/null +++ b/src/app/api/user/profile/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import { z } from 'zod'; +import { withRateLimit } from '@/lib/ratelimit'; +import { edgeLog } from '@/../infra/edge-config'; + +export const runtime = 'edge'; + +const profileBodySchema = z.object({ + firstName: z.string().min(1, 'First name is required').max(100), + lastName: z.string().min(1, 'Last name is required').max(100), + email: z.string().email('Invalid email address'), + bio: z.string().max(500, 'Bio must be at most 500 characters'), + location: z.string().max(200).optional(), + website: z.string().url('Invalid URL').max(500).optional().or(z.literal('')), + twitter: z.string().max(100).optional(), + github: z.string().max(100).optional(), + linkedin: z.string().max(100).optional(), +}); + +export async function PUT(request: Request) { + edgeLog('info', '/api/user/profile', 'PUT request received'); + const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE'); + if (rateLimitResponse) return rateLimitResponse; + + try { + const json = await request.json(); + const parsed = profileBodySchema.safeParse(json); + + if (!parsed.success) { + const fieldErrors = parsed.error.errors.map((e) => ({ + field: e.path.join('.'), + message: e.message, + })); + return addHeaders( + NextResponse.json( + { success: false, message: 'Validation failed', errors: fieldErrors }, + { status: 400 }, + ), + ); + } + + return addHeaders( + NextResponse.json({ + success: true, + data: { + ...parsed.data, + updatedAt: new Date().toISOString(), + }, + }), + ); + } catch { + return addHeaders( + NextResponse.json({ success: false, message: 'Bad request body' }, { status: 400 }), + ); + } +} diff --git a/src/app/components/profile/ProfileEditForm.tsx b/src/app/components/profile/ProfileEditForm.tsx index ff1b2c47..34d7f67c 100644 --- a/src/app/components/profile/ProfileEditForm.tsx +++ b/src/app/components/profile/ProfileEditForm.tsx @@ -9,6 +9,7 @@ import ImageUploader from '@/components/shared/ImageUploader'; import PreferencesSection from '@/components/profile/PreferencesSection'; import { useProfileUpdate } from '@/app/hooks/useProfileUpdate'; import { FieldError } from '@/components/forms/FormError'; +import { ApiError } from '@/utils/error-handler'; const profileSchema = z.object({ firstName: z.string().min(2, 'First name must be at least 2 characters'), @@ -42,6 +43,7 @@ export default function ProfileEditForm() { const { register, handleSubmit, + setError, formState: { errors }, } = useForm({ resolver: zodResolver(profileSchema), @@ -53,11 +55,18 @@ export default function ProfileEditForm() { try { await updateProfile(data); toast.success('Profile updated successfully!'); - } catch { - toast.error('Failed to update profile. Please try again.'); + } catch (error: unknown) { + if (error instanceof ApiError && error.errors) { + for (const fe of error.errors) { + setError(fe.field as keyof ProfileFormData, { message: fe.message }); + } + toast.error('Please fix the highlighted fields.'); + } else { + toast.error('Failed to update profile. Please try again.'); + } } }, - [updateProfile], + [updateProfile, setError], ); const handleImageSelect = useCallback(() => {}, []); diff --git a/src/app/hooks/useProfileUpdate.ts b/src/app/hooks/useProfileUpdate.ts index 625d7793..91c60511 100644 --- a/src/app/hooks/useProfileUpdate.ts +++ b/src/app/hooks/useProfileUpdate.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; +import { apiClient } from '@/lib/api'; import { createLogger } from '@/lib/logging'; const logger = createLogger('useProfileUpdate'); @@ -14,30 +15,32 @@ interface ProfileData { linkedin?: string; } +interface ProfileUpdateResponse { + success: boolean; + data: ProfileData & { updatedAt: string }; + message?: string; + errors?: { field: string; message: string }[]; +} + export function useProfileUpdate() { const [isLoading, setIsLoading] = useState(false); const updateProfile = useCallback(async (data: ProfileData) => { setIsLoading(true); try { - // TODO: Replace with actual API call - await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulated API delay + const res = await apiClient.put('/api/user/profile', data); - // Simulate API response - const response = { - success: true, - data: { - ...data, - updatedAt: new Date().toISOString(), - }, - }; - - if (!response.success) { - throw new Error('Failed to update profile'); + if (!res.success) { + throw res; } - return response.data; - } catch (error) { + return res.data; + } catch (error: unknown) { + if (error && typeof error === 'object' && 'errors' in error) { + const apiErr = error as ProfileUpdateResponse; + logger.error('Validation error updating profile', { errors: apiErr.errors }); + throw apiErr; + } logger.error('Error updating profile', { error }); throw error; } finally {