639 lines
14 KiB
Markdown
639 lines
14 KiB
Markdown
# 📤 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):**
|
||
```json
|
||
{
|
||
"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:**
|
||
```bash
|
||
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):**
|
||
```json
|
||
{
|
||
"message": "Avatar deleted successfully",
|
||
"user": {
|
||
"id": "uuid",
|
||
"username": "john_doe",
|
||
"email": "john@example.com",
|
||
"avatar": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
**cURL Örneği:**
|
||
```bash
|
||
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):**
|
||
```json
|
||
{
|
||
"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:**
|
||
```bash
|
||
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:**
|
||
```html
|
||
<img src="http://localhost:8080/uploads/avatars/user-uuid_1234567890.jpg" alt="Avatar">
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 Test Örnekleri
|
||
|
||
### Test 1: Avatar Yükleme (cURL)
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:8080/v1/user/avatar \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
### Test 3: Admin Avatar Upload
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```html
|
||
<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
|
||
|
||
```jsx
|
||
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
|
||
|
||
```vue
|
||
<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
|
||
|
||
1. ✅ **Dosya Boyutu:** Maksimum 5MB
|
||
2. ✅ **Dosya Formatı:** Sadece jpg, jpeg, png, gif, webp
|
||
3. ✅ **Authentication:** Bearer token zorunlu
|
||
4. ✅ **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:
|
||
|
||
```go
|
||
r.Static("/uploads", "./uploads")
|
||
```
|
||
|
||
Avatar URL'si:
|
||
```
|
||
http://localhost:8080/uploads/avatars/filename.jpg
|
||
```
|
||
|
||
### 2. Dosya Boyutu Limiti
|
||
|
||
Frontend'de validasyon yapmayı unutmayın:
|
||
|
||
```javascript
|
||
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ı
|
||
|
||
1. Login → Token al
|
||
2. POST `/v1/user/avatar` (multipart/form-data ile dosya gönder)
|
||
3. Response'da avatar URL gelir
|
||
4. Avatar'ı görüntüle: `GET /uploads/avatars/{filename}`
|
||
5. İsterseniz DELETE ile silin
|
||
|
||
### Admin
|
||
|
||
1. Admin login → Token al
|
||
2. POST `/v1/admin/users/{user_id}/avatar` (herhangi bir kullanıcı için)
|
||
3. 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! 🎉**
|