first commit
This commit is contained in:
80
app/api/v1/images/[id]/route.ts
Normal file
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
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
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;
|
||||
}
|
||||
Reference in New Issue
Block a user