first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:11:03 +03:00
commit 031582ea2c
98 changed files with 13281 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { and, eq } from "drizzle-orm";
import { authenticateWebOrAPIRequest } from "@/app/lib/api-auth";
import { isAdmin } from "@/app/lib/permissions";
import { maskApiKey } from "@/app/lib/jwt";
import {
expiresAtFromDays,
getDaysRemaining,
getExpiryRemainingLabel,
parseExpiresInDaysOptional,
} from "@/app/lib/api-key-utils";
/**
* PATCH /api/v1/admin/users/[id]/api-keys/[keyId]
*
* Body: { "expiresInDays": number | null }
* — null veya 0: süresiz; 13650: bugünden itibaren o kadar gün sonra sona erer
*/
export async function PATCH(
request: NextRequest,
context: { params: Promise<{ id: string; keyId: string }> }
) {
const auth = await authenticateWebOrAPIRequest(request);
if (!auth.authenticated) {
return NextResponse.json({ error: auth.error ?? "Yetkisiz" }, { status: 401 });
}
if (!isAdmin(auth.role!)) {
return NextResponse.json(
{ error: "Bu işlem için admin yetkisi gerekir." },
{ status: 403 }
);
}
const { id: userId, keyId } = await context.params;
let body: { expiresInDays?: unknown };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Geçersiz JSON" }, { status: 400 });
}
const parsed = parseExpiresInDaysOptional(body.expiresInDays);
if (!parsed.ok) {
return NextResponse.json({ error: parsed.error }, { status: 400 });
}
const expiresAt =
parsed.value === null ? null : expiresAtFromDays(parsed.value);
const updated = await db
.update(apiKeys)
.set({ expiresAt, updatedAt: new Date() })
.where(and(eq(apiKeys.id, keyId), eq(apiKeys.userId, userId)))
.returning({
id: apiKeys.id,
name: apiKeys.name,
key: apiKeys.key,
expiresAt: apiKeys.expiresAt,
isActive: apiKeys.isActive,
});
if (updated.length === 0) {
return NextResponse.json(
{ error: "Anahtar bulunamadı veya bu kullanıcıya ait değil." },
{ status: 404 }
);
}
const r = updated[0];
const exp = r.expiresAt ?? null;
return NextResponse.json({
success: true,
message: "API anahtarı süresi güncellendi.",
data: {
id: r.id,
name: r.name,
keyPreview: maskApiKey(r.key),
expiresAt: exp?.toISOString() ?? null,
daysRemaining: getDaysRemaining(exp),
remainingLabel: getExpiryRemainingLabel(exp),
isActive: r.isActive,
},
});
}

View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
import { authenticateWebOrAPIRequest } from "@/app/lib/api-auth";
import { isAdmin } from "@/app/lib/permissions";
import { maskApiKey } from "@/app/lib/jwt";
import { getDaysRemaining, getExpiryRemainingLabel } from "@/app/lib/api-key-utils";
/**
* GET /api/v1/admin/users/[id]/api-keys — Admin: seçilen kullanıcının API anahtarları
*/
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const auth = await authenticateWebOrAPIRequest(request);
if (!auth.authenticated) {
return NextResponse.json({ error: auth.error ?? "Yetkisiz" }, { status: 401 });
}
if (!isAdmin(auth.role!)) {
return NextResponse.json(
{ error: "Bu işlem için admin yetkisi gerekir." },
{ status: 403 }
);
}
const { id: userId } = await context.params;
const rows = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
key: apiKeys.key,
expiresAt: apiKeys.expiresAt,
lastUsedAt: apiKeys.lastUsedAt,
isActive: apiKeys.isActive,
createdAt: apiKeys.createdAt,
})
.from(apiKeys)
.where(eq(apiKeys.userId, userId))
.orderBy(desc(apiKeys.createdAt));
return NextResponse.json({
success: true,
data: {
keys: rows.map((r) => {
const exp = r.expiresAt ?? null;
return {
id: r.id,
name: r.name,
keyPreview: maskApiKey(r.key),
expiresAt: exp?.toISOString() ?? null,
daysRemaining: getDaysRemaining(exp),
remainingLabel: getExpiryRemainingLabel(exp),
lastUsedAt: r.lastUsedAt?.toISOString() ?? null,
isActive: r.isActive,
createdAt: r.createdAt.toISOString(),
};
}),
},
});
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View File

