'use server' import path from 'node:path' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { getAccessToken, refreshAccessToken } from '@/app/auth/actions' const API_BASE = process.env.API_BASE_URL ?? 'http://localhost:8080' type ApiError = Record export type Profile = { user_id: number first_name: string last_name: string avatar_url: string } export type ProfileFormState = { success?: boolean error?: string profile?: Profile } async function getToken(): Promise { const token = await getAccessToken() if (token) return token const fresh = await refreshAccessToken() if (fresh) return fresh redirect('/auth/login?from=/admin/profile') } function extractError(data: ApiError, fallback: string): string { if (typeof data?.error === 'string' && data.error) return data.error if (typeof data?.message === 'string' && data.message) return data.message if (typeof data?.detail === 'string' && data.detail) return data.detail return fallback } /** API'den gelen göreli yolu (/uploads/...) img ve multipart `avatar_url` için tam adrese çevirir. */ function absoluteProfileAvatarUrl(avatar_url: string): string { const v = avatar_url.trim() if (!v) return '' if (/^https?:\/\//i.test(v)) return v const pathPart = v.startsWith('/') ? v : `/${v}` const base = API_BASE.replace(/\/$/, '') return `${base}${pathPart}` } function extensionFromFilename(name: string): string | null { const ext = path.extname(name).replace('.', '').toLowerCase() if (['jpg', 'jpeg', 'png', 'webp', 'gif'].includes(ext)) return ext === 'jpeg' ? 'jpg' : ext return null } function extensionFromMime(mimeType: string): string | null { if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') return 'jpg' if (mimeType === 'image/png') return 'png' if (mimeType === 'image/webp') return 'webp' if (mimeType === 'image/gif') return 'gif' return null } function validateAvatarFile(file: File): void { const looksLikeImage = file.type.startsWith('image/') || file.type === '' || file.type === 'application/octet-stream' if (!looksLikeImage) { throw new Error('Lütfen geçerli bir görsel seçin') } if (file.size > 5 * 1024 * 1024) { throw new Error('Avatar en fazla 5MB olabilir') } const ext = extensionFromMime(file.type) ?? extensionFromFilename(file.name) if (!ext) { throw new Error('Desteklenmeyen dosya türü (jpg, png, webp, gif)') } } export async function getProfile(): Promise { let token = await getToken() const url = `${API_BASE}/api/v1/me/profile` const doFetch = () => fetch(url, { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store', }) let res: Response try { res = await doFetch() } catch (error) { console.error('[getProfile] fetch failed', error) throw new Error('API sunucusuna bağlanılamadı. Backend çalışıyor mu kontrol edin.') } if (res.status === 401) { const fresh = await refreshAccessToken() if (!fresh) throw new Error('Oturum süresi doldu') token = fresh try { res = await doFetch() } catch (error) { console.error('[getProfile] fetch failed after refresh', error) throw new Error('API sunucusuna bağlanılamadı. Backend çalışıyor mu kontrol edin.') } } if (!res.ok) { const data = (await res.json().catch(() => ({}))) as ApiError throw new Error(extractError(data, 'Profil bilgisi alınamadı')) } const raw = (await res.json()) as Profile return { ...raw, avatar_url: absoluteProfileAvatarUrl(raw.avatar_url ?? ''), } } export async function updateProfile( _prev: ProfileFormState, formData: FormData ): Promise { try { const token = await getToken() const avatarFile = formData.get('avatar') const avatarUrl = String(formData.get('avatar_url') ?? '') const first_name = String(formData.get('first_name') ?? '') const last_name = String(formData.get('last_name') ?? '') const hasNewFile = avatarFile instanceof File && avatarFile.size > 0 if (hasNewFile && avatarFile instanceof File) { validateAvatarFile(avatarFile) } const outbound = new FormData() outbound.append('first_name', first_name) outbound.append('last_name', last_name) if (hasNewFile && avatarFile instanceof File) { const filename = avatarFile.name?.trim() || 'avatar.jpg' outbound.append('avatar', avatarFile, filename) } else { const backendAvatarUrl = absoluteProfileAvatarUrl(avatarUrl.trim()) if (backendAvatarUrl) outbound.append('avatar_url', backendAvatarUrl) } console.log('[profile:avatar] PUT /api/v1/me/profile (multipart)', { API_BASE, hasFile: hasNewFile, has_avatar_url_field: !hasNewFile && Boolean(absoluteProfileAvatarUrl(avatarUrl.trim())), first_name, last_name, }) const res = await fetch(`${API_BASE}/api/v1/me/profile`, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, }, body: outbound, }) console.log('[profile:avatar] backend yanıt', { status: res.status, ok: res.ok, }) if (!res.ok) { const data = (await res.json().catch(() => ({}))) as ApiError console.log('[profile:avatar] backend hata gövdesi', data) return { error: extractError(data, 'Profil güncellenemedi') } } const raw = (await res.json()) as Profile const profile: Profile = { ...raw, avatar_url: absoluteProfileAvatarUrl(raw.avatar_url ?? ''), } console.log('[profile:avatar] başarı', { avatar_url: profile.avatar_url, }) revalidatePath('/admin/profile') return { success: true, profile } } catch (error) { console.error('[updateProfile]', error) return { error: 'Sunucu hatası' } } }