Files
atahango/belgeler/AVATAR_UPLOAD_API.md
Beyhan Oğur bbbf76b184 first commit
2026-04-26 21:35:24 +03:00

14 KiB
Raw Permalink Blame History

📤 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

  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:

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 ı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! 🎉