first commit
22
.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
README.md
|
||||||
|
SETUP.md
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
*.log
|
||||||
|
drizzle
|
||||||
|
.env*
|
||||||
14
.env
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#DATABASE_URL=postgres://cloud:gg7678290@10.80.80.70/image_api
|
||||||
|
#DATABASE_URL=postgresql://cloud:gg7678290@10.80.80.70:5432/image_api?search_path=public
|
||||||
|
DATABASE_URL=postgresql://cloud:gg7678290@188.132.232.119:5455/image_apiv2?search_path=public
|
||||||
|
BETTER_AUTH_SECRET=dB89kiKf56igxrB783yb3UyQToQIPZ93cRKADyq1yQEJ8EU4JRw5GlxBmGvQMu8e
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
JWT_SECRET=dB89kiKf56igxrB783yb3UyQToQIPZ93cRKADyq1yQEJ8EU4JRw5GlxBmGvQMu8e
|
||||||
|
|
||||||
|
# https://4f3dc7a1aa54f4ba52803e952d6cf6be.r2.cloudflarestorage.com/image-api
|
||||||
|
|
||||||
|
|
||||||
|
# local devop
|
||||||
|
# https://pub-79ea55a4c5a943c48147a7f1460049f2.r2.dev
|
||||||
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/image_api
|
||||||
|
|
||||||
|
# Better Auth
|
||||||
|
BETTER_AUTH_SECRET=your-secret-key-here-min-32-characters
|
||||||
|
BETTER_AUTH_URL=https://image.beyhano.com.tr
|
||||||
|
|
||||||
|
# App Settings
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# App URLs (production)
|
||||||
|
NEXT_PUBLIC_APP_URL=https://image.beyhano.com.tr
|
||||||
|
APP_URL=https://image.beyhano.com.tr
|
||||||
|
|
||||||
|
# JWT Secret
|
||||||
|
JWT_SECRET=your-jwt-secret-key-min-32-characters-long
|
||||||
14
.env.local
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#DATABASE_URL=postgres://cloud:gg7678290@10.80.80.70/image_api
|
||||||
|
#DATABASE_URL=postgresql://cloud:gg7678290@10.80.80.70:5432/image_api?search_path=public
|
||||||
|
DATABASE_URL=postgresql://cloud:gg7678290@188.132.232.119:5455/image_apiv2?search_path=public
|
||||||
|
BETTER_AUTH_SECRET=dB89kiKf56igxrB783yb3UyQToQIPZ93cRKADyq1yQEJ8EU4JRw5GlxBmGvQMu8e
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
JWT_SECRET=dB89kiKf56igxrB783yb3UyQToQIPZ93cRKADyq1yQEJ8EU4JRw5GlxBmGvQMu8e
|
||||||
|
|
||||||
|
# https://4f3dc7a1aa54f4ba52803e952d6cf6be.r2.cloudflarestorage.com/image-api
|
||||||
|
|
||||||
|
|
||||||
|
# local devop
|
||||||
|
# https://pub-79ea55a4c5a943c48147a7f1460049f2.r2.dev
|
||||||
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
313
ADMIN_PANEL.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# Admin Panel & Role-Based Access Control (RBAC)
|
||||||
|
|
||||||
|
## Genel Bakış
|
||||||
|
|
||||||
|
Bu proje artık kapsamlı bir rol tabanlı erişim kontrol (RBAC) sistemine sahiptir. Kullanıcılar rollere atanabilir ve bu roller belirli izinler sağlar.
|
||||||
|
|
||||||
|
## Roller
|
||||||
|
|
||||||
|
### 1. **User** (Varsayılan)
|
||||||
|
- Sadece kendi resimlerini yükleyebilir
|
||||||
|
- Sadece kendi resimlerini görebilir
|
||||||
|
- Sadece kendi resimlerini silebilir
|
||||||
|
|
||||||
|
### 2. **Moderator**
|
||||||
|
- Kendi resimlerini yükleyebilir
|
||||||
|
- **TÜM** kullanıcıların resimlerini görebilir
|
||||||
|
- **TÜM** resimleri silebilir
|
||||||
|
- Moderasyon yetkisi
|
||||||
|
|
||||||
|
### 3. **Admin**
|
||||||
|
- **TÜM** moderator izinlerine sahiptir
|
||||||
|
- Kullanıcıları yönetebilir (listeleme, silme)
|
||||||
|
- Kullanıcı rollerini değiştirebilir
|
||||||
|
- Tam sistem kontrolü
|
||||||
|
|
||||||
|
## İzinler
|
||||||
|
|
||||||
|
| İzin | User | Moderator | Admin |
|
||||||
|
|------|------|-----------|-------|
|
||||||
|
| `IMAGE_UPLOAD` | ✅ | ✅ | ✅ |
|
||||||
|
| `IMAGE_VIEW_OWN` | ✅ | ✅ | ✅ |
|
||||||
|
| `IMAGE_VIEW_ANY` | ❌ | ✅ | ✅ |
|
||||||
|
| `IMAGE_DELETE_OWN` | ✅ | ✅ | ✅ |
|
||||||
|
| `IMAGE_DELETE_ANY` | ❌ | ✅ | ✅ |
|
||||||
|
| `USER_VIEW` | ❌ | ❌ | ✅ |
|
||||||
|
| `USER_DELETE` | ❌ | ❌ | ✅ |
|
||||||
|
| `USER_MANAGE_ROLES` | ❌ | ❌ | ✅ |
|
||||||
|
| `USER_MANAGE_PERMISSIONS` | ❌ | ❌ | ✅ |
|
||||||
|
| `USER_MANAGE_API_KEYS` | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
## Admin Panel
|
||||||
|
|
||||||
|
Admin paneline erişim: **http://localhost:3000/admin**
|
||||||
|
|
||||||
|
### Özellikler
|
||||||
|
|
||||||
|
1. **Kullanıcı Listesi**
|
||||||
|
- Tüm kullanıcıları görüntüleme
|
||||||
|
- Kullanıcı detayları (email, rol, doğrulama durumu, kayıt tarihi)
|
||||||
|
|
||||||
|
2. **Rol Yönetimi**
|
||||||
|
- Dropdown menüden rol seçerek anında güncelleme
|
||||||
|
- Kendi rolünü değiştirememe koruması
|
||||||
|
|
||||||
|
3. **Kullanıcı Silme**
|
||||||
|
- Kullanıcıyı ve tüm verilerini silme
|
||||||
|
- Kullanıcının resimleri ve API anahtarları da silinir
|
||||||
|
- Kendi hesabını silememe koruması
|
||||||
|
|
||||||
|
4. **İstatistikler**
|
||||||
|
- Toplam kullanıcı sayısı
|
||||||
|
- Admin sayısı
|
||||||
|
- Moderatör sayısı
|
||||||
|
|
||||||
|
## API Endpoint'leri
|
||||||
|
|
||||||
|
### Admin API
|
||||||
|
|
||||||
|
#### 1. Kullanıcıları Listele
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/users
|
||||||
|
Authorization: Bearer <admin_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "user-id",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"role": "user",
|
||||||
|
"emailVerified": true,
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Kullanıcı Rolünü Değiştir
|
||||||
|
```http
|
||||||
|
PATCH /api/v1/admin/users/:id/role
|
||||||
|
Authorization: Bearer <admin_jwt_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "moderator"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Kullanıcı rolü başarıyla güncellendi",
|
||||||
|
"data": {
|
||||||
|
"userId": "user-id",
|
||||||
|
"newRole": "moderator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Koşullar:**
|
||||||
|
- Sadece admin yetkisi
|
||||||
|
- Kendi rolünü değiştiremez
|
||||||
|
- Geçerli roller: `user`, `moderator`, `admin`
|
||||||
|
|
||||||
|
#### 3. Kullanıcı Sil
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/admin/users/:id
|
||||||
|
Authorization: Bearer <admin_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Kullanıcı başarıyla silindi",
|
||||||
|
"data": {
|
||||||
|
"deletedUserId": "user-id",
|
||||||
|
"deletedUser": "user@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Koşullar:**
|
||||||
|
- Sadece admin yetkisi
|
||||||
|
- Kendi hesabını silemez
|
||||||
|
- Kullanıcının tüm resimleri ve API anahtarları da silinir
|
||||||
|
|
||||||
|
### Güncellenmiş Resim API'leri
|
||||||
|
|
||||||
|
#### 1. Resimleri Listele
|
||||||
|
```http
|
||||||
|
GET /api/v1/images
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Davranış:**
|
||||||
|
- **User**: Sadece kendi resimlerini görür
|
||||||
|
- **Moderator/Admin**: TÜM resimleri görür
|
||||||
|
|
||||||
|
#### 2. Resim Sil
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/images/:id
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Davranış:**
|
||||||
|
- **User**: Sadece kendi resimlerini silebilir
|
||||||
|
- **Moderator/Admin**: Herhangi bir resmi silebilir
|
||||||
|
|
||||||
|
## İlk Admin Oluşturma
|
||||||
|
|
||||||
|
Veritabanında manuel olarak ilk admin kullanıcısını oluşturun:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE "user"
|
||||||
|
SET role = 'admin'
|
||||||
|
WHERE email = 'your-email@example.com';
|
||||||
|
```
|
||||||
|
|
||||||
|
Veya direkt database üzerinden:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Drizzle Studio veya migration ile
|
||||||
|
import { db } from "./db";
|
||||||
|
import { user } from "./db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
await db.update(user)
|
||||||
|
.set({ role: "admin" })
|
||||||
|
.where(eq(user.email, "your-email@example.com"));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Güvenlik Özellikleri
|
||||||
|
|
||||||
|
1. **Kendi Hesap Koruması**
|
||||||
|
- Admin kendi rolünü değiştiremez
|
||||||
|
- Admin kendi hesabını silemez
|
||||||
|
|
||||||
|
2. **Yetkilendirme Kontrolleri**
|
||||||
|
- Her endpoint permission kontrolü yapar
|
||||||
|
- 401 (Unauthorized): Giriş yapmamış
|
||||||
|
- 403 (Forbidden): Yetkisi yok
|
||||||
|
|
||||||
|
3. **Cascade Silme**
|
||||||
|
- Kullanıcı silindiğinde tüm resimleri ve API anahtarları da silinir
|
||||||
|
|
||||||
|
4. **Role Validasyonu**
|
||||||
|
- Sadece geçerli roller kabul edilir
|
||||||
|
- Geçersiz rol atama engellenir
|
||||||
|
|
||||||
|
## Kullanım Örnekleri
|
||||||
|
|
||||||
|
### JavaScript/TypeScript (Web)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Admin paneline giriş (token localStorage'da saklanıyor)
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// Kullanıcıları listele
|
||||||
|
const response = await fetch("/api/v1/admin/users", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await response.json();
|
||||||
|
console.log(data.users);
|
||||||
|
|
||||||
|
// Rol değiştir
|
||||||
|
await fetch("/api/v1/admin/users/user-id/role", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ role: "moderator" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kullanıcı sil
|
||||||
|
await fetch("/api/v1/admin/users/user-id", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Kullanıcıları listele
|
||||||
|
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
|
||||||
|
http://localhost:3000/api/v1/admin/users
|
||||||
|
|
||||||
|
# Rol değiştir
|
||||||
|
curl -X PATCH \
|
||||||
|
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"role":"moderator"}' \
|
||||||
|
http://localhost:3000/api/v1/admin/users/USER_ID/role
|
||||||
|
|
||||||
|
# Kullanıcı sil
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
|
||||||
|
http://localhost:3000/api/v1/admin/users/USER_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Role field'ını veritabanına eklemek için:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Schema'dan migration oluştur
|
||||||
|
yarn db:generate
|
||||||
|
|
||||||
|
# Migration'ı çalıştır
|
||||||
|
yarn db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
Veya manuel olarak SQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE "user"
|
||||||
|
ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dosya Yapısı
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── lib/
|
||||||
|
│ ├── permissions.ts # RBAC sistem tanımları
|
||||||
|
│ └── api-auth.ts # Auth middleware (role içerir)
|
||||||
|
├── api/
|
||||||
|
│ └── v1/
|
||||||
|
│ └── admin/
|
||||||
|
│ └── users/
|
||||||
|
│ ├── route.ts # GET (list)
|
||||||
|
│ └── [id]/
|
||||||
|
│ ├── route.ts # DELETE
|
||||||
|
│ └── role/
|
||||||
|
│ └── route.ts # PATCH
|
||||||
|
└── admin/
|
||||||
|
└── page.tsx # Admin panel UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sonraki Adımlar
|
||||||
|
|
||||||
|
1. ✅ Role sistemi eklendi
|
||||||
|
2. ✅ Permission kontrolleri eklendi
|
||||||
|
3. ✅ Admin panel oluşturuldu
|
||||||
|
4. ✅ Kullanıcı yönetimi API'leri eklendi
|
||||||
|
5. ⏳ Email bildirimleri (rol değişikliği, hesap silme)
|
||||||
|
6. ⏳ Audit log sistemi (kim ne yaptı?)
|
||||||
|
7. ⏳ Gelişmiş filtreleme (role göre, tarihe göre)
|
||||||
351
API_README.md
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# Image API - Resim Manipülasyon API Dokümantasyonu
|
||||||
|
|
||||||
|
Bu API, dış uygulamaların resim yükleme, manipülasyon ve yönetim işlemlerini yapmasına olanak tanır.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
```
|
||||||
|
https://image.beyhano.com.tr
|
||||||
|
# veya development için
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
API, JWT token tabanlı kimlik doğrulama kullanır. Her istekte `Authorization` header'ında Bearer token gönderilmelidir.
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Kayıt Ol (Register)
|
||||||
|
|
||||||
|
Yeni kullanıcı kaydı oluşturur ve JWT token döner.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/v1/auth/register`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "minimum8karakter",
|
||||||
|
"name": "Kullanıcı Adı"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Kayıt başarılı",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": "user_123abc",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Kullanıcı Adı"
|
||||||
|
},
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hata Kodları:**
|
||||||
|
- `400` - Gerekli alanlar eksik veya geçersiz
|
||||||
|
- `409` - Email adresi zaten kullanımda
|
||||||
|
- `500` - Sunucu hatası
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Giriş Yap (Login)
|
||||||
|
|
||||||
|
Mevcut kullanıcı ile giriş yapar ve JWT token döner.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/v1/auth/login`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "minimum8karakter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Giriş başarılı",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": "user_123abc",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Kullanıcı Adı"
|
||||||
|
},
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hata Kodları:**
|
||||||
|
- `400` - Email veya şifre eksik
|
||||||
|
- `401` - Geçersiz email veya şifre
|
||||||
|
- `500` - Sunucu hatası
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Resim Yükle ve Manipüle Et
|
||||||
|
|
||||||
|
Resim yükler, belirtilen boyut/kalite/formatta işler ve kaydeder.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/v1/images/upload`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body (FormData):**
|
||||||
|
- `file` (required): Resim dosyası (max 10MB)
|
||||||
|
- `width` (optional): Genişlik (px), default: 800, max: 10000
|
||||||
|
- `height` (optional): Yükseklik (px), default: 600, max: 10000
|
||||||
|
- `quality` (optional): Kalite (1-100), default: 90
|
||||||
|
- `format` (optional): Format (jpeg, png, webp, avif), default: jpeg
|
||||||
|
|
||||||
|
**cURL Örneği:**
|
||||||
|
```bash
|
||||||
|
curl -X POST https://image.beyhano.com.tr/api/v1/images/upload \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-F "file=@/path/to/image.jpg" \
|
||||||
|
-F "width=1920" \
|
||||||
|
-F "height=1080" \
|
||||||
|
-F "quality=85" \
|
||||||
|
-F "format=webp"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Resim başarıyla yüklendi",
|
||||||
|
"data": {
|
||||||
|
"image": {
|
||||||
|
"id": "img_xyz789",
|
||||||
|
"url": "https://image.beyhano.com.tr/uploads/xyz789.webp",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"format": "webp",
|
||||||
|
"fileSize": 245678
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hata Kodları:**
|
||||||
|
- `400` - Dosya eksik, boyut çok büyük veya geçersiz tip
|
||||||
|
- `401` - Geçersiz veya eksik token
|
||||||
|
- `500` - Sunucu hatası
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Resimleri Listele
|
||||||
|
|
||||||
|
Kullanıcının tüm resimlerini listeler.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/v1/images`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": "img_xyz789",
|
||||||
|
"originalName": "photo.jpg",
|
||||||
|
"url": "https://image.beyhano.com.tr/uploads/xyz789.webp",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"quality": 85,
|
||||||
|
"format": "webp",
|
||||||
|
"fileSize": 245678,
|
||||||
|
"createdAt": "2026-01-06T02:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hata Kodları:**
|
||||||
|
- `401` - Geçersiz veya eksik token
|
||||||
|
- `500` - Sunucu hatası
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Resim Sil
|
||||||
|
|
||||||
|
Belirtilen ID'ye sahip resmi siler.
|
||||||
|
|
||||||
|
**Endpoint:** `DELETE /api/v1/images/{id}`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**cURL Örneği:**
|
||||||
|
```bash
|
||||||
|
curl -X DELETE https://image.beyhano.com.tr/api/v1/images/img_xyz789 \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Resim başarıyla silindi"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hata Kodları:**
|
||||||
|
- `401` - Geçersiz veya eksik token
|
||||||
|
- `404` - Resim bulunamadı veya size ait değil
|
||||||
|
- `500` - Sunucu hatası
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Örnek Kullanım (JavaScript/Node.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Kayıt ol
|
||||||
|
const registerResponse = await fetch('https://image.beyhano.com.tr/api/v1/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'securepassword123',
|
||||||
|
name: 'Kullanıcı'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const { data } = await registerResponse.json();
|
||||||
|
const token = data.accessToken;
|
||||||
|
|
||||||
|
// 2. Resim yükle
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
formData.append('width', '1920');
|
||||||
|
formData.append('height', '1080');
|
||||||
|
formData.append('quality', '85');
|
||||||
|
formData.append('format', 'webp');
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('https://image.beyhano.com.tr/api/v1/images/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const uploadData = await uploadResponse.json();
|
||||||
|
console.log('Yüklenen resim:', uploadData.data.image.url);
|
||||||
|
|
||||||
|
// 3. Resimleri listele
|
||||||
|
const imagesResponse = await fetch('https://image.beyhano.com.tr/api/v1/images', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const imagesData = await imagesResponse.json();
|
||||||
|
console.log('Toplam resim:', imagesData.data.total);
|
||||||
|
|
||||||
|
// 4. Resim sil
|
||||||
|
const deleteResponse = await fetch(`https://image.beyhano.com.tr/api/v1/images/${imageId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const deleteData = await deleteResponse.json();
|
||||||
|
console.log(deleteData.message);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Örnek Kullanım (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 1. Kayıt ol
|
||||||
|
register_response = requests.post(
|
||||||
|
'https://image.beyhano.com.tr/api/v1/auth/register',
|
||||||
|
json={
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'password': 'securepassword123',
|
||||||
|
'name': 'Kullanıcı'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
token = register_response.json()['data']['accessToken']
|
||||||
|
|
||||||
|
# 2. Resim yükle
|
||||||
|
with open('image.jpg', 'rb') as f:
|
||||||
|
upload_response = requests.post(
|
||||||
|
'https://image.beyhano.com.tr/api/v1/images/upload',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
files={'file': f},
|
||||||
|
data={
|
||||||
|
'width': '1920',
|
||||||
|
'height': '1080',
|
||||||
|
'quality': '85',
|
||||||
|
'format': 'webp'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
image_url = upload_response.json()['data']['image']['url']
|
||||||
|
print(f'Yüklenen resim: {image_url}')
|
||||||
|
|
||||||
|
# 3. Resimleri listele
|
||||||
|
images_response = requests.get(
|
||||||
|
'https://image.beyhano.com.tr/api/v1/images',
|
||||||
|
headers={'Authorization': f'Bearer {token}'}
|
||||||
|
)
|
||||||
|
images = images_response.json()['data']['images']
|
||||||
|
print(f'Toplam resim: {len(images)}')
|
||||||
|
|
||||||
|
# 4. Resim sil
|
||||||
|
delete_response = requests.delete(
|
||||||
|
f'https://image.beyhano.com.tr/api/v1/images/{image_id}',
|
||||||
|
headers={'Authorization': f'Bearer {token}'}
|
||||||
|
)
|
||||||
|
print(delete_response.json()['message'])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Güvenlik
|
||||||
|
|
||||||
|
- JWT token'lar 7 gün geçerlidir
|
||||||
|
- Şifreler bcrypt ile hashlenmiş olarak saklanır
|
||||||
|
- Token'ları güvenli bir şekilde saklayın
|
||||||
|
- HTTPS kullanın (production'da)
|
||||||
|
- Rate limiting uygulanabilir
|
||||||
|
|
||||||
|
## Limitler
|
||||||
|
|
||||||
|
- Maximum dosya boyutu: 10MB
|
||||||
|
- Maximum resim boyutu: 10000x10000 px
|
||||||
|
- Desteklenen formatlar: JPEG, PNG, WebP, AVIF, GIF
|
||||||
|
- JWT token geçerlilik süresi: 7 gün
|
||||||
|
|
||||||
|
## Hata Yönetimi
|
||||||
|
|
||||||
|
Tüm hata yanıtları şu formatta döner:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Hata mesajı burada"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP status kodları:
|
||||||
|
- `200` - Başarılı
|
||||||
|
- `400` - Kötü istek (geçersiz parametreler)
|
||||||
|
- `401` - Kimlik doğrulama hatası
|
||||||
|
- `404` - Bulunamadı
|
||||||
|
- `409` - Çakışma (örn: email zaten kullanımda)
|
||||||
|
- `500` - Sunucu hatası
|
||||||
84
API_TEST.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Image API - Test Senaryoları
|
||||||
|
|
||||||
|
## API Test Komutları (cURL)
|
||||||
|
|
||||||
|
### 1. Kayıt Ol
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "test12345",
|
||||||
|
"name": "Test User"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Login
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "test12345"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Yanıttan `accessToken` değerini alın ve aşağıdaki komutlarda kullanın:
|
||||||
|
```bash
|
||||||
|
export TOKEN="buraya_token_yapistirin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Resim Yükle
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/images/upload \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-F "file=@/path/to/image.jpg" \
|
||||||
|
-F "width=800" \
|
||||||
|
-F "height=600" \
|
||||||
|
-F "quality=90" \
|
||||||
|
-F "format=webp"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Resimleri Listele
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:3000/api/v1/images \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Resim Sil
|
||||||
|
```bash
|
||||||
|
export IMAGE_ID="buraya_image_id_yapistirin"
|
||||||
|
curl -X DELETE http://localhost:3000/api/v1/images/$IMAGE_ID \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Flow
|
||||||
|
|
||||||
|
1. Önce register olun ve token alın
|
||||||
|
2. Token ile resim yükleyin
|
||||||
|
3. Yüklenen resimleri listeleyin
|
||||||
|
4. Bir resmi silin
|
||||||
|
5. Tekrar listeleyin ve silindiğini doğrulayın
|
||||||
|
|
||||||
|
## Hata Testleri
|
||||||
|
|
||||||
|
### Geçersiz Token
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:3000/api/v1/images \
|
||||||
|
-H "Authorization: Bearer invalid_token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Olmadan
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:3000/api/v1/images
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geçersiz Login
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "yanlis_sifre"
|
||||||
|
}'
|
||||||
|
```
|
||||||
60
DOCKER.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Docker Deployment Guide
|
||||||
|
|
||||||
|
Bu proje Node.js v24.12.0 ve Yarn 1.22.22 ile Dockerize edilmiştir.
|
||||||
|
|
||||||
|
## Gereksinimler
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
## Hızlı Başlangıç
|
||||||
|
|
||||||
|
1. `.env` dosyası oluşturun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
||||||
|
BETTER_AUTH_SECRET=your-secret-key-here-min-32-characters-long
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Not:** `DATABASE_URL` mevcut PostgreSQL sunucunuzun bağlantı bilgilerini içermelidir. Eğer PostgreSQL Docker dışında çalışıyorsa, host IP adresini veya `host.docker.internal` (Mac/Windows) kullanabilirsiniz.
|
||||||
|
|
||||||
|
2. Docker Compose ile başlatın:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Veritabanı migration'larını çalıştırın (ilk kurulumda):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec image-api yarn db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Uygulama `http://localhost:3000` adresinde çalışacaktır.
|
||||||
|
|
||||||
|
## Manuel Docker Build
|
||||||
|
|
||||||
|
Sadece uygulamayı build etmek için:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t image-api .
|
||||||
|
docker run -p 3000:3000 --env-file .env image-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Notları
|
||||||
|
|
||||||
|
- `.env` dosyasında `BETTER_AUTH_SECRET` mutlaka güçlü bir secret olmalıdır (en az 32 karakter)
|
||||||
|
- Production'da `BETTER_AUTH_URL` gerçek domain'inizi içermelidir
|
||||||
|
- PostgreSQL veritabanı dış bir sunucuda çalışmaktadır (docker-compose'da dahil değildir)
|
||||||
|
- `DATABASE_URL` mevcut PostgreSQL sunucunuzun erişilebilir adresini içermelidir (örn: `postgresql://user:pass@10.80.80.70:5432/dbname`)
|
||||||
|
- Upload klasörü volume olarak mount edilmiştir, böylece veriler kalıcı olur
|
||||||
|
- Container, host'un network'ündeki PostgreSQL sunucusuna erişebilir
|
||||||
|
|
||||||
|
## Güvenlik
|
||||||
|
|
||||||
|
- Tüm güvenlik header'ları yapılandırılmıştır
|
||||||
|
- File upload validasyonları eklenmiştir
|
||||||
|
- Debug bilgileri production'dan kaldırılmıştır
|
||||||
|
- Input validasyonları eklenmiştir
|
||||||
107
DOKPLOY_TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Dokploy Troubleshooting Guide
|
||||||
|
|
||||||
|
## PostgreSQL Bağlantı Sorunları
|
||||||
|
|
||||||
|
### Hata: `ECONNREFUSED` veya `Failed query`
|
||||||
|
|
||||||
|
Bu hata, uygulamanın PostgreSQL sunucusuna bağlanamadığını gösterir.
|
||||||
|
|
||||||
|
### Kontrol Listesi
|
||||||
|
|
||||||
|
1. **DATABASE_URL Environment Variable Kontrolü**
|
||||||
|
|
||||||
|
Dokploy'da environment variables bölümünde `DATABASE_URL` değerini kontrol edin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Doğru format:
|
||||||
|
DATABASE_URL=postgresql://user:password@host:port/database?search_path=public
|
||||||
|
|
||||||
|
# Örnek:
|
||||||
|
DATABASE_URL=postgresql://cloud:gg7678290@10.80.80.70:5432/image_api?search_path=public
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Network Erişimi**
|
||||||
|
|
||||||
|
- Dokploy container'ından PostgreSQL sunucusuna erişilebilir olmalı
|
||||||
|
- Eğer PostgreSQL farklı bir network'teyse, network yapılandırmasını kontrol edin
|
||||||
|
- Firewall kurallarını kontrol edin (port 5432 açık olmalı)
|
||||||
|
|
||||||
|
3. **IP Adresi ve Port**
|
||||||
|
|
||||||
|
- Hata mesajında görünen IP/port ile DATABASE_URL'deki IP/port eşleşmeli
|
||||||
|
- Eğer farklıysa, Dokploy'da environment variable'ı güncelleyin
|
||||||
|
|
||||||
|
4. **Connection String Formatı**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Doğru
|
||||||
|
postgresql://user:password@10.80.80.70:5432/dbname
|
||||||
|
|
||||||
|
# ❌ Yanlış
|
||||||
|
postgres://user:password@10.80.80.70:5432/dbname # postgres yerine postgresql kullanın
|
||||||
|
postgresql://user:password@10.80.80.70/dbname # Port eksik
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Schema Belirtme**
|
||||||
|
|
||||||
|
Eğer `public` dışında bir schema kullanıyorsanız:
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://user:password@host:port/dbname?search_path=your_schema
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dokploy'da Environment Variables Ayarlama
|
||||||
|
|
||||||
|
1. Dokploy dashboard'a giriş yapın
|
||||||
|
2. Projenizi seçin
|
||||||
|
3. "Environment Variables" bölümüne gidin
|
||||||
|
4. `DATABASE_URL` değerini kontrol edin/güncelleyin
|
||||||
|
5. Değişikliklerden sonra container'ı yeniden başlatın
|
||||||
|
|
||||||
|
### Test Komutları
|
||||||
|
|
||||||
|
Container içinden bağlantıyı test etmek için:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container'a bağlan
|
||||||
|
docker exec -it image-api-app sh
|
||||||
|
|
||||||
|
# PostgreSQL bağlantısını test et
|
||||||
|
psql $DATABASE_URL -c "SELECT version();"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Yaygın Hatalar ve Çözümleri
|
||||||
|
|
||||||
|
**Hata:** `ECONNREFUSED 10.0.1.215:5455`
|
||||||
|
- **Sebep:** DATABASE_URL yanlış IP/port içeriyor
|
||||||
|
- **Çözüm:** Dokploy'da DATABASE_URL'i doğru IP ve port ile güncelleyin
|
||||||
|
|
||||||
|
**Hata:** `timeout expired`
|
||||||
|
- **Sebep:** Network erişim sorunu veya firewall
|
||||||
|
- **Çözüm:** Network yapılandırmasını ve firewall kurallarını kontrol edin
|
||||||
|
|
||||||
|
**Hata:** `password authentication failed`
|
||||||
|
- **Sebep:** Yanlış kullanıcı adı/şifre
|
||||||
|
- **Çözüm:** DATABASE_URL'deki credentials'ları kontrol edin
|
||||||
|
|
||||||
|
### Örnek Doğru DATABASE_URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local PostgreSQL
|
||||||
|
DATABASE_URL=postgresql://postgres:password@localhost:5432/image_api?search_path=public
|
||||||
|
|
||||||
|
# Remote PostgreSQL
|
||||||
|
DATABASE_URL=postgresql://cloud:gg7678290@10.80.80.70:5432/image_api?search_path=public
|
||||||
|
|
||||||
|
# SSL ile
|
||||||
|
DATABASE_URL=postgresql://user:pass@host:5432/dbname?sslmode=require
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Modu
|
||||||
|
|
||||||
|
Daha detaylı hata mesajları için:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# db.ts dosyasında pool.on('error') event handler'ı logları gösterir
|
||||||
|
# Container loglarını kontrol edin:
|
||||||
|
docker logs image-api-app
|
||||||
|
```
|
||||||
56
Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Node.js v24.12.0 kullan
|
||||||
|
FROM node:24.12.0-alpine AS base
|
||||||
|
|
||||||
|
# Yarn için corepack'i etkinleştir
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
# Dependencies stage - sadece gerekli bağımlılıkları yükle
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Package dosyalarını kopyala
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
# Builder stage - uygulamayı build et
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Node modules'ü kopyala
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build için gerekli environment variables
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
# Build-time için dummy değerler (runtime'da gerçek değerler kullanılacak)
|
||||||
|
ENV DATABASE_URL="postgresql://cloud:gg7678290@database-postgist-5pcspx:5432/image_api?search_path=public"
|
||||||
|
ENV BETTER_AUTH_SECRET="dB89kiKf56igxrB783yb3UyQToQIPZ93cRKADyq1yQEJ8EU4JRw5GlxBmGvQMu8e"
|
||||||
|
ENV BETTER_AUTH_URL="https://image.beyhano.com.tr"
|
||||||
|
ENV REGISTER_ENABLE=false
|
||||||
|
# Next.js build
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Runner stage - production için minimal image
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Güvenlik için non-root user oluştur
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Standalone build'den dosyaları kopyala
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
220
README.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Image Manipulation API
|
||||||
|
|
||||||
|
Modern, güvenli ve ölçeklenebilir resim yönetim ve manipülasyon platformu. Next.js 16, Better Auth ve Drizzle ORM ile geliştirilmiştir.
|
||||||
|
|
||||||
|
## ✨ Özellikler
|
||||||
|
|
||||||
|
### 🔐 Güvenlik & Kimlik Doğrulama
|
||||||
|
- **Better Auth** ile session tabanlı kimlik doğrulama
|
||||||
|
- **JWT** destekli REST API (dış uygulamalar için)
|
||||||
|
- **Role-Based Access Control (RBAC)** - 3 farklı rol (User, Moderator, Admin)
|
||||||
|
- API Key yönetimi
|
||||||
|
- Şifre hashleme (bcrypt)
|
||||||
|
|
||||||
|
### 🎨 Resim İşleme
|
||||||
|
- **Sharp** kütüphanesi ile hızlı resim manipülasyonu
|
||||||
|
- Boyutlandırma (width/height)
|
||||||
|
- Format dönüştürme (JPEG, PNG, WebP, AVIF)
|
||||||
|
- Kalite ayarı (1-100)
|
||||||
|
- Otomatik dosya boyutu optimizasyonu
|
||||||
|
|
||||||
|
### 👥 Kullanıcı Yönetimi
|
||||||
|
- **Admin Panel** - Kullanıcıları yönetme, rol atama, silme
|
||||||
|
- Kullanıcı profili
|
||||||
|
- Email doğrulama desteği
|
||||||
|
- Kayıt açma/kapama kontrolü
|
||||||
|
|
||||||
|
### 📊 Rol ve İzinler
|
||||||
|
|
||||||
|
| Rol | İzinler |
|
||||||
|
|-----|---------|
|
||||||
|
| **User** | Kendi resimlerini yükleyebilir, görüntüleyebilir ve silebilir |
|
||||||
|
| **Moderator** | Tüm resimleri görüntüleyebilir ve silebilir |
|
||||||
|
| **Admin** | Tüm moderator izinleri + Kullanıcı yönetimi |
|
||||||
|
|
||||||
|
### 🚀 API Özellikleri
|
||||||
|
- RESTful API tasarımı
|
||||||
|
- JWT token authentication
|
||||||
|
- API Key authentication
|
||||||
|
- Rate limiting (opsiyonel)
|
||||||
|
- Swagger-style dokümantasyon
|
||||||
|
|
||||||
|
## 📦 Teknolojiler
|
||||||
|
|
||||||
|
- **Framework**: Next.js 16.1.1 (App Router, Turbopack)
|
||||||
|
- **Auth**: better-auth v1.4.10
|
||||||
|
- **Database**: PostgreSQL + Drizzle ORM v0.45.1
|
||||||
|
- **Image Processing**: Sharp v0.34.5
|
||||||
|
- **Styling**: Tailwind CSS v4
|
||||||
|
- **Container**: Docker & Docker Compose
|
||||||
|
- **Runtime**: Node.js v24.12.0
|
||||||
|
|
||||||
|
## 🛠️ Kurulum
|
||||||
|
|
||||||
|
### 1. Proje Klonlama
|
||||||
|
\`\`\`bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd image-api
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 2. Ortam Değişkenleri
|
||||||
|
\`.env\` dosyası oluşturun:
|
||||||
|
\`\`\`env
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/image_api
|
||||||
|
BETTER_AUTH_SECRET=your-super-secret-key-min-32-chars
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
JWT_SECRET=another-super-secret-key-min-32-characters
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 3. Docker ile Çalıştırma
|
||||||
|
\`\`\`bash
|
||||||
|
# PostgreSQL başlat
|
||||||
|
docker compose up -d postgres
|
||||||
|
|
||||||
|
# Bağımlılıkları yükle
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Database migration
|
||||||
|
yarn db:push
|
||||||
|
|
||||||
|
# Development server
|
||||||
|
yarn dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4. İlk Admin Kullanıcısı
|
||||||
|
\`\`\`bash
|
||||||
|
# Kayıt ol
|
||||||
|
# Sonra admin yap:
|
||||||
|
npx tsx make-admin.ts your-email@example.com
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 📚 API Kullanımı
|
||||||
|
|
||||||
|
### Kayıt ve Login
|
||||||
|
\`\`\`bash
|
||||||
|
# Kayıt
|
||||||
|
curl -X POST http://localhost:3000/api/v1/auth/register \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"email":"user@example.com","password":"password123","name":"John Doe"}'
|
||||||
|
|
||||||
|
# Login
|
||||||
|
curl -X POST http://localhost:3000/api/v1/auth/login \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"email":"user@example.com","password":"password123"}'
|
||||||
|
|
||||||
|
# Response: {"success":true,"token":"jwt-token-here"}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Resim Yükleme
|
||||||
|
\`\`\`bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/images/upload \\
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \\
|
||||||
|
-F "file=@photo.jpg" \\
|
||||||
|
-F "width=800" \\
|
||||||
|
-F "quality=90" \\
|
||||||
|
-F "format=webp"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Resim Listeleme
|
||||||
|
\`\`\`bash
|
||||||
|
# User: Sadece kendi resimleri
|
||||||
|
# Moderator/Admin: Tüm resimler
|
||||||
|
curl http://localhost:3000/api/v1/images \\
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Admin - Kullanıcıları Listeleme
|
||||||
|
\`\`\`bash
|
||||||
|
curl http://localhost:3000/api/v1/admin/users \\
|
||||||
|
-H "Authorization: Bearer ADMIN_JWT_TOKEN"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Admin - Rol Değiştirme
|
||||||
|
\`\`\`bash
|
||||||
|
curl -X PATCH http://localhost:3000/api/v1/admin/users/USER_ID/role \\
|
||||||
|
-H "Authorization: Bearer ADMIN_JWT_TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"role":"moderator"}'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Detaylı API dokümantasyonu: **http://localhost:3000/api-docs**
|
||||||
|
|
||||||
|
## 🎯 Sayfalar
|
||||||
|
|
||||||
|
- \`/\` - Anasayfa
|
||||||
|
- \`/login\` - Giriş yap
|
||||||
|
- \`/register\` - Kayıt ol (REGISTER_ENABLE=true ise)
|
||||||
|
- \`/upload\` - Resim yükle
|
||||||
|
- \`/profile\` - Profil ve resimlerim
|
||||||
|
- \`/admin\` - Admin panel (sadece adminler)
|
||||||
|
- \`/api-docs\` - API dokümantasyonu
|
||||||
|
|
||||||
|
## 🔒 Güvenlik Özellikleri
|
||||||
|
|
||||||
|
1. **Password Hashing**: bcrypt ile şifreler güvenle saklanır
|
||||||
|
2. **JWT Tokens**: 7 günlük geçerlilik süresi
|
||||||
|
3. **Role-Based Access**: Endpoint bazında yetkilendirme
|
||||||
|
4. **Session Management**: Better Auth ile güvenli session yönetimi
|
||||||
|
5. **CORS**: Yapılandırılabilir CORS desteği
|
||||||
|
6. **Environment Variables**: Hassas bilgiler .env'de
|
||||||
|
|
||||||
|
## 📊 Database Schema
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
- \`user\` - Kullanıcı bilgileri (email, password_hash, role)
|
||||||
|
- \`session\` - Aktif oturumlar
|
||||||
|
- \`account\` - OAuth provider hesapları
|
||||||
|
- \`verification\` - Email doğrulama
|
||||||
|
- \`images\` - Yüklenen resimler
|
||||||
|
- \`apiKeys\` - API anahtarları
|
||||||
|
|
||||||
|
## 🐳 Docker Production Build
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Build image
|
||||||
|
docker build -t image-api .
|
||||||
|
|
||||||
|
# Run with docker-compose
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker compose logs -f image-api
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 📝 Scripts
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
yarn dev # Development server
|
||||||
|
yarn build # Production build
|
||||||
|
yarn start # Production server
|
||||||
|
yarn lint # ESLint
|
||||||
|
yarn db:generate # Generate migrations
|
||||||
|
yarn db:push # Push schema to DB
|
||||||
|
yarn db:studio # Drizzle Studio
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 🤝 Katkıda Bulunma
|
||||||
|
|
||||||
|
1. Fork edin
|
||||||
|
2. Feature branch oluşturun (\`git checkout -b feature/amazing\`)
|
||||||
|
3. Commit edin (\`git commit -m 'Add amazing feature'\`)
|
||||||
|
4. Push edin (\`git push origin feature/amazing\`)
|
||||||
|
5. Pull Request açın
|
||||||
|
|
||||||
|
## 📄 Lisans
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## 👨💻 Geliştirici
|
||||||
|
|
||||||
|
Beyhan Oğur - [beyhan@beyhan.dev](mailto:beyhan@beyhan.dev)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Daha fazla bilgi için:**
|
||||||
|
- [API Documentation](./API_README.md)
|
||||||
|
- [Admin Panel Guide](./ADMIN_PANEL.md)
|
||||||
|
- [Docker Setup](./DOCKER.md)
|
||||||
|
- [Security Guide](./SECURITY.md)
|
||||||
61
SECURITY.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Güvenlik İyileştirmeleri
|
||||||
|
|
||||||
|
Bu dokümanda production'a hazırlık için yapılan güvenlik iyileştirmeleri listelenmektedir.
|
||||||
|
|
||||||
|
## Yapılan İyileştirmeler
|
||||||
|
|
||||||
|
### 1. Debug Bilgilerinin Kaldırılması
|
||||||
|
- ✅ Production console.log ifadeleri kaldırıldı
|
||||||
|
- ✅ API response'lardan debug bilgileri çıkarıldı
|
||||||
|
- ✅ Hata mesajlarında hassas bilgi sızıntısı önlendi
|
||||||
|
|
||||||
|
### 2. File Upload Güvenliği
|
||||||
|
- ✅ Dosya boyutu limiti eklendi (maksimum 10MB)
|
||||||
|
- ✅ MIME type validasyonu eklendi
|
||||||
|
- ✅ Sadece resim dosyaları kabul ediliyor (jpeg, jpg, png, gif, webp, avif)
|
||||||
|
- ✅ Path traversal koruması (nanoid kullanımı)
|
||||||
|
|
||||||
|
### 3. Input Validasyonu
|
||||||
|
- ✅ Width/Height validasyonu (1-10000px arası)
|
||||||
|
- ✅ Quality validasyonu (1-100 arası)
|
||||||
|
- ✅ Format validasyonu (sadece izin verilen formatlar)
|
||||||
|
- ✅ Image ID validasyonu (uzunluk ve tip kontrolü)
|
||||||
|
|
||||||
|
### 4. Security Headers
|
||||||
|
- ✅ Strict-Transport-Security (HSTS)
|
||||||
|
- ✅ X-Frame-Options
|
||||||
|
- ✅ X-Content-Type-Options
|
||||||
|
- ✅ X-XSS-Protection
|
||||||
|
- ✅ Referrer-Policy
|
||||||
|
- ✅ Permissions-Policy
|
||||||
|
|
||||||
|
### 5. Authentication & Authorization
|
||||||
|
- ✅ Tüm API endpoint'lerinde authentication kontrolü
|
||||||
|
- ✅ User-based authorization (kullanıcılar sadece kendi resimlerini görebilir/silebilir)
|
||||||
|
- ✅ Better Auth kullanımı (güvenli session yönetimi)
|
||||||
|
|
||||||
|
### 6. Database Security
|
||||||
|
- ✅ Drizzle ORM kullanımı (SQL injection koruması)
|
||||||
|
- ✅ Parameterized queries
|
||||||
|
- ✅ Foreign key constraints
|
||||||
|
|
||||||
|
### 7. Environment Variables
|
||||||
|
- ✅ Hassas bilgiler environment variable'larda
|
||||||
|
- ✅ .env.example dosyası oluşturuldu
|
||||||
|
- ✅ .gitignore'da .env dosyaları ignore ediliyor
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [ ] `BETTER_AUTH_SECRET` güçlü bir secret olarak ayarlanmalı (min 32 karakter)
|
||||||
|
- [ ] `BETTER_AUTH_URL` production domain'i ile güncellenmeli
|
||||||
|
- [ ] `DATABASE_URL` production veritabanı bağlantısı ile güncellenmeli
|
||||||
|
- [ ] HTTPS kullanılmalı (production'da)
|
||||||
|
- [ ] Rate limiting eklenmeli (opsiyonel, yüksek trafik için)
|
||||||
|
- [ ] Monitoring ve logging kurulumu yapılmalı
|
||||||
|
- [ ] Regular backup stratejisi oluşturulmalı
|
||||||
|
|
||||||
|
## Notlar
|
||||||
|
|
||||||
|
- File upload limiti 10MB olarak ayarlanmıştır. Gerekirse artırılabilir.
|
||||||
|
- Tüm hata mesajları generic olarak döndürülmektedir (hassas bilgi sızıntısını önlemek için).
|
||||||
|
- Console.log ifadeleri production'dan kaldırılmıştır, ancak geliştirme ortamında gerekirse eklenebilir.
|
||||||
76
SETUP.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Kurulum Talimatları
|
||||||
|
|
||||||
|
Bu proje Next.js, Better Auth ve Drizzle ORM ile PostgreSQL entegrasyonu içerir.
|
||||||
|
|
||||||
|
## Gereksinimler
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- PostgreSQL veritabanı
|
||||||
|
- npm veya yarn
|
||||||
|
|
||||||
|
## Kurulum Adımları
|
||||||
|
|
||||||
|
### 1. Bağımlılıkları Yükleyin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# veya
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ortam Değişkenlerini Ayarlayın
|
||||||
|
|
||||||
|
Proje kök dizininde `.env` dosyası oluşturun:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||||
|
BETTER_AUTH_SECRET=your-secret-key-here-minimum-32-characters
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
REGISTER_ENABLE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Önemli:**
|
||||||
|
- `BETTER_AUTH_SECRET` en az 32 karakter uzunluğunda güvenli bir rastgele string olmalıdır.
|
||||||
|
- `REGISTER_ENABLE` kayıt sayfasını açıp kapatmak için kullanılır. `true` veya `false` değeri alabilir. Varsayılan olarak `true`'dur.
|
||||||
|
|
||||||
|
### 3. Veritabanı Şemasını Oluşturun
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:push
|
||||||
|
# veya
|
||||||
|
yarn db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
Bu komut, `db/schema.ts` dosyasındaki şemaya göre veritabanı tablolarını oluşturacaktır.
|
||||||
|
|
||||||
|
### 4. Geliştirme Sunucusunu Başlatın
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# veya
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Tarayıcınızda [http://localhost:3000](http://localhost:3000) adresine gidin.
|
||||||
|
|
||||||
|
## Kullanım
|
||||||
|
|
||||||
|
1. **Kayıt Ol:** `/register` sayfasından yeni bir hesap oluşturun
|
||||||
|
2. **Giriş Yap:** `/login` sayfasından giriş yapın
|
||||||
|
3. **Profil:** Giriş yaptıktan sonra `/profile` sayfasında kullanıcı bilgilerinizi görüntüleyin
|
||||||
|
|
||||||
|
## Veritabanı Komutları
|
||||||
|
|
||||||
|
- `npm run db:generate` - Migration dosyalarını oluşturur
|
||||||
|
- `npm run db:push` - Şemayı veritabanına uygular
|
||||||
|
- `npm run db:studio` - Drizzle Studio'yu açar (veritabanı görüntüleme aracı)
|
||||||
|
|
||||||
|
## Dosya Yapısı
|
||||||
|
|
||||||
|
- `db.ts` - Drizzle veritabanı bağlantısı
|
||||||
|
- `db/schema.ts` - Veritabanı şema tanımları
|
||||||
|
- `app/lib/auth.ts` - Better Auth yapılandırması
|
||||||
|
- `app/api/auth/[...all]/route.ts` - Better Auth API route handler
|
||||||
|
- `app/login/page.tsx` - Giriş sayfası
|
||||||
|
- `app/register/page.tsx` - Kayıt sayfası
|
||||||
|
- `app/profile/page.tsx` - Kullanıcı profil sayfası
|
||||||
401
app/admin/page.tsx
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPanel() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [currentUserRole, setCurrentUserRole] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/get-session", {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: Session = await res.json();
|
||||||
|
|
||||||
|
if (!session.user || session.user.role !== "admin") {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erişim Engellendi",
|
||||||
|
text: "Bu sayfaya erişim yetkiniz yok. Sadece adminler görebilir.",
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentUserRole(session.user.role || "user");
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/admin/users", {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === 403) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Yetki Hatası",
|
||||||
|
text: "Bu sayfaya erişim yetkiniz yok.",
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("Kullanıcılar yüklenemedi");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setUsers(data.data.users);
|
||||||
|
} catch (err: any) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Hata",
|
||||||
|
text: err.message,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeRole = async (userId: string, newRole: string, currentRole: string) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: "Rol Değiştir",
|
||||||
|
text: `Bu kullanıcının rolünü "${currentRole}" → "${newRole}" olarak değiştirmek istediğinizden emin misiniz?`,
|
||||||
|
icon: "question",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
cancelButtonColor: "#6b7280",
|
||||||
|
confirmButtonText: "Evet, değiştir",
|
||||||
|
cancelButtonText: "İptal",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
await fetchUsers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/users/${userId}/role`, {
|
||||||
|
method: "PATCH",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ role: newRole }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "Rol güncellenemedi");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Başarılı!",
|
||||||
|
text: "Kullanıcı rolü güncellendi",
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err: any) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Hata",
|
||||||
|
text: err.message,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
await fetchUsers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEmailVerification = async (userId: string, currentStatus: boolean, email: string) => {
|
||||||
|
const newStatus = !currentStatus;
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: "Email Doğrulama",
|
||||||
|
text: `${email} için email doğrulamasını ${newStatus ? "aktif" : "pasif"} yapmak istiyor musunuz?`,
|
||||||
|
icon: "question",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
cancelButtonColor: "#6b7280",
|
||||||
|
confirmButtonText: "Evet, değiştir",
|
||||||
|
cancelButtonText: "İptal",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/users/${userId}/verification`, {
|
||||||
|
method: "PATCH",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ emailVerified: newStatus }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "Doğrulama güncellenemedi");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Başarılı!",
|
||||||
|
text: `Email doğrulama ${newStatus ? "aktif edildi" : "pasif edildi"}`,
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err: any) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Hata",
|
||||||
|
text: err.message,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (userId: string, email: string) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: "Kullanıcıyı Sil",
|
||||||
|
html: `<b>${email}</b> kullanıcısını silmek istediğinizden emin misiniz?<br><br>
|
||||||
|
<span style="color: #ef4444; font-weight: 600;">⚠️ Bu işlem geri alınamaz!</span><br>
|
||||||
|
<small>Kullanıcının tüm resimleri ve verileri silinecek.</small>`,
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#ef4444",
|
||||||
|
cancelButtonColor: "#6b7280",
|
||||||
|
confirmButtonText: "Evet, sil!",
|
||||||
|
cancelButtonText: "İptal",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/users/${userId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "Kullanıcı silinemedi");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Silindi!",
|
||||||
|
text: "Kullanıcı başarıyla silindi",
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err: any) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Hata",
|
||||||
|
text: err.message,
|
||||||
|
confirmButtonColor: "#3b82f6",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoleBadgeColor = (role: string) => {
|
||||||
|
switch (role) {
|
||||||
|
case "admin":
|
||||||
|
return "bg-red-100 text-red-800 border-red-200";
|
||||||
|
case "moderator":
|
||||||
|
return "bg-blue-100 text-blue-800 border-blue-200";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800 border-gray-200";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Admin Panel</h1>
|
||||||
|
<p className="text-gray-600">Kullanıcı yönetimi ve rol atama</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">Kullanıcı</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">Email</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">Rol</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">Doğrulama</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">Kayıt Tarihi</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{user.name || "İsimsiz"}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">{user.id.substring(0, 8)}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-700">{user.email}</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<select
|
||||||
|
value={user.role}
|
||||||
|
onChange={(e) => changeRole(user.id, e.target.value, user.role)}
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-medium border ${getRoleBadgeColor(
|
||||||
|
user.role
|
||||||
|
)} focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer transition-all hover:shadow-md`}
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="moderator">Moderator</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleEmailVerification(user.id, user.emailVerified, user.email)}
|
||||||
|
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium transition-all cursor-pointer hover:shadow-md ${
|
||||||
|
user.emailVerified
|
||||||
|
? "bg-green-100 text-green-700 hover:bg-green-200"
|
||||||
|
: "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{user.emailVerified ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Doğrulandı
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Beklemede
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-600">
|
||||||
|
{new Date(user.createdAt).toLocaleDateString("tr-TR")}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<button
|
||||||
|
onClick={() => deleteUser(user.id, user.email)}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Sil
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 mb-2">Toplam Kullanıcı</h3>
|
||||||
|
<p className="text-3xl font-bold text-gray-900">{users.length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 mb-2">Admin Sayısı</h3>
|
||||||
|
<p className="text-3xl font-bold text-red-600">
|
||||||
|
{users.filter((u) => u.role === "admin").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 mb-2">Moderatör Sayısı</h3>
|
||||||
|
<p className="text-3xl font-bold text-blue-600">
|
||||||
|
{users.filter((u) => u.role === "moderator").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 mb-2">Doğrulananlar</h3>
|
||||||
|
<p className="text-3xl font-bold text-green-600">
|
||||||
|
{users.filter((u) => u.emailVerified).length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
279
app/api-docs/page.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function ApiDocsPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-black dark:to-zinc-900">
|
||||||
|
<div className="mx-auto max-w-5xl px-4 py-12">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Ana Sayfa
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div className="mb-12 text-center">
|
||||||
|
<h1 className="mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-5xl font-bold text-transparent dark:from-blue-400 dark:to-purple-400">
|
||||||
|
API Dokümantasyonu
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300">
|
||||||
|
Image Manipulation API - REST API Kullanım Kılavuzu
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Base URL */}
|
||||||
|
<section className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Base URL
|
||||||
|
</h2>
|
||||||
|
<div className="rounded-lg bg-gray-100 p-4 dark:bg-zinc-900">
|
||||||
|
<code className="text-blue-600 dark:text-blue-400">
|
||||||
|
https://v2.beyhano.com.tr
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Authentication */}
|
||||||
|
<section className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Authentication
|
||||||
|
</h2>
|
||||||
|
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||||
|
API, JWT token tabanlı kimlik doğrulama kullanır. Her istekte Authorization header'ında Bearer token gönderilmelidir:
|
||||||
|
</p>
|
||||||
|
<div className="rounded-lg bg-gray-100 p-4 dark:bg-zinc-900">
|
||||||
|
<code className="text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Endpoints */}
|
||||||
|
<section className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Endpoints
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Register */}
|
||||||
|
<div className="mb-8 border-l-4 border-green-500 pl-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="rounded bg-green-100 px-2 py-1 text-xs font-semibold text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||||
|
POST
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
/api/v1/auth/register
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="mb-3 text-gray-600 dark:text-gray-400">
|
||||||
|
Yeni kullanıcı kaydı oluşturur ve JWT token döner.
|
||||||
|
</p>
|
||||||
|
<div className="rounded-lg bg-gray-100 p-4 dark:bg-zinc-900">
|
||||||
|
<pre className="text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
{`{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "minimum8karakter",
|
||||||
|
"name": "Kullanıcı Adı"
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Login */}
|
||||||
|
<div className="mb-8 border-l-4 border-blue-500 pl-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-semibold text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
|
||||||
|
POST
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
/api/v1/auth/login
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="mb-3 text-gray-600 dark:text-gray-400">
|
||||||
|
Mevcut kullanıcı ile giriş yapar ve JWT token döner.
|
||||||
|
</p>
|
||||||
|
<div className="rounded-lg bg-gray-100 p-4 dark:bg-zinc-900">
|
||||||
|
<pre className="text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
{`{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "minimum8karakter"
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Image */}
|
||||||
|
<div className="mb-8 border-l-4 border-purple-500 pl-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-semibold text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
|
||||||
|
POST
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
/api/v1/images/upload
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="mb-3 text-gray-600 dark:text-gray-400">
|
||||||
|
Resim yükler, belirtilen boyut/kalite/formatta işler ve kaydeder. (multipart/form-data)
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div><strong>file</strong> (required): Resim dosyası (max 10MB)</div>
|
||||||
|
<div><strong>width</strong> (optional): Genişlik (px), default: 800</div>
|
||||||
|
<div><strong>height</strong> (optional): Yükseklik (px), default: 600</div>
|
||||||
|
<div><strong>quality</strong> (optional): Kalite (1-100), default: 90</div>
|
||||||
|
<div><strong>format</strong> (optional): jpeg, png, webp, avif</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List Images */}
|
||||||
|
<div className="mb-8 border-l-4 border-yellow-500 pl-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="rounded bg-yellow-100 px-2 py-1 text-xs font-semibold text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||||
|
GET
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
/api/v1/images
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Kullanıcının tüm resimlerini listeler.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Image */}
|
||||||
|
<div className="border-l-4 border-red-500 pl-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="rounded bg-red-100 px-2 py-1 text-xs font-semibold text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||||
|
DELETE
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
/api/v1/images/:id
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Belirtilen ID'ye sahip resmi siler.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Example Code */}
|
||||||
|
<section className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Örnek Kullanım (JavaScript)
|
||||||
|
</h2>
|
||||||
|
<div className="rounded-lg bg-gray-100 p-4 dark:bg-zinc-900">
|
||||||
|
<pre className="overflow-x-auto text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
{`// 1. Kayıt ol
|
||||||
|
const registerResponse = await fetch(
|
||||||
|
'https://image.beyhano.com.tr/api/v1/auth/register',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'securepassword123',
|
||||||
|
name: 'Kullanıcı'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { data } = await registerResponse.json();
|
||||||
|
const token = data.accessToken;
|
||||||
|
|
||||||
|
// 2. Resim yükle
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
formData.append('width', '1920');
|
||||||
|
formData.append('quality', '85');
|
||||||
|
formData.append('format', 'webp');
|
||||||
|
|
||||||
|
const uploadResponse = await fetch(
|
||||||
|
'https://image.beyhano.com.tr/api/v1/images/upload',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': \`Bearer \${token}\` },
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const uploadData = await uploadResponse.json();
|
||||||
|
console.log('URL:', uploadData.data.image.url);`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<section className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Özellikler ve Limitler
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 font-semibold text-gray-900 dark:text-white">
|
||||||
|
✅ Desteklenen Formatlar
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<li>• JPEG / JPG</li>
|
||||||
|
<li>• PNG</li>
|
||||||
|
<li>• WebP</li>
|
||||||
|
<li>• AVIF</li>
|
||||||
|
<li>• GIF</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 font-semibold text-gray-900 dark:text-white">
|
||||||
|
⚙️ Limitler
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<li>• Max dosya: 10MB</li>
|
||||||
|
<li>• Max boyut: 10000x10000 px</li>
|
||||||
|
<li>• Token süresi: 7 gün</li>
|
||||||
|
<li>• HTTPS zorunlu (production)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Download Full Docs */}
|
||||||
|
<section className="rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 p-6 text-center text-white shadow-lg">
|
||||||
|
<h3 className="mb-2 text-xl font-bold">Detaylı Dokümantasyon</h3>
|
||||||
|
<p className="mb-4">
|
||||||
|
Tüm endpoint'ler, hata kodları ve örnekler için tam dokümantasyonu indirin.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/yourusername/image-api"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 font-semibold text-blue-600 transition-transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
GitHub'da Görüntüle
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
app/api/admin/users/[id]/role/route.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { isAdmin, UserRole, updateUserRole } from "@/app/lib/permissions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/admin/users/[id]/role
|
||||||
|
* Kullanıcının rolünü değiştir (Sadece admin - Web Session)
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Giriş yapmalısınız" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
const userRole = (session.user as any).role || "user";
|
||||||
|
if (!isAdmin(userRole)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler rol değiştirebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { role } = body;
|
||||||
|
|
||||||
|
// Role validasyonu
|
||||||
|
const validRoles: UserRole[] = ["user", "admin", "moderator"];
|
||||||
|
if (!role || !validRoles.includes(role)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Geçersiz rol. Geçerli roller: user, admin, moderator" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kendi rolünü değiştirmeyi engelle
|
||||||
|
if (userId === session.user.id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kendi rolünüzü değiştiremezsiniz" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rolü güncelle
|
||||||
|
await updateUserRole(userId, role);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Kullanıcı rolü başarıyla güncellendi",
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
newRole: role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Rol güncelleme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Rol güncellenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
app/api/admin/users/[id]/route.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { hasPermission, PERMISSIONS } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user, images, apiKeys } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/admin/users/[id]
|
||||||
|
* Kullanıcıyı sil (Sadece admin - Web Session)
|
||||||
|
* Kullanıcının tüm resimleri ve API anahtarları da silinir
|
||||||
|
*/
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Giriş yapmalısınız" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission kontrolü
|
||||||
|
const userRole = (session.user as any).role || "user";
|
||||||
|
if (!hasPermission(userRole, PERMISSIONS.USER_DELETE)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler kullanıcı silebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
|
||||||
|
// Kendi hesabını silmeyi engelle
|
||||||
|
if (userId === session.user.id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kendi hesabınızı silemezsiniz" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcının var olup olmadığını kontrol et
|
||||||
|
const targetUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);
|
||||||
|
if (targetUser.length === 0) {
|
||||||
|
return NextResponse.json({ error: "Kullanıcı bulunamadı" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcının resimlerini sil
|
||||||
|
await db.delete(images).where(eq(images.userId, userId));
|
||||||
|
|
||||||
|
// Kullanıcının API anahtarlarını sil
|
||||||
|
await db.delete(apiKeys).where(eq(apiKeys.userId, userId));
|
||||||
|
|
||||||
|
// Kullanıcıyı sil
|
||||||
|
await db.delete(user).where(eq(user.id, userId));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Kullanıcı başarıyla silindi",
|
||||||
|
data: {
|
||||||
|
deletedUserId: userId,
|
||||||
|
deletedUser: targetUser[0].email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Kullanıcı silme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kullanıcı silinemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/api/admin/users/[id]/verification/route.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { isAdmin } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/admin/users/[id]/verification
|
||||||
|
* Kullanıcının email doğrulamasını değiştir (Sadece admin - Web Session)
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Giriş yapmalısınız" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
const userRole = (session.user as any).role || "user";
|
||||||
|
if (!isAdmin(userRole)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler doğrulama değiştirebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { emailVerified } = body;
|
||||||
|
|
||||||
|
// Boolean validasyonu
|
||||||
|
if (typeof emailVerified !== "boolean") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "emailVerified boolean olmalıdır" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email doğrulama durumunu güncelle
|
||||||
|
const result = await db
|
||||||
|
.update(user)
|
||||||
|
.set({ emailVerified })
|
||||||
|
.where(eq(user.id, userId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return NextResponse.json({ error: "Kullanıcı bulunamadı" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Email doğrulama ${emailVerified ? "aktif edildi" : "pasif edildi"}`,
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
emailVerified,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Email doğrulama güncelleme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Email doğrulama güncellenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/api/admin/users/route.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { isAdmin } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user } from "@/db/schema";
|
||||||
|
import { desc } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/admin/users
|
||||||
|
* Tüm kullanıcıları listele (Sadece admin - Web Session)
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Giriş yapmalısınız" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
const userRole = (session.user as any).role || "user";
|
||||||
|
if (!isAdmin(userRole)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler kullanıcıları görüntüleyebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await db
|
||||||
|
.select({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
emailVerified: user.emailVerified,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
})
|
||||||
|
.from(user)
|
||||||
|
.orderBy(desc(user.createdAt));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users,
|
||||||
|
total: users.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Kullanıcı listesi hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kullanıcılar yüklenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/api/auth/[...all]/route.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
|
export const { GET, POST } = toNextJsHandler(auth);
|
||||||
7
app/api/config/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
registerEnabled: process.env.REGISTER_ENABLE === "true",
|
||||||
|
});
|
||||||
|
}
|
||||||
80
app/api/images/[id]/route.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { unlink } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
async function getUserId(request: NextRequest): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
return session?.user?.id || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> | { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = await getUserId(request);
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Yetkisiz erişim" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next.js 15'te params async olabilir
|
||||||
|
const resolvedParams = await Promise.resolve(params);
|
||||||
|
const imageId = resolvedParams.id;
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
if (!imageId || typeof imageId !== "string" || imageId.length > 255) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Geçersiz resim ID" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resmi veritabanından bul
|
||||||
|
const image = await db
|
||||||
|
.select()
|
||||||
|
.from(images)
|
||||||
|
.where(and(eq(images.id, imageId), eq(images.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (image.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Resim bulunamadı veya yetkiniz yok" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = image[0];
|
||||||
|
|
||||||
|
// Dosyayı sil
|
||||||
|
try {
|
||||||
|
await unlink(imageData.filePath);
|
||||||
|
} catch (error) {
|
||||||
|
// Dosya bulunamazsa devam et (log production'da kaldırıldı)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Veritabanından sil
|
||||||
|
await db.delete(images).where(eq(images.id, imageId));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "Resim başarıyla silindi",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Silme işlemi başarısız" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
app/api/images/route.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
|
||||||
|
async function getUserId(request: NextRequest): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
return session?.user?.id || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(request: NextRequest): string {
|
||||||
|
// First, check environment variables (production should set this)
|
||||||
|
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_URL;
|
||||||
|
}
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
return process.env.APP_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reverse proxy headers (X-Forwarded-Host, X-Forwarded-Proto)
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||||
|
|
||||||
|
if (forwardedHost && forwardedProto) {
|
||||||
|
return `${forwardedProto}://${forwardedHost}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to request origin
|
||||||
|
return request.nextUrl.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const userId = await getUserId(request);
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Yetkisiz erişim" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userImages = await db
|
||||||
|
.select()
|
||||||
|
.from(images)
|
||||||
|
.where(eq(images.userId, userId))
|
||||||
|
.orderBy(desc(images.createdAt));
|
||||||
|
|
||||||
|
// Get base URL
|
||||||
|
const baseUrl = getBaseUrl(request);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
images: userImages.map((img) => ({
|
||||||
|
id: img.id,
|
||||||
|
originalName: img.originalName,
|
||||||
|
url: `${baseUrl}${img.url}`,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
quality: img.quality,
|
||||||
|
format: img.format,
|
||||||
|
fileSize: img.fileSize,
|
||||||
|
createdAt: img.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Resimler yüklenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
196
app/api/images/upload/route.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { writeFile, mkdir } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
|
||||||
|
async function getUserId(request: NextRequest): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
return session?.user?.id || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(request: NextRequest): string {
|
||||||
|
// First, check environment variables (production should set this)
|
||||||
|
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_URL;
|
||||||
|
}
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
return process.env.APP_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reverse proxy headers (X-Forwarded-Host, X-Forwarded-Proto)
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||||
|
|
||||||
|
if (forwardedHost && forwardedProto) {
|
||||||
|
return `${forwardedProto}://${forwardedHost}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to request origin
|
||||||
|
return request.nextUrl.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const userId = await getUserId(request);
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Yetkisiz erişim" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get("file") as File;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Dosya bulunamadı" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File size validation (max 10MB)
|
||||||
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Dosya boyutu çok büyük. Maksimum 10MB olmalıdır." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File type validation
|
||||||
|
const allowedMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/avif"];
|
||||||
|
if (!allowedMimeTypes.includes(file.type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Geçersiz dosya tipi. Sadece resim dosyaları kabul edilir." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
const widthInput = formData.get("width") as string;
|
||||||
|
const heightInput = formData.get("height") as string;
|
||||||
|
const qualityInput = formData.get("quality") as string;
|
||||||
|
const formatInput = (formData.get("format") as string) || "jpeg";
|
||||||
|
|
||||||
|
const width = Math.max(1, Math.min(10000, parseInt(widthInput) || 800));
|
||||||
|
const height = Math.max(1, Math.min(10000, parseInt(heightInput) || 600));
|
||||||
|
const quality = Math.max(1, Math.min(100, parseInt(qualityInput) || 90));
|
||||||
|
const allowedFormats = ["jpeg", "jpg", "png", "webp", "avif"];
|
||||||
|
const format = allowedFormats.includes(formatInput) ? formatInput : "jpeg";
|
||||||
|
|
||||||
|
const bytes = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(bytes);
|
||||||
|
|
||||||
|
// Resim manipülasyonu - Tam istenen boyuta getir (crop ile, bozmadan)
|
||||||
|
// fit: "cover" kullanarak resmi tam boyuta getiriyoruz
|
||||||
|
// Aspect ratio korunur, fazla kısımlar ortadan kesilir (crop)
|
||||||
|
let processedBuffer = sharp(buffer).resize(width, height, {
|
||||||
|
fit: "cover", // Tam boyuta getir, aspect ratio koru, fazla kısımları kes
|
||||||
|
position: "center", // Ortadan crop yap
|
||||||
|
withoutEnlargement: false, // Gerekirse büyüt de tam boyuta getir
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format ve kalite ayarları
|
||||||
|
const normalizedFormat = format === "jpg" ? "jpeg" : format;
|
||||||
|
if (normalizedFormat === "jpeg") {
|
||||||
|
processedBuffer = processedBuffer.jpeg({ quality });
|
||||||
|
} else if (normalizedFormat === "png") {
|
||||||
|
processedBuffer = processedBuffer.png({ quality });
|
||||||
|
} else if (normalizedFormat === "webp") {
|
||||||
|
processedBuffer = processedBuffer.webp({ quality });
|
||||||
|
} else if (normalizedFormat === "avif") {
|
||||||
|
processedBuffer = processedBuffer.avif({ quality });
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedImage = await processedBuffer.toBuffer();
|
||||||
|
const metadata = await sharp(processedImage).metadata();
|
||||||
|
|
||||||
|
// Dosya adı oluştur
|
||||||
|
const fileId = nanoid();
|
||||||
|
const originalName = file.name;
|
||||||
|
const fileExtension = normalizedFormat === "jpeg" ? "jpg" : normalizedFormat;
|
||||||
|
const fileName = `${fileId}.${fileExtension}`;
|
||||||
|
|
||||||
|
// Uploads klasörünü oluştur
|
||||||
|
// Docker volume mount: /app/public/uploads
|
||||||
|
// Standalone build'de process.cwd() = /app olmalı
|
||||||
|
const uploadsDir = join(process.cwd(), "public", "uploads");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
} catch (mkdirError: any) {
|
||||||
|
console.error("Uploads klasörü oluşturulamadı:", mkdirError);
|
||||||
|
console.error("Klasör yolu:", uploadsDir);
|
||||||
|
console.error("Current working directory:", process.cwd());
|
||||||
|
throw new Error(`Uploads klasörü oluşturulamadı: ${mkdirError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dosyayı kaydet
|
||||||
|
const filePath = join(uploadsDir, fileName);
|
||||||
|
try {
|
||||||
|
await writeFile(filePath, processedImage);
|
||||||
|
} catch (writeError: any) {
|
||||||
|
console.error("Dosya yazılamadı:", writeError);
|
||||||
|
console.error("Dosya yolu:", filePath);
|
||||||
|
console.error("Dosya boyutu:", processedImage.length);
|
||||||
|
throw new Error(`Dosya yazılamadı: ${writeError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Veritabanına kaydet
|
||||||
|
const imageUrl = `/uploads/${fileName}`;
|
||||||
|
const imageId = nanoid();
|
||||||
|
|
||||||
|
await db.insert(images).values({
|
||||||
|
id: imageId,
|
||||||
|
userId,
|
||||||
|
originalName,
|
||||||
|
fileName,
|
||||||
|
filePath: filePath,
|
||||||
|
url: imageUrl,
|
||||||
|
width: metadata.width || null,
|
||||||
|
height: metadata.height || null,
|
||||||
|
quality,
|
||||||
|
format: normalizedFormat,
|
||||||
|
fileSize: processedImage.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get base URL
|
||||||
|
const baseUrl = getBaseUrl(request);
|
||||||
|
const fullImageUrl = `${baseUrl}${imageUrl}`;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "Resim başarıyla yüklendi",
|
||||||
|
image: {
|
||||||
|
id: imageId,
|
||||||
|
url: fullImageUrl,
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Upload hatası:", error);
|
||||||
|
console.error("Error stack:", error?.stack);
|
||||||
|
console.error("Error message:", error?.message);
|
||||||
|
|
||||||
|
// Production'da detaylı hata mesajı döndür (debug için)
|
||||||
|
const errorMessage = process.env.NODE_ENV === "production"
|
||||||
|
? `Yükleme başarısız: ${error?.message || "Bilinmeyen hata"}`
|
||||||
|
: "Yükleme başarısız";
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: errorMessage },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/api/v1/admin/users/[id]/role/route.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { isAdmin, UserRole, updateUserRole } from "@/app/lib/permissions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/v1/admin/users/[id]/role
|
||||||
|
* Kullanıcının rolünü değiştir (Sadece admin)
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
if (!isAdmin(auth.role!)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler rol değiştirebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { role } = body;
|
||||||
|
|
||||||
|
// Role validasyonu
|
||||||
|
const validRoles: UserRole[] = ["user", "admin", "moderator"];
|
||||||
|
if (!role || !validRoles.includes(role)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Geçersiz rol. Geçerli roller: user, admin, moderator" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kendi rolünü değiştirmeyi engelle
|
||||||
|
if (userId === auth.userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kendi rolünüzü değiştiremezsiniz" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rolü güncelle
|
||||||
|
await updateUserRole(userId, role);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Kullanıcı rolü başarıyla güncellendi",
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
newRole: role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Rol güncelleme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Rol güncellenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/api/v1/admin/users/[id]/route.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { hasPermission, PERMISSIONS } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user, images, apiKeys } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/v1/admin/users/[id]
|
||||||
|
* Kullanıcıyı sil (Sadece admin)
|
||||||
|
* Kullanıcının tüm resimleri ve API anahtarları da silinir
|
||||||
|
*/
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission kontrolü
|
||||||
|
if (!hasPermission(auth.role!, PERMISSIONS.USER_DELETE)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler kullanıcı silebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
|
||||||
|
// Kendi hesabını silmeyi engelle
|
||||||
|
if (userId === auth.userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kendi hesabınızı silemezsiniz" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcının var olup olmadığını kontrol et
|
||||||
|
const targetUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);
|
||||||
|
if (targetUser.length === 0) {
|
||||||
|
return NextResponse.json({ error: "Kullanıcı bulunamadı" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcının resimlerini sil
|
||||||
|
const deletedImages = await db.delete(images).where(eq(images.userId, userId));
|
||||||
|
|
||||||
|
// Kullanıcının API anahtarlarını sil
|
||||||
|
await db.delete(apiKeys).where(eq(apiKeys.userId, userId));
|
||||||
|
|
||||||
|
// Kullanıcıyı sil
|
||||||
|
await db.delete(user).where(eq(user.id, userId));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Kullanıcı başarıyla silindi",
|
||||||
|
data: {
|
||||||
|
deletedUserId: userId,
|
||||||
|
deletedUser: targetUser[0].email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Kullanıcı silme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kullanıcı silinemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/api/v1/admin/users/[id]/verification/route.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { isAdmin } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/v1/admin/users/[id]/verification
|
||||||
|
* Kullanıcının email doğrulamasını değiştir (Sadece admin - JWT)
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
if (!isAdmin(auth.role!)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler doğrulama değiştirebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id: userId } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { emailVerified } = body;
|
||||||
|
|
||||||
|
// Boolean validasyonu
|
||||||
|
if (typeof emailVerified !== "boolean") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "emailVerified boolean olmalıdır" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email doğrulama durumunu güncelle
|
||||||
|
const result = await db
|
||||||
|
.update(user)
|
||||||
|
.set({ emailVerified })
|
||||||
|
.where(eq(user.id, userId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return NextResponse.json({ error: "Kullanıcı bulunamadı" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Email doğrulama ${emailVerified ? "aktif edildi" : "pasif edildi"}`,
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
emailVerified,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Email doğrulama güncelleme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Email doğrulama güncellenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/api/v1/admin/users/route.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { isAdmin } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { user } from "@/db/schema";
|
||||||
|
import { desc } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/admin/users
|
||||||
|
* Tüm kullanıcıları listele (Sadece admin)
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin kontrolü
|
||||||
|
if (!isAdmin(auth.role!)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu işlem için yetkiniz yok. Sadece adminler kullanıcıları görüntüleyebilir." },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await db
|
||||||
|
.select({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
emailVerified: user.emailVerified,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
})
|
||||||
|
.from(user)
|
||||||
|
.orderBy(desc(user.createdAt));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users,
|
||||||
|
total: users.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Kullanıcı listesi hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Kullanıcılar yüklenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/api/v1/auth/login/route.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { signJWT } from "@/app/lib/jwt";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { email, password } = body;
|
||||||
|
|
||||||
|
// Validasyon
|
||||||
|
if (!email || !password) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Email ve password gereklidir" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better Auth ile giriş yap
|
||||||
|
try {
|
||||||
|
const signInResponse = await auth.api.signInEmail({
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!signInResponse || !signInResponse.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Geçersiz email veya şifre" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = signInResponse.user;
|
||||||
|
|
||||||
|
// JWT token oluştur
|
||||||
|
const accessToken = signJWT(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
type: "access",
|
||||||
|
},
|
||||||
|
"7d"
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Giriş başarılı",
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (authError: any) {
|
||||||
|
// Better Auth hatası - muhtemelen geçersiz credentials
|
||||||
|
console.error("Better Auth login hatası:", authError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Geçersiz email veya şifre" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Login API hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Giriş sırasında bir hata oluştu" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/api/v1/auth/register/route.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/lib/auth";
|
||||||
|
import { signJWT } from "@/app/lib/jwt";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { email, password, name } = body;
|
||||||
|
|
||||||
|
// Validasyon
|
||||||
|
if (!email || !password || !name) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Email, password ve name gereklidir" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Şifre en az 8 karakter olmalıdır" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better Auth ile kullanıcı oluştur
|
||||||
|
try {
|
||||||
|
const signUpResponse = await auth.api.signUpEmail({
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!signUpResponse || !signUpResponse.user) {
|
||||||
|
throw new Error("Kullanıcı oluşturulamadı");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = signUpResponse.user;
|
||||||
|
|
||||||
|
// JWT token oluştur
|
||||||
|
const accessToken = signJWT(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
type: "access",
|
||||||
|
},
|
||||||
|
"7d"
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Kayıt başarılı",
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (authError: any) {
|
||||||
|
// Better Auth hatası - muhtemelen email zaten kullanımda
|
||||||
|
if (authError.message?.includes("exists") || authError.message?.includes("duplicate")) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu email adresi zaten kullanımda" },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw authError;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Register API hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || "Kayıt sırasında bir hata oluştu" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/api/v1/images/[id]/route.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { hasPermission, PERMISSIONS } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { unlink } from "fs/promises";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/v1/images/[id]
|
||||||
|
* Resim sil
|
||||||
|
* Kullanıcılar sadece kendi resimlerini silebilir
|
||||||
|
* Moderator ve adminler herhangi bir resmi silebilir
|
||||||
|
*
|
||||||
|
* Headers:
|
||||||
|
* - Authorization: Bearer <jwt_token>
|
||||||
|
*/
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
// Permission kontrolü - moderator ve admin herhangi bir resmi silebilir
|
||||||
|
const canDeleteAny = hasPermission(auth.role!, PERMISSIONS.IMAGE_DELETE_ANY);
|
||||||
|
|
||||||
|
// Resmi bul
|
||||||
|
const imageRecords = await db
|
||||||
|
.select()
|
||||||
|
.from(images)
|
||||||
|
.where(eq(images.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (imageRecords.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Resim bulunamadı" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = imageRecords[0];
|
||||||
|
|
||||||
|
// Yetki kontrolü - kendi resmi değilse ve delete any yetkisi yoksa reddedilir
|
||||||
|
if (!canDeleteAny && image.userId !== auth.userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Bu resmi silme yetkiniz yok" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dosyayı sil
|
||||||
|
try {
|
||||||
|
await unlink(image.filePath);
|
||||||
|
} catch (fileError) {
|
||||||
|
console.error("Dosya silinemedi:", fileError);
|
||||||
|
// Devam et, veritabanından sil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Veritabanından sil
|
||||||
|
await db.delete(images).where(eq(images.id, id));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Resim başarıyla silindi",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("API - Resim silme hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Resim silinemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/api/v1/images/route.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { hasPermission, PERMISSIONS } from "@/app/lib/permissions";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/images
|
||||||
|
* Kullanıcının tüm resimlerini listele
|
||||||
|
* Moderator ve adminler tüm resimleri görebilir
|
||||||
|
*
|
||||||
|
* Headers:
|
||||||
|
* - Authorization: Bearer <jwt_token>
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Permission kontrolü - admin ve moderator tüm resimleri görebilir
|
||||||
|
const canViewAll = hasPermission(auth.role!, PERMISSIONS.IMAGE_VIEW_ANY);
|
||||||
|
|
||||||
|
let userImages;
|
||||||
|
if (canViewAll) {
|
||||||
|
// Tüm resimleri listele
|
||||||
|
userImages = await db
|
||||||
|
.select()
|
||||||
|
.from(images)
|
||||||
|
.orderBy(desc(images.createdAt));
|
||||||
|
} else {
|
||||||
|
// Sadece kendi resimlerini listele
|
||||||
|
userImages = await db
|
||||||
|
.select()
|
||||||
|
.from(images)
|
||||||
|
.where(eq(images.userId, auth.userId!))
|
||||||
|
.orderBy(desc(images.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base URL'i al
|
||||||
|
const baseUrl = getBaseUrl(request);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
images: userImages.map((img) => ({
|
||||||
|
id: img.id,
|
||||||
|
originalName: img.originalName,
|
||||||
|
url: `${baseUrl}${img.url}`,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
quality: img.quality,
|
||||||
|
format: img.format,
|
||||||
|
fileSize: img.fileSize,
|
||||||
|
createdAt: img.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
total: userImages.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("API - Resim listesi hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Resimler yüklenemedi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(request: NextRequest): string {
|
||||||
|
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_URL;
|
||||||
|
}
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
return process.env.APP_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||||
|
|
||||||
|
if (forwardedHost && forwardedProto) {
|
||||||
|
return `${forwardedProto}://${forwardedHost}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.nextUrl.origin;
|
||||||
|
}
|
||||||
182
app/api/v1/images/upload/route.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authenticateAPIRequest } from "@/app/lib/api-auth";
|
||||||
|
import { writeFile, mkdir } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { images } from "@/db/schema";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/images/upload
|
||||||
|
* Resim yükle ve manipüle et
|
||||||
|
*
|
||||||
|
* Headers:
|
||||||
|
* - Authorization: Bearer <jwt_token>
|
||||||
|
* - Content-Type: multipart/form-data
|
||||||
|
*
|
||||||
|
* Body (FormData):
|
||||||
|
* - file: Resim dosyası
|
||||||
|
* - width: Genişlik (px) - opsiyonel, default: 800
|
||||||
|
* - height: Yükseklik (px) - opsiyonel, default: 600
|
||||||
|
* - quality: Kalite (1-100) - opsiyonel, default: 90
|
||||||
|
* - format: Format (jpeg, png, webp, avif) - opsiyonel, default: jpeg
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const auth = await authenticateAPIRequest(request);
|
||||||
|
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return NextResponse.json({ error: auth.error }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get("file") as File;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Dosya bulunamadı" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dosya boyutu kontrolü (max 10MB)
|
||||||
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Dosya boyutu çok büyük. Maksimum 10MB olmalıdır." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dosya tipi kontrolü
|
||||||
|
const allowedMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/avif"];
|
||||||
|
if (!allowedMimeTypes.includes(file.type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Geçersiz dosya tipi. Sadece resim dosyaları kabul edilir." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parametreleri al
|
||||||
|
const widthInput = formData.get("width") as string;
|
||||||
|
const heightInput = formData.get("height") as string;
|
||||||
|
const qualityInput = formData.get("quality") as string;
|
||||||
|
const formatInput = (formData.get("format") as string) || "jpeg";
|
||||||
|
|
||||||
|
const width = Math.max(1, Math.min(10000, parseInt(widthInput) || 800));
|
||||||
|
const height = Math.max(1, Math.min(10000, parseInt(heightInput) || 600));
|
||||||
|
const quality = Math.max(1, Math.min(100, parseInt(qualityInput) || 90));
|
||||||
|
const allowedFormats = ["jpeg", "jpg", "png", "webp", "avif"];
|
||||||
|
const format = allowedFormats.includes(formatInput) ? formatInput : "jpeg";
|
||||||
|
|
||||||
|
const bytes = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(bytes);
|
||||||
|
|
||||||
|
// Resim manipülasyonu
|
||||||
|
let processedBuffer = sharp(buffer).resize(width, height, {
|
||||||
|
fit: "cover",
|
||||||
|
position: "center",
|
||||||
|
withoutEnlargement: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format ve kalite ayarları
|
||||||
|
const normalizedFormat = format === "jpg" ? "jpeg" : format;
|
||||||
|
if (normalizedFormat === "jpeg") {
|
||||||
|
processedBuffer = processedBuffer.jpeg({ quality });
|
||||||
|
} else if (normalizedFormat === "png") {
|
||||||
|
processedBuffer = processedBuffer.png({ quality });
|
||||||
|
} else if (normalizedFormat === "webp") {
|
||||||
|
processedBuffer = processedBuffer.webp({ quality });
|
||||||
|
} else if (normalizedFormat === "avif") {
|
||||||
|
processedBuffer = processedBuffer.avif({ quality });
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedImage = await processedBuffer.toBuffer();
|
||||||
|
const metadata = await sharp(processedImage).metadata();
|
||||||
|
|
||||||
|
// Dosya kaydet
|
||||||
|
const fileId = nanoid();
|
||||||
|
const originalName = file.name;
|
||||||
|
const fileExtension = normalizedFormat === "jpeg" ? "jpg" : normalizedFormat;
|
||||||
|
const fileName = `${fileId}.${fileExtension}`;
|
||||||
|
|
||||||
|
const uploadsDir = join(process.cwd(), "public", "uploads");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
} catch (mkdirError: any) {
|
||||||
|
console.error("Uploads klasörü oluşturulamadı:", mkdirError);
|
||||||
|
throw new Error(`Uploads klasörü oluşturulamadı: ${mkdirError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = join(uploadsDir, fileName);
|
||||||
|
try {
|
||||||
|
await writeFile(filePath, processedImage);
|
||||||
|
} catch (writeError: any) {
|
||||||
|
console.error("Dosya yazılamadı:", writeError);
|
||||||
|
throw new Error(`Dosya yazılamadı: ${writeError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Veritabanına kaydet
|
||||||
|
const imageUrl = `/uploads/${fileName}`;
|
||||||
|
const imageId = nanoid();
|
||||||
|
|
||||||
|
await db.insert(images).values({
|
||||||
|
id: imageId,
|
||||||
|
userId: auth.userId!,
|
||||||
|
originalName,
|
||||||
|
fileName,
|
||||||
|
filePath: filePath,
|
||||||
|
url: imageUrl,
|
||||||
|
width: metadata.width || null,
|
||||||
|
height: metadata.height || null,
|
||||||
|
quality,
|
||||||
|
format: normalizedFormat,
|
||||||
|
fileSize: processedImage.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Base URL
|
||||||
|
const baseUrl = getBaseUrl(request);
|
||||||
|
const fullImageUrl = `${baseUrl}${imageUrl}`;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Resim başarıyla yüklendi",
|
||||||
|
data: {
|
||||||
|
image: {
|
||||||
|
id: imageId,
|
||||||
|
url: fullImageUrl,
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
|
format: normalizedFormat,
|
||||||
|
fileSize: processedImage.length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("API - Upload hatası:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || "Yükleme başarısız" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(request: NextRequest): string {
|
||||||
|
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_URL;
|
||||||
|
}
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
return process.env.APP_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||||
|
|
||||||
|
if (forwardedHost && forwardedProto) {
|
||||||
|
return `${forwardedProto}://${forwardedHost}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.nextUrl.origin;
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
26
app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
34
app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
170
app/lib/api-auth.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { apiKeys, user } from "@/db/schema";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { verifyJWT, isValidAPIKeyFormat } from "./jwt";
|
||||||
|
import { UserRole } from "./permissions";
|
||||||
|
|
||||||
|
export interface AuthenticatedRequest extends NextRequest {
|
||||||
|
userId?: string;
|
||||||
|
email?: string;
|
||||||
|
role?: UserRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResult {
|
||||||
|
authenticated: boolean;
|
||||||
|
userId?: string;
|
||||||
|
email?: string;
|
||||||
|
role?: UserRole;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API isteklerini doğrula (JWT token veya API key ile)
|
||||||
|
*
|
||||||
|
* Kullanım:
|
||||||
|
* const authResult = await authenticateAPIRequest(request);
|
||||||
|
* if (!authResult.authenticated) {
|
||||||
|
* return NextResponse.json({ error: authResult.error }, { status: 401 });
|
||||||
|
* }
|
||||||
|
* const userId = authResult.userId;
|
||||||
|
*/
|
||||||
|
export async function authenticateAPIRequest(request: NextRequest): Promise<AuthResult> {
|
||||||
|
const authHeader = request.headers.get("authorization");
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Authorization header eksik. Bearer token veya API key gerekli.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer token kontrolü
|
||||||
|
if (authHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
|
// JWT token mu yoksa API key mi?
|
||||||
|
if (isValidAPIKeyFormat(token)) {
|
||||||
|
// API Key doğrulama
|
||||||
|
return await validateAPIKey(token);
|
||||||
|
} else {
|
||||||
|
// JWT token doğrulama
|
||||||
|
return await validateJWTToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Geçersiz authorization formatı. 'Bearer <token>' formatında olmalı.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT token doğrula ve kullanıcı bilgilerini getir
|
||||||
|
*/
|
||||||
|
async function validateJWTToken(token: string): Promise<AuthResult> {
|
||||||
|
const payload = verifyJWT(token);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Geçersiz veya süresi dolmuş token.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcı bilgilerini DB'den al (role için)
|
||||||
|
try {
|
||||||
|
const users = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.id, payload.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Kullanıcı bulunamadı.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = users[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
userId: payload.userId,
|
||||||
|
email: payload.email,
|
||||||
|
role: (userData.role as UserRole) || "user",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("User lookup error:", error);
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Kimlik doğrulama sırasında bir hata oluştu.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key doğrula (veritabanından kontrol)
|
||||||
|
*/
|
||||||
|
async function validateAPIKey(key: string): Promise<AuthResult> {
|
||||||
|
try {
|
||||||
|
const apiKey = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeys)
|
||||||
|
.where(and(eq(apiKeys.key, key), eq(apiKeys.isActive, true)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Geçersiz API key.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyData = apiKey[0];
|
||||||
|
|
||||||
|
// Süre kontrolü
|
||||||
|
if (keyData.expiresAt && keyData.expiresAt < new Date()) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "API key süresi dolmuş.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kullanıcı bilgilerini al
|
||||||
|
const users = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.id, keyData.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Kullanıcı bulunamadı.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = users[0];
|
||||||
|
|
||||||
|
// Son kullanım tarihini güncelle (opsiyonel)
|
||||||
|
await db
|
||||||
|
.update(apiKeys)
|
||||||
|
.set({ lastUsedAt: new Date() })
|
||||||
|
.where(eq(apiKeys.id, keyData.id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
userId: keyData.userId,
|
||||||
|
email: userData.email,
|
||||||
|
role: (userData.role as UserRole) || "user",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API key doğrulama hatası:", error);
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: "Kimlik doğrulama sırasında bir hata oluştu.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/lib/auth.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as schema from "@/db/schema";
|
||||||
|
|
||||||
|
// Validate BETTER_AUTH_SECRET at runtime (not during build)
|
||||||
|
const secret = process.env.BETTER_AUTH_SECRET;
|
||||||
|
if (!secret && process.env.NODE_ENV === "production") {
|
||||||
|
console.warn("WARNING: BETTER_AUTH_SECRET is not set. Authentication will not work properly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "pg",
|
||||||
|
schema: {
|
||||||
|
user: schema.user,
|
||||||
|
session: schema.session,
|
||||||
|
account: schema.account,
|
||||||
|
verification: schema.verification,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
secret: secret || "build-time-secret-key-minimum-32-characters-long-temp",
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
role: {
|
||||||
|
type: "string",
|
||||||
|
defaultValue: "user",
|
||||||
|
required: false,
|
||||||
|
input: false, // Don't allow setting role on signup
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
48
app/lib/jwt.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import jwt, { SignOptions } from "jsonwebtoken";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET || "fallback-secret-key";
|
||||||
|
const API_KEY_PREFIX = "img_";
|
||||||
|
|
||||||
|
export interface JWTPayload {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
type: "access" | "refresh";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT token oluştur
|
||||||
|
* @param payload - Token içeriği
|
||||||
|
* @param expiresIn - Geçerlilik süresi (örn: "7d", "1h")
|
||||||
|
*/
|
||||||
|
export function signJWT(payload: JWTPayload, expiresIn: string | number = "7d"): string {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, { expiresIn } as SignOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT token doğrula
|
||||||
|
* @param token - Doğrulanacak token
|
||||||
|
*/
|
||||||
|
export function verifyJWT(token: string): JWTPayload | null {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||||
|
return decoded;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key oluştur
|
||||||
|
* Formad: img_xxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
*/
|
||||||
|
export function generateAPIKey(): string {
|
||||||
|
return `${API_KEY_PREFIX}${nanoid(32)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key validasyonu
|
||||||
|
*/
|
||||||
|
export function isValidAPIKeyFormat(key: string): boolean {
|
||||||
|
return key.startsWith(API_KEY_PREFIX) && key.length === 36; // img_ + 32 chars
|
||||||
|
}
|
||||||
15
app/lib/next js beter auth yuklu ve drizze orm y.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
next js beter auth yuklu ve drizze orm yuklu posgrsql veritabanı ile entegre edilecek.
|
||||||
|
drizzle orm ile veritabanına bağlanılacak.
|
||||||
|
beter auth ile register yapılacak.
|
||||||
|
beter auth ile giriş yapılacak.
|
||||||
|
giriş yapıldıktan sonra kullanıcının bilgileri veritabanından alınacak.
|
||||||
|
kullanıcının bilgileri veritabanından alındıktan sonra kullanıcının bilgileri sayfada görüntülenecek.
|
||||||
|
birkaç yapilandirma eklendi ama duzgun olmayabilir sen kotrol et ve duzelt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sadece login olmus userlerin giris yapabilecegi bir sayfa olacak. ve sayfada resim dosyalri yuklenecek en boy kalite format vs kullnacini verdigi bilgilere gore
|
||||||
|
resim manipule edilecek ve database ye drizzle orm ile kaydedilecek.
|
||||||
|
resim url si çıkartilarak download edilebilir ve bir buton ile resmin url si kopyalanabilir. olacak ve bu url ile resim indirilebilir.
|
||||||
93
app/lib/permissions.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { db } from "@/db";
|
||||||
|
import { user } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export type UserRole = "user" | "admin" | "moderator";
|
||||||
|
|
||||||
|
// Permission tanımları
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
// Image permissions
|
||||||
|
IMAGE_UPLOAD: "image:upload",
|
||||||
|
IMAGE_DELETE_OWN: "image:delete:own",
|
||||||
|
IMAGE_DELETE_ANY: "image:delete:any",
|
||||||
|
IMAGE_VIEW_OWN: "image:view:own",
|
||||||
|
IMAGE_VIEW_ANY: "image:view:any",
|
||||||
|
|
||||||
|
// User permissions
|
||||||
|
USER_VIEW: "user:view",
|
||||||
|
USER_EDIT: "user:edit",
|
||||||
|
USER_DELETE: "user:delete",
|
||||||
|
USER_MANAGE_ROLES: "user:manage:roles",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Role'lere göre izinler
|
||||||
|
export const ROLE_PERMISSIONS: Record<UserRole, string[]> = {
|
||||||
|
user: [
|
||||||
|
PERMISSIONS.IMAGE_UPLOAD,
|
||||||
|
PERMISSIONS.IMAGE_DELETE_OWN,
|
||||||
|
PERMISSIONS.IMAGE_VIEW_OWN,
|
||||||
|
],
|
||||||
|
moderator: [
|
||||||
|
PERMISSIONS.IMAGE_UPLOAD,
|
||||||
|
PERMISSIONS.IMAGE_DELETE_OWN,
|
||||||
|
PERMISSIONS.IMAGE_VIEW_OWN,
|
||||||
|
PERMISSIONS.IMAGE_VIEW_ANY,
|
||||||
|
PERMISSIONS.USER_VIEW,
|
||||||
|
],
|
||||||
|
admin: Object.values(PERMISSIONS), // Tüm izinler
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının belirli bir role sahip olup olmadığını kontrol eder
|
||||||
|
*/
|
||||||
|
export function hasRole(userRole: UserRole, requiredRole: UserRole | UserRole[]): boolean {
|
||||||
|
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
|
||||||
|
return roles.includes(userRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının belirli bir izne sahip olup olmadığını kontrol eder
|
||||||
|
*/
|
||||||
|
export function hasPermission(userRole: UserRole, permission: string): boolean {
|
||||||
|
const rolePermissions = ROLE_PERMISSIONS[userRole] || [];
|
||||||
|
return rolePermissions.includes(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının birden fazla izne sahip olup olmadığını kontrol eder
|
||||||
|
*/
|
||||||
|
export function hasPermissions(userRole: UserRole, permissions: string[]): boolean {
|
||||||
|
return permissions.every(permission => hasPermission(userRole, permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının en az bir izne sahip olup olmadığını kontrol eder
|
||||||
|
*/
|
||||||
|
export function hasAnyPermission(userRole: UserRole, permissions: string[]): boolean {
|
||||||
|
return permissions.some(permission => hasPermission(userRole, permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının admin olup olmadığını kontrol eder
|
||||||
|
*/
|
||||||
|
export function isAdmin(userRole: UserRole): boolean {
|
||||||
|
return userRole === "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcı bilgilerini userId'den alır
|
||||||
|
*/
|
||||||
|
export async function getUserById(userId: string) {
|
||||||
|
const users = await db.select().from(user).where(eq(user.id, userId)).limit(1);
|
||||||
|
return users[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kullanıcının rolünü günceller (sadece admin yapabilir)
|
||||||
|
*/
|
||||||
|
export async function updateUserRole(userId: string, newRole: UserRole) {
|
||||||
|
await db.update(user).set({
|
||||||
|
role: newRole,
|
||||||
|
updatedAt: new Date()
|
||||||
|
}).where(eq(user.id, userId));
|
||||||
|
}
|
||||||
142
app/login/page.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [registerEnabled, setRegisterEnabled] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkRegisterEnabled = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/config");
|
||||||
|
const data = await response.json();
|
||||||
|
setRegisterEnabled(data.registerEnabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Config kontrolü başarısız:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkRegisterEnabled();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/sign-in/email", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Giriş başarısız");
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/profile");
|
||||||
|
router.refresh();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Bir hata oluştu");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="w-full max-w-md space-y-8 rounded-lg bg-white p-8 shadow-lg dark:bg-zinc-900">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-center text-3xl font-bold text-black dark:text-zinc-50">
|
||||||
|
Giriş Yap
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
E-posta
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="ornek@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Giriş yapılıyor..." : "Giriş Yap"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{registerEnabled && (
|
||||||
|
<div className="text-center text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
Hesabınız yok mu?{" "}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
Kayıt Ol
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
293
app/page.tsx
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [registerEnabled, setRegisterEnabled] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const [authResponse, configResponse] = await Promise.all([
|
||||||
|
fetch("/api/auth/get-session", {
|
||||||
|
credentials: "include",
|
||||||
|
}),
|
||||||
|
fetch("/api/config"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = await authResponse.json();
|
||||||
|
const configData = await configResponse.json();
|
||||||
|
|
||||||
|
setIsAuthenticated(!!authData.user);
|
||||||
|
setRegisterEnabled(configData.registerEnabled);
|
||||||
|
} catch (error) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setRegisterEnabled(true);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-black dark:to-zinc-900">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="h-12 w-12 animate-spin rounded-full border-4 border-blue-200 border-t-blue-600"></div>
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Yükleniyor...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-black dark:to-zinc-900">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div className="w-full max-w-6xl">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="text-center">
|
||||||
|
{/* Icon/Logo */}
|
||||||
|
<div className="mb-8 flex justify-center">
|
||||||
|
<div className="rounded-2xl bg-gradient-to-br from-blue-600 to-purple-600 p-6 shadow-2xl">
|
||||||
|
<svg
|
||||||
|
className="h-16 w-16 text-white"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1 className="mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-5xl font-bold text-transparent dark:from-blue-400 dark:to-purple-400 sm:text-6xl md:text-7xl">
|
||||||
|
Image Manipulation API
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="mx-auto mb-4 max-w-2xl text-xl text-gray-600 dark:text-gray-300">
|
||||||
|
Resimlerinizi yükleyin, boyutlandırın, formatını değiştirin ve
|
||||||
|
istediğiniz kalitede kaydedin.
|
||||||
|
</p>
|
||||||
|
<p className="mx-auto mb-12 max-w-2xl text-lg text-gray-500 dark:text-gray-400">
|
||||||
|
JWT API desteği ile dış uygulamalarınızdan da kullanabilirsiniz.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="mx-auto mb-12 grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-3">
|
||||||
|
<div className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<div className="mb-3 flex justify-center">
|
||||||
|
<div className="rounded-full bg-blue-100 p-3 dark:bg-blue-900/30">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 text-blue-600 dark:text-blue-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-2 font-semibold text-gray-900 dark:text-white">
|
||||||
|
Hızlı İşlem
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Saniyeler içinde resim manipülasyonu
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<div className="mb-3 flex justify-center">
|
||||||
|
<div className="rounded-full bg-purple-100 p-3 dark:bg-purple-900/30">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 text-purple-600 dark:text-purple-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-2 font-semibold text-gray-900 dark:text-white">
|
||||||
|
Çoklu Format
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
JPEG, PNG, WebP, AVIF desteği
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl bg-white p-6 shadow-lg dark:bg-zinc-800/50">
|
||||||
|
<div className="mb-3 flex justify-center">
|
||||||
|
<div className="rounded-full bg-green-100 p-3 dark:bg-green-900/30">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 text-green-600 dark:text-green-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-2 font-semibold text-gray-900 dark:text-white">
|
||||||
|
Güvenli API
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
JWT token ile korumalı erişim
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Buttons */}
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href="/upload"
|
||||||
|
className="group flex h-14 w-full items-center justify-center gap-2 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 px-8 text-lg font-semibold text-white shadow-lg transition-all hover:scale-105 hover:shadow-xl sm:w-auto"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Resim Yükle
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="flex h-14 w-full items-center justify-center gap-2 rounded-full border-2 border-gray-300 bg-white px-8 text-lg font-semibold text-gray-700 transition-all hover:border-gray-400 hover:bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:hover:border-zinc-600 dark:hover:bg-zinc-700 sm:w-auto"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Profilim
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className="flex h-14 w-full items-center justify-center gap-2 rounded-full border-2 border-red-300 bg-white px-8 text-lg font-semibold text-red-700 transition-all hover:border-red-400 hover:bg-red-50 dark:border-red-700 dark:bg-zinc-800 dark:text-red-400 dark:hover:border-red-600 dark:hover:bg-red-900/20 sm:w-auto"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Admin Panel
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="group flex h-14 w-full items-center justify-center gap-2 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 px-8 text-lg font-semibold text-white shadow-lg transition-all hover:scale-105 hover:shadow-xl sm:w-auto"
|
||||||
|
>
|
||||||
|
Giriş Yap
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 transition-transform group-hover:translate-x-1"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
{registerEnabled && (
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="flex h-14 w-full items-center justify-center gap-2 rounded-full border-2 border-gray-300 bg-white px-8 text-lg font-semibold text-gray-700 transition-all hover:border-gray-400 hover:bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:hover:border-zinc-600 dark:hover:bg-zinc-700 sm:w-auto"
|
||||||
|
>
|
||||||
|
Kayıt Ol
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Link */}
|
||||||
|
<div className="mt-12">
|
||||||
|
<Link
|
||||||
|
href="/api-docs"
|
||||||
|
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
API Dokümantasyonu
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
app/profile/page.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
image: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/get-session", {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.user) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(data.user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Kullanıcı bilgileri alınamadı:", error);
|
||||||
|
router.push("/login");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/auth/sign-out", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
router.push("/login");
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Çıkış yapılamadı:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Yükleniyor...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-16">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg dark:bg-zinc-900">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold text-black dark:text-zinc-50">
|
||||||
|
Kullanıcı Bilgileri
|
||||||
|
</h1>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="/upload"
|
||||||
|
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Resim Yükle
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-md bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
Ana Sayfa
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Çıkış Yap
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="rounded-md border border-gray-200 p-6 dark:border-zinc-700">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-black dark:text-zinc-50">
|
||||||
|
Profil Bilgileri
|
||||||
|
</h2>
|
||||||
|
<dl className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Ad Soyad
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-lg text-black dark:text-zinc-50">
|
||||||
|
{user.name}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
E-posta
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-lg text-black dark:text-zinc-50">
|
||||||
|
{user.email}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
E-posta Doğrulandı mı?
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-lg text-black dark:text-zinc-50">
|
||||||
|
{user.emailVerified ? (
|
||||||
|
<span className="text-green-600 dark:text-green-400">
|
||||||
|
Evet
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-red-600 dark:text-red-400">
|
||||||
|
Hayır
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Kullanıcı ID
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||||
|
{user.id}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Kayıt Tarihi
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-lg text-black dark:text-zinc-50">
|
||||||
|
{new Date(user.createdAt).toLocaleString("tr-TR")}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Son Güncelleme
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-lg text-black dark:text-zinc-50">
|
||||||
|
{new Date(user.updatedAt).toLocaleString("tr-TR")}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
211
app/register/page.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [registerEnabled, setRegisterEnabled] = useState(true);
|
||||||
|
const [checking, setChecking] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkRegisterEnabled = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/config");
|
||||||
|
const data = await response.json();
|
||||||
|
setRegisterEnabled(data.registerEnabled);
|
||||||
|
if (!data.registerEnabled) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Config kontrolü başarısız:", error);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkRegisterEnabled();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
// Şifre kontrolü
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Şifreler eşleşmiyor");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError("Şifre en az 6 karakter olmalıdır");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/sign-up/email", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Kayıt başarısız");
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/profile");
|
||||||
|
router.refresh();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Bir hata oluştu");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (checking) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Yükleniyor...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registerEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="w-full max-w-md space-y-8 rounded-lg bg-white p-8 shadow-lg dark:bg-zinc-900">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-center text-3xl font-bold text-black dark:text-zinc-50">
|
||||||
|
Kayıt Ol
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Ad Soyad
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="Adınız Soyadınız"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
E-posta
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="ornek@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Şifre Tekrar
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Kayıt yapılıyor..." : "Kayıt Ol"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
Zaten hesabınız var mı?{" "}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
Giriş Yap
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
415
app/upload/page.tsx
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
id: string;
|
||||||
|
originalName: string;
|
||||||
|
url: string;
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
|
quality: number | null;
|
||||||
|
format: string;
|
||||||
|
fileSize: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UploadPage() {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [width, setWidth] = useState<number>(800);
|
||||||
|
const [height, setHeight] = useState<number>(600);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFile = e.target.files?.[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
|
||||||
|
// Resmin boyutlarını al
|
||||||
|
const img = new Image();
|
||||||
|
const objectUrl = URL.createObjectURL(selectedFile);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
setWidth(img.naturalWidth);
|
||||||
|
setHeight(img.naturalHeight);
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = objectUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const [quality, setQuality] = useState<number>(90);
|
||||||
|
const [format, setFormat] = useState<string>("avif");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [success, setSuccess] = useState("");
|
||||||
|
const [images, setImages] = useState<Image[]>([]);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/get-session", {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.user) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
loadImages();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auth kontrolü başarısız:", error);
|
||||||
|
router.push("/login");
|
||||||
|
} finally {
|
||||||
|
setCheckingAuth(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const loadImages = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/images", {
|
||||||
|
credentials: "include",
|
||||||
|
cache: 'no-store', // Cache'i devre dışı bırak
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Yüklenen resimler:", data.images?.length || 0);
|
||||||
|
setImages(data.images || []);
|
||||||
|
} else {
|
||||||
|
console.error("Resimler yüklenemedi, status:", response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Resimler yüklenemedi:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
setError("Lütfen bir dosya seçin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("width", width.toString());
|
||||||
|
formData.append("height", height.toString());
|
||||||
|
formData.append("quality", quality.toString());
|
||||||
|
formData.append("format", format);
|
||||||
|
|
||||||
|
const response = await fetch("/api/images/upload", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Yükleme başarısız");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formu tamamen resetle
|
||||||
|
setSuccess("Resim başarıyla yüklendi!");
|
||||||
|
setFile(null);
|
||||||
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resimleri yeniden yükle
|
||||||
|
await loadImages();
|
||||||
|
|
||||||
|
// Success mesajını kısa süre sonra temizle
|
||||||
|
setTimeout(() => setSuccess(""), 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Bir hata oluştu");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (url: string) => {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
setSuccess("URL kopyalandı!");
|
||||||
|
setTimeout(() => setSuccess(""), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadImage = (url: string, originalName: string) => {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = originalName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (imageId: string, originalName: string) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: "Emin misiniz?",
|
||||||
|
text: `${originalName} adlı resmi silmek istediğinize emin misiniz?`,
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
cancelButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "Evet, Sil!",
|
||||||
|
cancelButtonText: "İptal",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
console.log("Silme isteği gönderiliyor, imageId:", imageId);
|
||||||
|
console.log("Image objesi:", images.find(img => img.id === imageId));
|
||||||
|
const response = await fetch(`/api/images/${encodeURIComponent(imageId)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Silme hatası:", data);
|
||||||
|
throw new Error(data.message || "Silme işlemi başarısız");
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire("Silindi!", "Resim başarıyla silindi.", "success");
|
||||||
|
loadImages();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Silme hatası:", error);
|
||||||
|
Swal.fire("Hata!", error.message || "Resim silinirken bir hata oluştu.", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (checkingAuth) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Yükleniyor...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-8">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold text-black dark:text-zinc-50">
|
||||||
|
Resim Yükle ve Manipüle Et
|
||||||
|
</h1>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="rounded-md bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
Profil
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Ana Sayfa
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
|
{/* Upload Form */}
|
||||||
|
<div className="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900 lg:col-span-1">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-black dark:text-zinc-50">
|
||||||
|
Resim Yükle
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="rounded-md bg-green-50 p-3 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Resim Dosyası
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Genişlik (px)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={width}
|
||||||
|
onChange={(e) => setWidth(parseInt(e.target.value) || 800)}
|
||||||
|
min="1"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Yükseklik (px)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={height}
|
||||||
|
onChange={(e) => setHeight(parseInt(e.target.value) || 600)}
|
||||||
|
min="1"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Kalite (1-100)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={quality}
|
||||||
|
onChange={(e) => setQuality(parseInt(e.target.value) || 90)}
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Format
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={format}
|
||||||
|
onChange={(e) => setFormat(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-zinc-800 dark:text-white"
|
||||||
|
>
|
||||||
|
<option value="avif">AVIF</option>
|
||||||
|
<option value="jpeg">JPEG</option>
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="webp">WebP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Yükleniyor..." : "Resmi Yükle ve İşle"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Images List */}
|
||||||
|
<div className="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900 lg:col-span-2">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-black dark:text-zinc-50">
|
||||||
|
Yüklenen Resimler ({images.length})
|
||||||
|
</h2>
|
||||||
|
{images.length === 0 ? (
|
||||||
|
<div className="flex h-64 items-center justify-center rounded-lg border-2 border-dashed border-gray-300 dark:border-zinc-700">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Henüz resim yüklenmedi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{images.map((image) => (
|
||||||
|
<div
|
||||||
|
key={image.id}
|
||||||
|
className="group relative overflow-hidden rounded-lg border border-gray-200 bg-white shadow-md transition-shadow hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<div className="relative aspect-square overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={image.url}
|
||||||
|
alt={image.originalName}
|
||||||
|
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/10" />
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="mb-2 truncate text-sm font-semibold text-gray-900 dark:text-zinc-50">
|
||||||
|
{image.originalName}
|
||||||
|
</h3>
|
||||||
|
<div className="mb-3 space-y-1 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Boyut:</span> {image.width} × {image.height} px
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Format:</span> {image.format.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Dosya:</span> {Math.round(image.fileSize / 1024)} KB
|
||||||
|
</p>
|
||||||
|
{image.quality && (
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Kalite:</span> {image.quality}%
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(image.url)}
|
||||||
|
className="rounded-md bg-green-600 px-3 py-2 text-xs font-medium text-white transition-colors hover:bg-green-700"
|
||||||
|
title="URL'yi Kopyala"
|
||||||
|
>
|
||||||
|
Kopyala
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => downloadImage(image.url, image.originalName)}
|
||||||
|
className="rounded-md bg-blue-600 px-3 py-2 text-xs font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
title="İndir"
|
||||||
|
>
|
||||||
|
İndir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(image.id, image.originalName)}
|
||||||
|
className="mt-2 w-full rounded-md bg-red-600 px-3 py-2 text-xs font-medium text-white transition-colors hover:bg-red-700"
|
||||||
|
title="Sil"
|
||||||
|
>
|
||||||
|
Sil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
app/uploads/[...path]/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ path: string[] }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { path } = await params;
|
||||||
|
const filePath = join(process.cwd(), "public", "uploads", ...path);
|
||||||
|
const fileBuffer = await readFile(filePath);
|
||||||
|
|
||||||
|
// Dosya uzantısına göre content-type belirle
|
||||||
|
const fileName = path[path.length - 1];
|
||||||
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
|
const contentTypeMap: Record<string, string> = {
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'webp': 'image/webp',
|
||||||
|
'avif': 'image/avif',
|
||||||
|
'svg': 'image/svg+xml',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentType = contentTypeMap[ext || ''] || 'application/octet-stream';
|
||||||
|
|
||||||
|
return new NextResponse(fileBuffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dosya okuma hatası:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Dosya bulunamadı" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
db.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// Create pool - DATABASE_URL check will happen at runtime when pool is actually used
|
||||||
|
// During build time, this won't fail even if DATABASE_URL is not set
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL || "postgresql://localhost:5432/temp",
|
||||||
|
max: 20,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
ssl: process.env.DATABASE_SSL === "true" ? { rejectUnauthorized: false } : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test connection on startup
|
||||||
|
pool.on("error", (err) => {
|
||||||
|
console.error("Unexpected error on idle client", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate DATABASE_URL at runtime (not during build)
|
||||||
|
if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL) {
|
||||||
|
console.warn("WARNING: DATABASE_URL is not set. Database operations will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = drizzle(pool);
|
||||||
90
db/schema.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
// Roles enum
|
||||||
|
export const roleEnum = ["user", "admin", "moderator"] as const;
|
||||||
|
export type UserRole = typeof roleEnum[number];
|
||||||
|
|
||||||
|
// Better-auth requires these tables
|
||||||
|
export const user = pgTable("user", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||||
|
image: text("image"),
|
||||||
|
role: text("role").notNull().default("user"), // user, admin, moderator
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const session = pgTable("session", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
expiresAt: timestamp("expiresAt").notNull(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
ipAddress: text("ipAddress"),
|
||||||
|
userAgent: text("userAgent"),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const account = pgTable("account", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
accountId: text("accountId").notNull(),
|
||||||
|
providerId: text("providerId").notNull(),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
accessToken: text("accessToken"),
|
||||||
|
refreshToken: text("refreshToken"),
|
||||||
|
idToken: text("idToken"),
|
||||||
|
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
|
||||||
|
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
||||||
|
scope: text("scope"),
|
||||||
|
password: text("password"),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verification = pgTable("verification", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
expiresAt: timestamp("expiresAt").notNull(),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const images = pgTable("images", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
originalName: text("originalName").notNull(),
|
||||||
|
fileName: text("fileName").notNull(),
|
||||||
|
filePath: text("filePath").notNull(),
|
||||||
|
url: text("url").notNull(),
|
||||||
|
width: integer("width"),
|
||||||
|
height: integer("height"),
|
||||||
|
quality: integer("quality"),
|
||||||
|
format: text("format").notNull(),
|
||||||
|
fileSize: integer("fileSize").notNull(),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// API Keys for external applications
|
||||||
|
export const apiKeys = pgTable("apiKeys", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(), // API key ismi (örn: "Mobile App", "Dashboard")
|
||||||
|
key: text("key").notNull().unique(), // API key
|
||||||
|
lastUsedAt: timestamp("lastUsedAt"),
|
||||||
|
expiresAt: timestamp("expiresAt"), // null = süresiz
|
||||||
|
isActive: boolean("isActive").notNull().default(true),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
image-apiv2:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: image-apiv2
|
||||||
|
user: "1001:1001" # nextjs user (uid:gid)
|
||||||
|
ports:
|
||||||
|
- "3151:3000"
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||||
|
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:3000}
|
||||||
|
- REGISTER_ENABLE=${REGISTER_ENABLE:-true}
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
# Uploads klasörünü persist et
|
||||||
|
- uploads_data:/app/public/uploads
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
uploads_data:
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
|
# Opsiyonel: PostgreSQL ile birlikte kullanmak için
|
||||||
|
# depends_on:
|
||||||
|
# - postgres
|
||||||
|
|
||||||
|
# Opsiyonel: PostgreSQL veritabanı
|
||||||
|
# postgres:
|
||||||
|
# image: postgres:16-alpine
|
||||||
|
# container_name: image-api-db
|
||||||
|
# environment:
|
||||||
|
# - POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||||
|
# - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
||||||
|
# - POSTGRES_DB=${POSTGRES_DB:-image_api}
|
||||||
|
# volumes:
|
||||||
|
# - postgres_data:/var/lib/postgresql/data
|
||||||
|
# ports:
|
||||||
|
# - "5432:5432"
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
# volumes:
|
||||||
|
# postgres_data:
|
||||||
9
docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Uploads klasörünü oluştur ve izinleri ayarla (volume mount için)
|
||||||
|
mkdir -p /app/public/uploads
|
||||||
|
chmod -R 775 /app/public/uploads
|
||||||
|
|
||||||
|
# Uygulamayı başlat
|
||||||
|
exec node server.js
|
||||||
13
drizzle.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./db/schema.ts",
|
||||||
|
out: "./drizzle",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
});
|
||||||
81
drizzle/0000_perpetual_alice.sql
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
CREATE TABLE "account" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"accountId" text NOT NULL,
|
||||||
|
"providerId" text NOT NULL,
|
||||||
|
"userId" text NOT NULL,
|
||||||
|
"accessToken" text,
|
||||||
|
"refreshToken" text,
|
||||||
|
"idToken" text,
|
||||||
|
"accessTokenExpiresAt" timestamp,
|
||||||
|
"refreshTokenExpiresAt" timestamp,
|
||||||
|
"scope" text,
|
||||||
|
"password" text,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "apiKeys" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"userId" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"lastUsedAt" timestamp,
|
||||||
|
"expiresAt" timestamp,
|
||||||
|
"isActive" boolean DEFAULT true NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "apiKeys_key_unique" UNIQUE("key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "images" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"userId" text NOT NULL,
|
||||||
|
"originalName" text NOT NULL,
|
||||||
|
"fileName" text NOT NULL,
|
||||||
|
"filePath" text NOT NULL,
|
||||||
|
"url" text NOT NULL,
|
||||||
|
"width" integer,
|
||||||
|
"height" integer,
|
||||||
|
"quality" integer,
|
||||||
|
"format" text NOT NULL,
|
||||||
|
"fileSize" integer NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "session" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"expiresAt" timestamp NOT NULL,
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"ipAddress" text,
|
||||||
|
"userAgent" text,
|
||||||
|
"userId" text NOT NULL,
|
||||||
|
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"emailVerified" boolean DEFAULT false NOT NULL,
|
||||||
|
"image" text,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "verification" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"identifier" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
"expiresAt" timestamp NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apiKeys" ADD CONSTRAINT "apiKeys_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "images" ADD CONSTRAINT "images_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
1
drizzle/0001_harsh_morbius.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
|
||||||
527
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
{
|
||||||
|
"id": "e57a66f4-9087-4f42-843d-86044c7d703b",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"name": "accountId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"providerId": {
|
||||||
|
"name": "providerId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"accessToken": {
|
||||||
|
"name": "accessToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refreshToken": {
|
||||||
|
"name": "refreshToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"idToken": {
|
||||||
|
"name": "idToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"accessTokenExpiresAt": {
|
||||||
|
"name": "accessTokenExpiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refreshTokenExpiresAt": {
|
||||||
|
"name": "refreshTokenExpiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_userId_user_id_fk": {
|
||||||
|
"name": "account_userId_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.apiKeys": {
|
||||||
|
"name": "apiKeys",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"lastUsedAt": {
|
||||||
|
"name": "lastUsedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"isActive": {
|
||||||
|
"name": "isActive",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apiKeys_userId_user_id_fk": {
|
||||||
|
"name": "apiKeys_userId_user_id_fk",
|
||||||
|
"tableFrom": "apiKeys",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"apiKeys_key_unique": {
|
||||||
|
"name": "apiKeys_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.images": {
|
||||||
|
"name": "images",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"originalName": {
|
||||||
|
"name": "originalName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"name": "fileName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"filePath": {
|
||||||
|
"name": "filePath",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"name": "width",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"name": "quality",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"name": "format",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fileSize": {
|
||||||
|
"name": "fileSize",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"images_userId_user_id_fk": {
|
||||||
|
"name": "images_userId_user_id_fk",
|
||||||
|
"tableFrom": "images",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"ipAddress": {
|
||||||
|
"name": "ipAddress",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"name": "userAgent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_userId_user_id_fk": {
|
||||||
|
"name": "session_userId_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"emailVerified": {
|
||||||
|
"name": "emailVerified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
534
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
{
|
||||||
|
"id": "fc4f3c3d-f7b9-4e17-b7dc-88e2944f9303",
|
||||||
|
"prevId": "e57a66f4-9087-4f42-843d-86044c7d703b",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"name": "accountId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"providerId": {
|
||||||
|
"name": "providerId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"accessToken": {
|
||||||
|
"name": "accessToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refreshToken": {
|
||||||
|
"name": "refreshToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"idToken": {
|
||||||
|
"name": "idToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"accessTokenExpiresAt": {
|
||||||
|
"name": "accessTokenExpiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refreshTokenExpiresAt": {
|
||||||
|
"name": "refreshTokenExpiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_userId_user_id_fk": {
|
||||||
|
"name": "account_userId_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.apiKeys": {
|
||||||
|
"name": "apiKeys",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"lastUsedAt": {
|
||||||
|
"name": "lastUsedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"isActive": {
|
||||||
|
"name": "isActive",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apiKeys_userId_user_id_fk": {
|
||||||
|
"name": "apiKeys_userId_user_id_fk",
|
||||||
|
"tableFrom": "apiKeys",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"apiKeys_key_unique": {
|
||||||
|
"name": "apiKeys_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.images": {
|
||||||
|
"name": "images",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"originalName": {
|
||||||
|
"name": "originalName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"name": "fileName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"filePath": {
|
||||||
|
"name": "filePath",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"name": "width",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"name": "quality",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"name": "format",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fileSize": {
|
||||||
|
"name": "fileSize",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"images_userId_user_id_fk": {
|
||||||
|
"name": "images_userId_user_id_fk",
|
||||||
|
"tableFrom": "images",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"ipAddress": {
|
||||||
|
"name": "ipAddress",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"name": "userAgent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_userId_user_id_fk": {
|
||||||
|
"name": "session_userId_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"emailVerified": {
|
||||||
|
"name": "emailVerified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767680035227,
|
||||||
|
"tag": "0000_perpetual_alice",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767682768043,
|
||||||
|
"tag": "0001_harsh_morbius",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
BIN
image_api.sql
Normal file
35
make-admin.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Admin kullanıcı oluşturma scripti
|
||||||
|
import { db } from "./db";
|
||||||
|
import { user } from "./db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
async function makeAdmin() {
|
||||||
|
const email = process.argv[2];
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
console.error("Kullanım: tsx make-admin.mjs email@example.com");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db
|
||||||
|
.update(user)
|
||||||
|
.set({ role: "admin" })
|
||||||
|
.where(eq(user.email, email))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
console.error(`❌ ${email} bulunamadı`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ ${email} artık admin!`);
|
||||||
|
console.log(result[0]);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Hata:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeAdmin();
|
||||||
115
nemoriebank.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Cline's Memory Bank
|
||||||
|
|
||||||
|
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||||
|
|
||||||
|
## Memory Bank Structure
|
||||||
|
|
||||||
|
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
PB[projectbrief.md] --> PC[productContext.md]
|
||||||
|
PB --> SP[systemPatterns.md]
|
||||||
|
PB --> TC[techContext.md]
|
||||||
|
|
||||||
|
PC --> AC[activeContext.md]
|
||||||
|
SP --> AC
|
||||||
|
TC --> AC
|
||||||
|
|
||||||
|
AC --> P[progress.md]
|
||||||
|
|
||||||
|
### Core Files (Required)
|
||||||
|
1. `projectbrief.md`
|
||||||
|
- Foundation document that shapes all other files
|
||||||
|
- Created at project start if it doesn't exist
|
||||||
|
- Defines core requirements and goals
|
||||||
|
- Source of truth for project scope
|
||||||
|
|
||||||
|
2. `productContext.md`
|
||||||
|
- Why this project exists
|
||||||
|
- Problems it solves
|
||||||
|
- How it should work
|
||||||
|
- User experience goals
|
||||||
|
|
||||||
|
3. `activeContext.md`
|
||||||
|
- Current work focus
|
||||||
|
- Recent changes
|
||||||
|
- Next steps
|
||||||
|
- Active decisions and considerations
|
||||||
|
- Important patterns and preferences
|
||||||
|
- Learnings and project insights
|
||||||
|
|
||||||
|
4. `systemPatterns.md`
|
||||||
|
- System architecture
|
||||||
|
- Key technical decisions
|
||||||
|
- Design patterns in use
|
||||||
|
- Component relationships
|
||||||
|
- Critical implementation paths
|
||||||
|
|
||||||
|
5. `techContext.md`
|
||||||
|
- Technologies used
|
||||||
|
- Development setup
|
||||||
|
- Technical constraints
|
||||||
|
- Dependencies
|
||||||
|
- Tool usage patterns
|
||||||
|
|
||||||
|
6. `progress.md`
|
||||||
|
- What works
|
||||||
|
- What's left to build
|
||||||
|
- Current status
|
||||||
|
- Known issues
|
||||||
|
- Evolution of project decisions
|
||||||
|
|
||||||
|
### Additional Context
|
||||||
|
Create additional files/folders within memory-bank/ when they help organize:
|
||||||
|
- Complex feature documentation
|
||||||
|
- Integration specifications
|
||||||
|
- API documentation
|
||||||
|
- Testing strategies
|
||||||
|
- Deployment procedures
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
### Plan Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||||
|
ReadFiles --> CheckFiles{Files Complete?}
|
||||||
|
|
||||||
|
CheckFiles -->|No| Plan[Create Plan]
|
||||||
|
Plan --> Document[Document in Chat]
|
||||||
|
|
||||||
|
CheckFiles -->|Yes| Verify[Verify Context]
|
||||||
|
Verify --> Strategy[Develop Strategy]
|
||||||
|
Strategy --> Present[Present Approach]
|
||||||
|
|
||||||
|
### Act Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> Context[Check Memory Bank]
|
||||||
|
Context --> Update[Update Documentation]
|
||||||
|
Update --> Execute[Execute Task]
|
||||||
|
Execute --> Document[Document Changes]
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
Memory Bank updates occur when:
|
||||||
|
1. Discovering new project patterns
|
||||||
|
2. After implementing significant changes
|
||||||
|
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||||
|
4. When context needs clarification
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
Start[Update Process]
|
||||||
|
|
||||||
|
subgraph Process
|
||||||
|
P1[Review ALL Files]
|
||||||
|
P2[Document Current State]
|
||||||
|
P3[Clarify Next Steps]
|
||||||
|
P4[Document Insights & Patterns]
|
||||||
|
|
||||||
|
P1 --> P2 --> P3 --> P4
|
||||||
|
end
|
||||||
|
|
||||||
|
Start --> Process
|
||||||
|
|
||||||
|
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||||
|
|
||||||
|
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||||
44
next.config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-DNS-Prefetch-Control",
|
||||||
|
value: "on",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Strict-Transport-Security",
|
||||||
|
value: "max-age=63072000; includeSubDomains; preload",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Frame-Options",
|
||||||
|
value: "SAMEORIGIN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-XSS-Protection",
|
||||||
|
value: "1; mode=block",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Referrer-Policy",
|
||||||
|
value: "strict-origin-when-cross-origin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Permissions-Policy",
|
||||||
|
value: "camera=(), microphone=(), geolocation=()",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "image-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"better-auth": "^1.4.10",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"next": "16.1.1",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"sweetalert2": "^11.26.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"drizzle-kit": "^0.31.8",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.1",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/uploads/G2IZKMVncbgNhi9rU1bon.avif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/uploads/UTwr4wJNmDaIHgmovLogH.avif
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/uploads/_u1OsfM__88agO_goDnRz.jpg
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
public/uploads/diQ-g_qzoYCI4GuOiNny6.avif
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
public/uploads/kGGOVikvdoUO7F7ci0MLx.avif
Normal file
|
After Width: | Height: | Size: 43 KiB |
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
3
sadece login olmus userlerin giris yapab.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
sadece login olmus userlerin giris yapabilecegi bir sayfa olacak. ve sayfada resim dosyalri yuklenecek en boy kalite format vs kullnacini verdigi bilgilere gore
|
||||||
|
resim manipule edilecek ve database ye drizzle orm ile kaydedilecek.
|
||||||
|
resim url si çıkartilarak download edilebilir ve bir buton ile resmin url si kopyalanabilir. olacak ve bu url ile resim indirilebilir.
|
||||||
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||