14 KiB
14 KiB
📤 Avatar Upload API - Multipart Form Data
✨ Özellikler
Avatar artık multipart/form-data ile dosya upload olarak gönderilir (JSON değil).
✅ Desteklenen Özellikler
- 📁 Dosya upload (multipart/form-data)
- 🖼️ Format kontrolü (jpg, jpeg, png, gif, webp)
- 📏 Boyut kontrolü (max 5MB)
- 🗑️ Eski avatar otomatik silme
- 👤 Kullanıcı kendi avatar'ını yükleyebilir
- 👨💼 Admin herhangi bir kullanıcının avatar'ını yükleyebilir
- 🌐 Static file serving
📋 Endpoint'ler
1. Kullanıcı Kendi Avatar'ını Yükler
POST /v1/user/avatar
Content-Type: multipart/form-data
Authorization: Bearer {token}
Form Data:
avatar(file, required) - Avatar dosyası
Desteklenen Formatlar:
- JPG / JPEG
- PNG
- GIF
- WebP
Maksimum Boyut: 5MB
Response (200):
{
"message": "Avatar uploaded successfully",
"avatar_url": "/uploads/avatars/user-uuid_1234567890.jpg",
"user": {
"id": "uuid",
"username": "john_doe",
"email": "john@example.com",
"avatar": "/uploads/avatars/user-uuid_1234567890.jpg"
}
}
cURL Örneği:
curl -X POST http://localhost:8080/v1/user/avatar \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "avatar=@/path/to/image.jpg"
2. Kullanıcı Avatar'ını Siler
DELETE /v1/user/avatar
Authorization: Bearer {token}
Response (200):
{
"message": "Avatar deleted successfully",
"user": {
"id": "uuid",
"username": "john_doe",
"email": "john@example.com",
"avatar": ""
}
}
cURL Örneği:
curl -X DELETE http://localhost:8080/v1/user/avatar \
-H "Authorization: Bearer YOUR_TOKEN"
3. Admin Kullanıcı Avatar'ı Yükler
POST /v1/admin/users/{user_id}/avatar
Content-Type: multipart/form-data
Authorization: Bearer {admin_token}
Form Data:
avatar(file, required) - Avatar dosyası
Response (200):
{
"message": "Avatar uploaded successfully",
"avatar_url": "/uploads/avatars/user-uuid_1234567890.jpg",
"user": {
"id": "uuid",
"username": "john_doe",
"email": "john@example.com",
"avatar": "/uploads/avatars/user-uuid_1234567890.jpg"
}
}
cURL Örneği:
curl -X POST http://localhost:8080/v1/admin/users/USER_ID/avatar \
-H "Authorization: Bearer ADMIN_TOKEN" \
-F "avatar=@/path/to/image.jpg"
4. Avatar Görüntüleme (Static File)
GET /uploads/avatars/{filename}
Avatar dosyaları otomatik olarak static file server tarafından sunulur.
Örnek:
http://localhost:8080/uploads/avatars/user-uuid_1234567890.jpg
HTML'de Kullanım:
<img src="http://localhost:8080/uploads/avatars/user-uuid_1234567890.jpg" alt="Avatar">
🧪 Test Örnekleri
Test 1: Avatar Yükleme (cURL)
# 1. Login olun
curl -X POST http://localhost:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "Test123!"
}'
# Response'dan token alın
TOKEN="eyJhbGci..."
# 2. Avatar yükleyin
curl -X POST http://localhost:8080/v1/user/avatar \
-H "Authorization: Bearer $TOKEN" \
-F "avatar=@./my-photo.jpg"
# Response:
# {
# "message": "Avatar uploaded successfully",
# "avatar_url": "/uploads/avatars/uuid_1234567890.jpg"
# }
Test 2: Avatar Silme
curl -X DELETE http://localhost:8080/v1/user/avatar \
-H "Authorization: Bearer $TOKEN"
Test 3: Admin Avatar Upload
# Admin token ile
ADMIN_TOKEN="admin_token_here"
USER_ID="user-uuid-here"
curl -X POST http://localhost:8080/v1/admin/users/$USER_ID/avatar \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "avatar=@./profile-picture.jpg"
💻 Frontend Kullanımı
HTML Form
<form id="avatarForm">
<input type="file" id="avatarInput" accept="image/*" required>
<button type="submit">Upload Avatar</button>
</form>
<script>
document.getElementById('avatarForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('avatarInput');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file');
return;
}
const formData = new FormData();
formData.append('avatar', file);
const token = localStorage.getItem('access_token');
try {
const response = await fetch('http://localhost:8080/v1/user/avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const data = await response.json();
if (response.ok) {
console.log('Avatar uploaded:', data.avatar_url);
alert('Avatar uploaded successfully!');
// Avatar'ı göster
document.getElementById('userAvatar').src =
'http://localhost:8080' + data.avatar_url;
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed');
}
});
</script>
React Component
import { useState } from 'react';
function AvatarUpload() {
const [uploading, setUploading] = useState(false);
const [avatarUrl, setAvatarUrl] = useState('');
const handleUpload = async (e) => {
e.preventDefault();
const file = e.target.avatar.files[0];
if (!file) return;
// Validate file size
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
return;
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert('Only JPG, PNG, GIF, and WebP images are allowed');
return;
}
const formData = new FormData();
formData.append('avatar', file);
const token = localStorage.getItem('access_token');
setUploading(true);
try {
const response = await fetch('http://localhost:8080/v1/user/avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const data = await response.json();
if (response.ok) {
setAvatarUrl('http://localhost:8080' + data.avatar_url);
alert('Avatar uploaded successfully!');
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed');
} finally {
setUploading(false);
}
};
const handleDelete = async () => {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('http://localhost:8080/v1/user/avatar', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
setAvatarUrl('');
alert('Avatar deleted successfully!');
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Delete failed:', error);
}
};
return (
<div className="avatar-upload">
{avatarUrl && (
<div>
<img
src={avatarUrl}
alt="Avatar"
className="w-32 h-32 rounded-full object-cover"
/>
<button onClick={handleDelete} className="btn-danger">
Delete Avatar
</button>
</div>
)}
<form onSubmit={handleUpload}>
<input
type="file"
name="avatar"
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
disabled={uploading}
/>
<button type="submit" disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload Avatar'}
</button>
</form>
</div>
);
}
export default AvatarUpload;
Vue.js Component
<template>
<div class="avatar-upload">
<div v-if="avatarUrl">
<img :src="avatarUrl" alt="Avatar" class="avatar-preview" />
<button @click="deleteAvatar" class="btn-danger">Delete Avatar</button>
</div>
<form @submit.prevent="uploadAvatar">
<input
type="file"
ref="fileInput"
accept="image/*"
@change="handleFileSelect"
:disabled="uploading"
/>
<button type="submit" :disabled="uploading || !selectedFile">
{{ uploading ? 'Uploading...' : 'Upload Avatar' }}
</button>
</form>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script>
export default {
data() {
return {
avatarUrl: '',
selectedFile: null,
uploading: false,
error: ''
}
},
methods: {
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) {
this.error = 'File size must be less than 5MB';
this.$refs.fileInput.value = '';
return;
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
this.error = 'Only JPG, PNG, GIF, and WebP images are allowed';
this.$refs.fileInput.value = '';
return;
}
this.selectedFile = file;
this.error = '';
},
async uploadAvatar() {
if (!this.selectedFile) return;
const formData = new FormData();
formData.append('avatar', this.selectedFile);
const token = localStorage.getItem('access_token');
this.uploading = true;
this.error = '';
try {
const response = await fetch('http://localhost:8080/v1/user/avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const data = await response.json();
if (response.ok) {
this.avatarUrl = 'http://localhost:8080' + data.avatar_url;
this.$refs.fileInput.value = '';
this.selectedFile = null;
} else {
this.error = data.error || 'Upload failed';
}
} catch (error) {
this.error = 'Upload failed: ' + error.message;
} finally {
this.uploading = false;
}
},
async deleteAvatar() {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('http://localhost:8080/v1/user/avatar', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
this.avatarUrl = '';
} else {
const data = await response.json();
this.error = data.error || 'Delete failed';
}
} catch (error) {
this.error = 'Delete failed: ' + error.message;
}
}
}
}
</script>
<style scoped>
.avatar-preview {
width: 128px;
height: 128px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
</style>
📊 Dosya Sistemi
Klasör Yapısı
AuthCentral/
├── uploads/
│ └── avatars/
│ ├── uuid1_1234567890.jpg
│ ├── uuid2_1234567891.png
│ └── uuid3_1234567892.webp
├── api/
│ └── handlers/
│ └── avatar_handler.go
└── main.go
Avatar URL Formatı
Uploaded Files:
/uploads/avatars/{user_id}_{timestamp}.{ext}
Örnek:
/uploads/avatars/550e8400-e29b-41d4-a716-446655440000_1707012345.jpg
OAuth Avatar URLs (değişmez):
https://lh3.googleusercontent.com/a/...
https://avatars.githubusercontent.com/u/...
🔒 Güvenlik
Dosya Validasyonu
- ✅ Dosya Boyutu: Maksimum 5MB
- ✅ Dosya Formatı: Sadece jpg, jpeg, png, gif, webp
- ✅ Authentication: Bearer token zorunlu
- ✅ Authorization: Kullanıcı sadece kendi avatar'ını yükleyebilir (admin hariç)
Dosya İsimlendirme
- Unique filename:
{user_id}_{timestamp}.{ext} - Collision önleme: Timestamp kullanımı
- User ID ile ilişkilendirme
Eski Dosya Temizleme
- Yeni avatar yüklendiğinde eski dosya otomatik silinir
- Sadece
/uploads/ile başlayan dosyalar silinir (OAuth URL'leri korunur)
⚠️ Önemli Notlar
1. Static File Serving
Uploads klasörü /uploads route'u ile sunuluyor:
r.Static("/uploads", "./uploads")
Avatar URL'si:
http://localhost:8080/uploads/avatars/filename.jpg
2. Dosya Boyutu Limiti
Frontend'de validasyon yapmayı unutmayın:
if (file.size > 5 * 1024 * 1024) {
alert('File too large! Max 5MB');
return;
}
3. CORS
Frontend farklı origin'den çalışıyorsa, static files için de CORS gerekebilir.
4. Production Deployment
Production'da:
- Cloud storage kullanın (S3, Google Cloud Storage, etc.)
- CDN kullanın
- Image optimization yapın
- Thumbnail oluşturun
📋 Endpoint Özeti
| Method | Endpoint | Auth | Açıklama |
|---|---|---|---|
| POST | /v1/user/avatar |
✅ User | Kendi avatar'ını yükle |
| DELETE | /v1/user/avatar |
✅ User | Kendi avatar'ını sil |
| POST | /v1/admin/users/:id/avatar |
✅ Admin | Kullanıcı avatar'ı yükle |
| GET | /uploads/avatars/{filename} |
❌ | Avatar görüntüle |
🎯 Kullanım Akışı
Normal Kullanıcı
- Login → Token al
- POST
/v1/user/avatar(multipart/form-data ile dosya gönder) - Response'da avatar URL gelir
- Avatar'ı görüntüle:
GET /uploads/avatars/{filename} - İsterseniz DELETE ile silin
Admin
- Admin login → Token al
- POST
/v1/admin/users/{user_id}/avatar(herhangi bir kullanıcı için) - Kullanıcının avatar'ı güncellenir
✅ Başarı Kriterleri
- ✅ Multipart/form-data ile dosya upload
- ✅ 5MB boyut limiti
- ✅ Format validasyonu
- ✅ Eski avatar otomatik silme
- ✅ Static file serving
- ✅ User + Admin endpoint'leri
- ✅ Authentication & Authorization
Avatar upload sistemi tam çalışıyor! 🎉