@@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { and, eq } from "drizzle-orm";
import { authenticateWebOrAPIRequest } from "@/app/lib/api-auth";
/**
* DELETE /api/v1/api-keys/[id] — Kendi anahtarını iptal et (isActive: false)
*/
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const auth = await authenticateWebOrAPIRequest(request);
if (!auth.authenticated || !auth.userId) {
return NextResponse.json({ error: auth.error ?? "Yetkisiz" }, { status: 401 });
}
const { id } = await context.params;
const updated = await db
.update(apiKeys)
.set({ isActive: false, updatedAt: new Date() })
.where(and(eq(apiKeys.id, id), eq(apiKeys.userId, auth.userId)))
.returning({ id: apiKeys.id });
if (updated.length === 0) {
return NextResponse.json(
{ error: "Anahtar bulunamadı veya size ait değil." },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
message: "API anahtarı iptal edildi.",
});
}

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from "next/server";
import { nanoid } from "nanoid";
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
import { authenticateWebOrAPIRequest } from "@/app/lib/api-auth";
import { generateAPIKey, maskApiKey } from "@/app/lib/jwt";
import {
MAX_API_KEY_NAME_LEN,
expiresAtFromDays,
getDaysRemaining,
getExpiryRemainingLabel,
parseExpiresInDaysOptional,
} from "@/app/lib/api-key-utils";
/**
* GET /api/v1/api-keys — Oturum veya Bearer ile: kendi API anahtarlarını listele (tam key dönmez)
*/
export async function GET(request: NextRequest) {
const auth = await authenticateWebOrAPIRequest(request);
if (!auth.authenticated || !auth.userId) {
return NextResponse.json({ error: auth.error ?? "Yetkisiz" }, { status: 401 });
}
const rows = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
key: apiKeys.key,
expiresAt: apiKeys.expiresAt,
lastUsedAt: apiKeys.lastUsedAt,
isActive: apiKeys.isActive,
createdAt: apiKeys.createdAt,
})
.from(apiKeys)
.where(eq(apiKeys.userId, auth.userId))
.orderBy(desc(apiKeys.createdAt));
return NextResponse.json({
success: true,
data: {
keys: rows.map((r) => {
const exp = r.expiresAt ?? null;
return {
id: r.id,
name: r.name,
keyPreview: maskApiKey(r.key),
expiresAt: exp?.toISOString() ?? null,
daysRemaining: getDaysRemaining(exp),
remainingLabel: getExpiryRemainingLabel(exp),
lastUsedAt: r.lastUsedAt?.toISOString() ?? null,
isActive: r.isActive,
createdAt: r.createdAt.toISOString(),
};
}),
},
});
}
/**
* POST /api/v1/api-keys — Yeni API anahtarı oluştur (tam key yalnızca bu yanıtta bir kez)
*
* Body: { "name": string, "expiresInDays"?: number | null }
* — expiresInDays yok/null/0: süresiz; 13650: bugünden itibaren o kadar gün
*/
export async function POST(request: NextRequest) {
const auth = await authenticateWebOrAPIRequest(request);
if (!auth.authenticated || !auth.userId) {
return NextResponse.json({ error: auth.error ?? "Yetkisiz" }, { status: 401 });
}
let body: { name?: unknown; expiresInDays?: unknown };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Geçersiz JSON" }, { status: 400 });
}
const name = typeof body.name === "string" ? body.name.trim() : "";
if (!name || name.length > MAX_API_KEY_NAME_LEN) {
return NextResponse.json(
{
error: `name zorunludur ve en fazla ${MAX_API_KEY_NAME_LEN} karakter olabilir.`,
},
{ status: 400 }
);
}
const parsed = parseExpiresInDaysOptional(body.expiresInDays);
if (!parsed.ok) {
return NextResponse.json({ error: parsed.error }, { status: 400 });
}
const expiresAt =
parsed.value === null ? null : expiresAtFromDays(parsed.value);
const plainKey = generateAPIKey();
const id = nanoid();
await db.insert(apiKeys).values({
id,
userId: auth.userId,
name,
key: plainKey,
expiresAt,
isActive: true,
});
return NextResponse.json({
success: true,
message:
"API anahtarı oluşturuldu. Tam değeri yalnızca bu yanıtta saklayın; bir daha gösterilmez.",
data: {
id,
name,
key: plainKey,
expiresAt: expiresAt?.toISOString() ?? null,
daysRemaining: getDaysRemaining(expiresAt),
remainingLabel: getExpiryRemainingLabel(expiresAt),
},
});
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 { deleteFromR2 } from "@/app/lib/r2-storage";
/**
* 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 }
);
}
// R2'den dosyayı sil
try {
await deleteFromR2(image.fileName);
} catch (fileError) {
console.error("R2'den silme hatası:", 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 }
);
}
}

View File

@@ -0,0 +1,67 @@
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));
}
return NextResponse.json({
success: true,
data: {
images: userImages.map((img) => ({
id: img.id,
originalName: img.originalName,
url: img.url, // R2 URL'leri zaten tam URL olarak kaydedildi
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 }
);
}
}

View File

@@ -0,0 +1,151 @@
import { NextRequest, NextResponse } from "next/server";
import { authenticateAPIRequest } from "@/app/lib/api-auth";
import sharp from "sharp";
import { db } from "@/db";
import { images } from "@/db/schema";
import { nanoid } from "nanoid";
import { uploadToR2, getContentType } from "@/app/lib/r2-storage";
/**
* 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}`;
// R2'ye yükle
const contentType = getContentType(fileExtension);
const r2Url = await uploadToR2({
buffer: processedImage,
fileName,
contentType,
});
// Veritabanına kaydet
const imageId = nanoid();
await db.insert(images).values({
id: imageId,
userId: auth.userId!,
originalName,
fileName,
filePath: fileName, // R2'de sadece fileName yeterli
url: r2Url, // R2'nin tam URL'si
width: metadata.width || null,
height: metadata.height || null,
quality,
format: normalizedFormat,
fileSize: processedImage.length,
});
const fullImageUrl = r2Url;
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 }
);
}
}