first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:48:15 +03:00
commit e6f3268c28
50 changed files with 4930 additions and 0 deletions

800
README.md Normal file
View File

@@ -0,0 +1,800 @@
# 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