first commit
This commit is contained in:
638
AVATAR_UPLOAD_API.md
Normal file
638
AVATAR_UPLOAD_API.md
Normal file
@@ -0,0 +1,638 @@
|
||||
# 📤 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! 🎉**
|
||||
Reference in New Issue
Block a user