196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
'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<string, unknown>
|
||
|
||
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<string> {
|
||
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<Profile> {
|
||
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<ProfileFormState> {
|
||
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ı' }
|
||
}
|
||
}
|