Files
goimgApi/README.md
Beyhan Oğur e6f3268c28 first commit
2026-04-26 21:48:15 +03:00

801 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Go Image Manipulation API
Fiber v3, GORM (MySQL) ve libvips/bimg tabanlı, JWT kimlik doğrulamalı resim yükleme ve işleme REST API.
---
## İçindekiler
- [Özellikler](#özellikler)
- [Teknoloji Yığını](#teknoloji-yığını)
- [Proje Yapısı](#proje-yapısı)
- [Gereksinimler](#gereksinimler)
- [Kurulum ve Çalıştırma](#kurulum-ve-çalıştırma)
- [Ortam Değişkenleri](#ortam-değişkenleri)
- [API Referansı](#api-referansı)
- [Auth](#auth)
- [Images](#images)
- [Admin](#admin)
- [Static Files](#static-files)
- [Kimlik Doğrulama](#kimlik-doğrulama)
- [Veri Modelleri](#veri-modelleri)
- [Resim İşleme](#resim-işleme)
- [Swagger UI](#swagger-ui)
- [Testler](#testler)
- [Deployment](#deployment)
---
## Özellikler
| Özellik | Detay |
|---------|-------|
| 🔐 JWT Auth | Access token (15 dk) + Refresh token (7 gün), `role` claim içerir |
| 🖼️ Resim Yükleme | UUID tabanlı isim, otomatik format dönüşümü |
| ⚙️ Resim İşleme | Yeniden boyutlandırma, kırpma, cover modu, kalite, format değişimi |
| 🗂️ Resim Listeleme | Sayfalı listeleme, tek resim detayı |
| 🌐 Statik Erişim | Yüklenen resimlere doğrudan URL ile erişim |
| 🛡️ Admin Panel | Kullanıcıya API token atama, tüm resimleri listeleme |
| 🗄️ MySQL + GORM | Otomatik migrasyon, soft delete |
| ⚡ Redis | Bağlantı havuzu hazır |
| 📄 Swagger UI | `/swagger` adresinde interaktif API belgesi |
---
## Teknoloji Yığını
| Katman | Kütüphane/Araç |
|--------|---------------|
| HTTP Framework | [Fiber v3](https://github.com/gofiber/fiber) |
| ORM | [GORM](https://gorm.io) + MySQL driver |
| Resim İşleme | [bimg](https://github.com/h2non/bimg) (libvips bağlayıcısı) |
| JWT | [golang-jwt/jwt v5](https://github.com/golang-jwt/jwt) |
| Redis | [go-redis/redis v9](https://github.com/redis/go-redis) |
| Parola Hashing | bcrypt |
| UUID | google/uuid |
| Swagger | swaggo/swag |
| Env Yönetimi | godotenv |
---
## Proje Yapısı
```
goimgApi/
├── main.go # Uygulama başlangıç noktası
├── go.mod / go.sum
├── .env # Ortam değişkenleri (Git'e eklenmez)
├── accounts/ # Kimlik doğrulama & kullanıcı yönetimi
│ ├── models.go # User struct (GORM modeli)
│ ├── handlers.go # Register, Login, Refresh, Middleware, Admin handlers
│ ├── jwt.go # Token üretimi & parse, rol normalizasyonu
│ ├── accounts_test.go # JWT & rol testleri
│ ├── jwt_test.go # Token claim testleri
│ └── models/ # İkincil model paketi (sosyal hesap, profil, refresh token)
│ ├── account.go
│ ├── token.go
│ └── models_test.go
├── images/ # Resim yükleme & işleme
│ ├── models.go # Image struct (GORM modeli)
│ ├── handlers.go # Upload, ListImages, GetImage, AdminListImages, Process
│ ├── processor.go # bimg işleme motoru
│ └── handlers_test.go # Path yardımcı & sayfalama testleri
├── configs/ # Bağlantı yapılandırmaları
│ ├── db.go # MySQL bağlantısı, CORS/rate-limit seed yardımcıları
│ ├── redis.go # Redis bağlantısı
│ └── configs_test.go # Yardımcı fonksiyon testleri
├── router/
│ ├── routers.go # Tüm route tanımları, statik dosya sunumu
│ └── routers_test.go # Swagger & statik route testleri
├── docs/ # Swaggo tarafından üretilen Swagger dosyaları
│ ├── docs.go
│ ├── swagger.json
│ ├── swagger.yaml
│ └── docs_test.go
├── uploads/ # Yüklenen resimlerin depolandığı dizin
└── tmp/ # Geliştirme sırasında kullanılan geçici scriptler
```
---
## Gereksinimler
- **Go 1.26+**
- **MySQL 8.0+**
- **Redis 6+**
- **libvips 8.8+** (bimg için)
### libvips Kurulumu
```bash
# Ubuntu / Debian
sudo apt-get install -y libvips-dev
# macOS
brew install vips
# Arch Linux
sudo pacman -S libvips
```
---
## Kurulum ve Çalıştırma
### 1. Depoyu klonlayın
```bash
git clone https://github.com/kullanici/goimgApi.git
cd goimgApi
```
### 2. Bağımlılıkları yükleyin
```bash
go mod download
```
### 3. Ortam değişkenlerini ayarlayın
```bash
cp .env.example .env
# .env dosyasını düzenleyin
```
### 4. Swagger spec'ini üretin
```bash
# swag CLI kurulumu (tek seferlik)
go install github.com/swaggo/swag/cmd/swag@latest
# docs/ klasörünü yeniden üret
swag init --parseDependency --parseInternal
```
> ⚠️ **Önemli:** Yeni bir endpoint ekledikten veya mevcut bir endpoint'in godoc yorumunu değiştirdikten sonra bu komutu **mutlaka** tekrar çalıştırın; aksi hâlde Swagger UI güncellenmiş endpoint'leri göstermez.
### 5. Uygulamayı başlatın
```bash
go run .
```
Sunucu `http://localhost:8080` adresinde başlar.
### Geliştirme modunda (hot-reload)
```bash
# air kurulumu (opsiyonel)
go install github.com/air-verse/air@latest
air
```
---
## Ortam Değişkenleri
Proje kök dizininde bir `.env` dosyası oluşturun:
```dotenv
# ─── Veritabanı (MySQL) ──────────────────────────────────────────────────────
DB_HOST=localhost
DB_PORT=3306
DB_USER=go_imgapi
DB_PASSWORD=your_password
DB_NAME=go_imgapi
# ─── Redis ───────────────────────────────────────────────────────────────────
REDIS_URL=redis://default:your_password@localhost:6379/0
# veya ayrı ayrı:
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_PASSWORD=
# ─── JWT ─────────────────────────────────────────────────────────────────────
JWT_SECRET=en-az-32-karakter-uzun-gizli-anahtar
JWT_REFRESH_SECRET=farkli-bir-gizli-anahtar-refresh-icin
# ─── Sunucu ──────────────────────────────────────────────────────────────────
PORT=8080
# ─── CORS Bootstrap (opsiyonel) ──────────────────────────────────────────────
CORS_BOOTSTRAP_WHITELIST_ORIGINS=http://localhost:3000,https://yourfrontend.com
# ─── Rate Limit Bootstrap (opsiyonel) ────────────────────────────────────────
RL_BOOTSTRAP_LOGIN_MAX_REQUESTS=10
RL_BOOTSTRAP_LOGIN_WINDOW_SECONDS=60
RL_BOOTSTRAP_API_MAX_REQUESTS=120
RL_BOOTSTRAP_API_WINDOW_SECONDS=60
```
> **Not:** `JWT_SECRET` ve `JWT_REFRESH_SECRET` birbirinden farklı ve en az 32 karakter olmalıdır.
---
## API Referansı
Base URL: `http://localhost:8080`
### Auth
#### `POST /auth/register` — Kayıt
Yeni kullanıcı oluşturur.
**İstek** (`multipart/form-data`)
| Alan | Tür | Zorunlu | Açıklama |
|------|-----|---------|----------|
| `email` | string | ✅ | Geçerli e-posta adresi |
| `password` | string | ✅ | En az 6 karakter |
**Başarılı Yanıt** `201 Created`
```json
{
"message": "User registered",
"user_id": 1
}
```
**Hata Yanıtları**
| Kod | Açıklama |
|-----|----------|
| `400` | Email boş / şifre 6 karakterden kısa / email zaten kullanımda |
---
#### `POST /auth/login` — Giriş
Kullanıcıyı doğrular ve JWT token çifti döner.
**İstek** (`multipart/form-data`)
| Alan | Tür | Zorunlu |
|------|-----|---------|
| `email` | string | ✅ |
| `password` | string | ✅ |
**Başarılı Yanıt** `200 OK`
```json
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"user": {
"id": 1,
"email": "kullanici@ornek.com",
"role": "admin"
}
}
```
> `role` değeri `"admin"` veya `"user"` olur.
**Hata Yanıtları**
| Kod | Açıklama |
|-----|----------|
| `401` | Geçersiz e-posta veya şifre |
---
#### `POST /auth/refresh` — Token Yenile
Geçerli bir refresh token ile yeni token çifti üretir.
Refresh sırasında kullanıcının güncel rolü DB'den yeniden okunur.
**İstek** (`multipart/form-data`)
| Alan | Tür | Zorunlu |
|------|-----|---------|
| `refresh_token` | string | ✅ |
**Başarılı Yanıt** `200 OK`
```json
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci..."
}
```
**Hata Yanıtları**
| Kod | Açıklama |
|-----|----------|
| `401` | Refresh token eksik, geçersiz veya süresi dolmuş |
| `401` | Token'a ait kullanıcı bulunamadı (silinmiş olabilir) |
---
### Images
Tüm `/images` endpoint'leri `Authorization: Bearer <access_token>` başlığı gerektirir.
#### `POST /images` — Resim Yükle
**İstek** (`multipart/form-data`)
| Alan | Tür | Zorunlu | Açıklama |
|------|-----|---------|----------|
| `image` | file | ✅ | Resim dosyası |
| `w` | int | | Hedef genişlik (px) |
| `h` | int | | Hedef yükseklik (px) |
| `q` | int | | Kalite 1-100 (varsayılan: 85) |
| `f` | string | | Format: `webp`, `avif`, `png`, `jpg` |
| `mode` | string | | `cover` — kırparak doldur |
**Başarılı Yanıt** `201 Created`
```json
{
"message": "Image uploaded successfully",
"image_id": 12,
"filename": "550e8400-e29b-41d4-a716-446655440000.webp",
"public_path": "/uploads/550e8400-e29b-41d4-a716-446655440000.webp",
"image_url": "http://localhost:8080/uploads/550e8400-e29b-41d4-a716-446655440000.webp"
}
```
> **`public_path` vs `image_url` farkı:**
> - `public_path` — domain bağımsız, DB'ye kaydedilen göreli yol (`/uploads/...`). Farklı ortamlarda (staging, prod, CDN) taşınabilir.
> - `image_url` — isteği yapan host'a göre dinamik üretilen tam URL. `X-Forwarded-Host` / `X-Forwarded-Proto` başlıkları varsa onları kullanır (proxy/CDN için).
>
> Frontend'de resmi göstermek için `image_url` değerini doğrudan kullanabilir ya da `API_BASE_URL + public_path` formülünü tercih edebilirsiniz.
---
#### `GET /images` — Resimleri Listele
Kimliği doğrulanmış kullanıcının resimlerini sayfalı döner.
**Query Parametreleri**
| Parametre | Varsayılan | Maks | Açıklama |
|-----------|-----------|------|----------|
| `page` | `1` | — | Sayfa numarası |
| `limit` | `20` | `100` | Sayfa başına kayıt |
**Başarılı Yanıt** `200 OK`
```json
{
"data": [
{
"id": 12,
"user_id": 1,
"filename": "550e8400....webp",
"public_path": "/uploads/550e8400....webp",
"image_url": "http://localhost:8080/uploads/550e8400....webp",
"mime_type": "image/jpeg",
"size_kb": 42,
"width": 800,
"height": 600,
"quality": 85,
"format": "webp",
"mode": "original",
"created_at": "2026-04-10T01:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
```
---
#### `GET /images/:id` — Tek Resim Detayı
Kimliği doğrulanmış kullanıcıya ait belirli bir resmin detayını döner.
**Başarılı Yanıt** `200 OK`
Listeleme endpoint'indeki tek kayıt yapısı ile aynı.
**Hata Yanıtları**
| Kod | Açıklama |
|-----|----------|
| `404` | Resim bulunamadı veya kullanıcıya ait değil |
---
#### `GET /images/:id/process` — Resmi İşle ve Döndür
API token ile korunur (JWT gerektirmez). Web sitelerinde `<img src="...">` ile doğrudan kullanım için tasarlanmıştır.
**Query Parametreleri**
| Parametre | Zorunlu | Açıklama |
|-----------|---------|----------|
| `token` | ✅ | Kullanıcıya ait API token |
| `w` | | Hedef genişlik (px) |
| `h` | | Hedef yükseklik (px) |
| `q` | | Kalite 1-100 |
| `f` | | Format: `webp`, `avif`, `png`, `jpg` |
| `mode` | | `cover` — kırparak doldur |
**Başarılı Yanıt** `200 OK` — İşlenmiş resim binary verisi (`image/webp`, `image/jpeg`, vb.)
**Örnek Kullanım**
```html
<!-- Orijinal resmi göster -->
<img src="http://localhost:8080/images/12/process?token=API_TOKEN">
<!-- 400×300 WebP olarak göster -->
<img src="http://localhost:8080/images/12/process?token=API_TOKEN&w=400&h=300&f=webp">
<!-- Cover kırpma ile 200×200 thumbnail -->
<img src="http://localhost:8080/images/12/process?token=API_TOKEN&w=200&h=200&mode=cover&f=webp">
```
**Hata Yanıtları**
| Kod | Açıklama |
|-----|----------|
| `401` | Token eksik, geçersiz veya süresi dolmuş |
| `404` | Resim bulunamadı |
| `500` | Dosya okunamadı veya işleme hatası |
---
### Admin
Tüm `/admin` endpoint'leri hem `Authorization: Bearer <access_token>` hem de admin yetkisi gerektirir.
#### `POST /admin/users/:id/api-token` — API Token Oluştur
Belirtilen kullanıcıya yeni bir API token atar.
**Path Parametreleri**
| Parametre | Tür | Açıklama |
|-----------|-----|----------|
| `id` | int | Hedef kullanıcı ID |
**İstek** (`multipart/form-data` veya query)
| Alan | Tür | Açıklama |
|------|-----|----------|
| `expires_in_days` | int | Kaç gün geçerli (0 = süresiz) |
**Başarılı Yanıt** `200 OK`
```json
{
"message": "API token created successfully",
"api_token": "550e8400-e29b-41d4-a716-446655440000",
"expires_at": "2026-05-10T01:00:00Z"
}
```
---
#### `GET /admin/images` — Tüm Resimleri Listele
Tüm kullanıcılara ait resimleri sayfalı döner.
**Query Parametreleri**
| Parametre | Açıklama |
|-----------|----------|
| `page` | Sayfa numarası (varsayılan: 1) |
| `limit` | Sayfa başına kayıt (varsayılan: 20, maks: 100) |
| `user_id` | Belirli bir kullanıcıya göre filtrele |
**Başarılı Yanıt** `200 OK`
```json
{
"data": [
{
"id": 12,
"user_id": 3,
"filename": "550e8400-e29b-41d4-a716-446655440000.webp",
"public_path": "/uploads/550e8400-e29b-41d4-a716-446655440000.webp",
"image_url": "http://localhost:8080/uploads/550e8400-e29b-41d4-a716-446655440000.webp",
"mime_type": "image/jpeg",
"size_kb": 42,
"width": 800,
"height": 600,
"quality": 85,
"format": "webp",
"mode": "original",
"created_at": "2026-04-10T01:00:00Z"
}
],
"total": 50,
"page": 1,
"limit": 20
}
```
---
### Static Files
#### `GET /uploads/:filename` — Yüklenen Resme Eriş
Yüklenen resimlere doğrudan URL ile erişim sağlar. Path traversal saldırılarına karşı korumalıdır.
```
GET /uploads/550e8400-e29b-41d4-a716-446655440000.webp
```
---
## Kimlik Doğrulama
### JWT Token Yapısı
```json
{
"user_id": 1,
"role": "admin",
"exp": 1775771479,
"iat": 1775770579
}
```
| Claim | Açıklama |
|-------|----------|
| `user_id` | Kullanıcı birincil anahtarı |
| `role` | `"admin"` veya `"user"` |
| `exp` | Token son geçerlilik zamanı (Unix timestamp) |
| `iat` | Token üretim zamanı |
### Token Ömürleri
| Token | Ömür |
|-------|------|
| Access Token | **15 dakika** |
| Refresh Token | **7 gün** |
| API Token | Admin tarafından belirlenir (sınırsız veya belirli gün) |
### İstek Başlığı
```http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Token Yenileme Akışı
```
POST /auth/login
→ access_token (15 dk) + refresh_token (7 gün)
access_token süresi dolunca:
POST /auth/refresh body: refresh_token=...
→ yeni access_token + yeni refresh_token
```
---
## Veri Modelleri
### `User` (accounts paketi)
| Alan | Tür | Açıklama |
|------|-----|----------|
| `id` | uint | Birincil anahtar |
| `email` | string | Benzersiz, zorunlu |
| `password_hash` | string | bcrypt (JSON'da gizli) |
| `is_admin` | bool | Admin yetkisi (varsayılan: false) |
| `api_token` | string | Resim işleme için API token |
| `api_token_expires_at` | *time | Token bitiş zamanı (null = süresiz) |
| `created_at` | time | |
| `updated_at` | time | |
| `deleted_at` | gorm.DeletedAt | Soft delete |
### `Image` (images paketi)
| Alan | Tür | Açıklama |
|------|-----|----------|
| `id` | uint | Birincil anahtar |
| `user_id` | uint | Sahibi (indexed) |
| `filename` | string | UUID tabanlı dosya adı |
| `public_path` | string | `/uploads/<uuid>.<ext>` |
| `mime_type` | string | Orijinal MIME türü |
| `size` | int64 | KB cinsinden boyut |
| `width` | int | Piksel genişlik |
| `height` | int | Piksel yükseklik |
| `quality` | int | 1100 arası kalite değeri |
| `format` | string | bimg format adı |
| `mode` | string | `original` veya `cover` |
| `created_at` | time | |
---
## Resim İşleme
Resimler [libvips](https://libvips.github.io/libvips/) ile işlenir. Desteklenen işlemler:
| İşlem | Parametre | Örnek |
|-------|-----------|-------|
| Genişlik ayarla | `w=400` | 400px genişliğe küçült |
| Yükseklik ayarla | `h=300` | 300px yüksekliğe küçült |
| Kalite ayarla | `q=75` | %75 kalite |
| Format dönüştür | `f=webp` | WebP formatına çevir |
| Cover kırpma | `mode=cover` | Oranı koruyarak kırp ve doldur |
### Desteklenen Formatlar
| Format | Parametre Değeri | MIME Türü |
|--------|-----------------|-----------|
| JPEG | `jpg` veya `jpeg` | `image/jpeg` |
| PNG | `png` | `image/png` |
| WebP | `webp` | `image/webp` |
| AVIF | `avif` | `image/avif` |
### Yükleme Sırasında İşleme
Upload endpoint'ine `w`, `h`, `f`, `q` parametreleri verilirse resim **diske yazılmadan önce** işlenir:
```bash
curl -X POST http://localhost:8080/images \
-H "Authorization: Bearer TOKEN" \
-F "image=@foto.jpg" \
-F "w=800" \
-F "f=webp" \
-F "q=85"
```
---
## Swagger UI
Tarayıcıda aç:
```
http://localhost:8080/swagger
```
Swagger JSON spec:
```
http://localhost:8080/docs/swagger.json
```
Spec içindeki `host` ve `schemes` alanları **çalışma zamanında** dinamik olarak kaldırılır; böylece farklı ortamlarda (proxy, HTTPS, CDN) Swagger UI her zaman mevcut host'a istek atar.
Swagger'dan istek atmak için:
1. `/auth/login` endpoint'ini çalıştırın
2. Dönen `access_token`'ı kopyalayın
3. Sağ üstteki **Authorize** butonuna tıklayın
4. `Bearer <token>` formatında girin
---
## Testler
```bash
# Tüm paket testleri
go test ./...
# Detaylı çıktı
go test -v ./...
# Belirli paket
go test -v ./accounts
go test -v ./images
go test -v ./router
go test -v ./configs
go test -v ./docs
go test -v ./accounts/models
```
### Test Kapsamı
| Paket | Test Sayısı | Kapsam |
|-------|-------------|--------|
| `accounts` | 15 | JWT üretimi, parse, rol normalizasyonu, `roleFromUser`, User model |
| `accounts/models` | 6 | `IsEmailVerified`, JSON tag güvenliği, GORM tag, `RefreshToken` alanları |
| `configs` | 27 | `normalizeOrigin` (8), `parseOriginList` (4), `envIntOr`/`envInt64Or` (8), bootstrap (7) |
| `docs` | 10 | `SwaggerInfo` alanları, şablon içeriği, swaggo registry kaydı |
| `images` | 9 | Path yardımcıları, `buildImageURL` (forwarded header), sayfalama |
| `router` | 2 | Swagger JSON host/scheme kaldırma, statik dosya servisi |
> Veritabanı veya Redis bağlantısı gerektiren testler mevcut değildir; tüm testler izole ve bağımsız çalışır.
---
## Swagger Spec Güncelleme
Yeni endpoint eklendiğinde veya mevcut godoc yorumu değiştirildiğinde spec'i yeniden üretmek gerekir:
```bash
swag init --parseDependency --parseInternal
```
Bu komut şu dosyaları günceller:
```
docs/
├── docs.go ← Go kodu (swaggo runtime için)
├── swagger.json ← Swagger UI'ın okuduğu spec
└── swagger.yaml ← YAML formatı
```
---
## Deployment
### Docker ile
```dockerfile
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache vips-dev build-base
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o goimgApi .
FROM alpine:3.19
RUN apk add --no-cache vips
WORKDIR /app
COPY --from=builder /app/goimgApi .
COPY --from=builder /app/docs ./docs
RUN mkdir -p uploads
EXPOSE 8080
CMD ["./goimgApi"]
```
```bash
docker build -t goimgapi .
docker run -p 8080:8080 --env-file .env goimgapi
```
### Nginx Reverse Proxy Örneği
```nginx
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
client_max_body_size 50M;
}
}
```
> `X-Forwarded-Proto` ve `X-Forwarded-Host` başlıkları, upload response'unda dönen `image_url` değerinin doğru domain ile üretilmesi için gereklidir.
### Production için .env Kontrol Listesi
- [ ] `JWT_SECRET` — en az 64 karakter, rastgele üretilmiş
- [ ] `JWT_REFRESH_SECRET` — JWT_SECRET'tan farklı, en az 64 karakter
- [ ] `DB_PASSWORD` — güçlü, production şifresi
- [ ] `CORS_BOOTSTRAP_WHITELIST_ORIGINS` — yalnızca gerçek frontend domain'leri
- [ ] `uploads/` dizini uygulama tarafından yazılabilir (`chmod 755`)
---
## Katkı
1. Fork'layın
2. Feature branch oluşturun: `git checkout -b feature/ozellik-adi`
3. Testler ekleyin ve geçtiğinden emin olun: `go test ./...`
4. Pull request açın
---
## Lisans
MIT