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

619
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,619 @@
"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 AdminApiKeyRow {
id: string;
name: string;
keyPreview: string;
expiresAt: string | null;
daysRemaining: number | null;
remainingLabel: string;
lastUsedAt: string | null;
isActive: 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>("");
const [keysModalUser, setKeysModalUser] = useState<User | null>(null);
const [userKeys, setUserKeys] = useState<AdminApiKeyRow[]>([]);
const [keysLoading, setKeysLoading] = useState(false);
const [keysPatching, setKeysPatching] = useState<string | null>(null);
const [expiryDraft, setExpiryDraft] = useState<Record<string, 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 fetchUserKeysList = async (userId: string) => {
setKeysLoading(true);
try {
const res = await fetch(`/api/v1/admin/users/${userId}/api-keys`, {
credentials: "include",
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Anahtarlar yüklenemedi");
}
setUserKeys(data.data?.keys || []);
} finally {
setKeysLoading(false);
}
};
const openApiKeysModal = async (u: User) => {
setKeysModalUser(u);
setUserKeys([]);
setExpiryDraft({});
try {
await fetchUserKeysList(u.id);
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
setKeysModalUser(null);
}
};
const patchKeyExpiry = async (userId: string, keyId: string, draft: string) => {
let expiresInDays: number | null;
const t = draft.trim();
if (t === "") {
expiresInDays = null;
} else {
const n = parseInt(t, 10);
if (!Number.isFinite(n) || n < 0) {
await Swal.fire({
icon: "warning",
title: "Geçersiz",
text: "Gün sayısı boş (süresiz) veya 0 veya pozitif tam sayı olmalı.",
confirmButtonColor: "#3b82f6",
});
return;
}
expiresInDays = n === 0 ? null : n;
}
setKeysPatching(keyId);
try {
const res = await fetch(
`/api/v1/admin/users/${userId}/api-keys/${keyId}`,
{
method: "PATCH",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ expiresInDays }),
}
);
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Güncellenemedi");
}
await Swal.fire({
icon: "success",
title: "Güncellendi",
text: "API anahtarı süresi kaydedildi.",
timer: 1800,
showConfirmButton: false,
});
setExpiryDraft((prev) => {
const next = { ...prev };
delete next[keyId];
return next;
});
await fetchUserKeysList(userId);
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
} finally {
setKeysPatching(null);
}
};
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">
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => openApiKeysModal(user)}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors"
>
API Keys
</button>
<button
type="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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{keysModalUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-gray-900">API anahtarları</h2>
<p className="text-sm text-gray-600">
{keysModalUser.email} süre: bugünden itibaren gün sayısı (boş veya 0 =
süresiz)
</p>
</div>
<button
type="button"
onClick={() => setKeysModalUser(null)}
className="rounded-lg px-3 py-1 text-sm text-gray-600 hover:bg-gray-100"
>
Kapat
</button>
</div>
{keysLoading ? (
<p className="text-gray-600">Yükleniyor</p>
) : userKeys.length === 0 ? (
<p className="text-gray-600">Bu kullanıcının henüz API anahtarı yok.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="py-2 pr-3">İsim</th>
<th className="py-2 pr-3">Önizleme</th>
<th className="py-2 pr-3">Mevcut bitiş</th>
<th className="py-2 pr-3">Kalan süre</th>
<th className="py-2 pr-3">Yeni süre (gün)</th>
<th className="py-2" />
</tr>
</thead>
<tbody>
{userKeys.map((k) => (
<tr key={k.id} className="border-b border-gray-100">
<td className="py-2 pr-3 font-medium">{k.name}</td>
<td className="py-2 pr-3 font-mono text-xs">{k.keyPreview}</td>
<td className="py-2 pr-3 text-xs">
{k.expiresAt
? new Date(k.expiresAt).toLocaleString("tr-TR")
: "—"}
</td>
<td className="py-2 pr-3">
<span
className={
k.remainingLabel === "Süresi doldu"
? "font-medium text-red-600"
: k.remainingLabel === "Süresiz"
? "text-gray-600"
: "font-medium text-emerald-700"
}
>
{k.remainingLabel}
</span>
</td>
<td className="py-2 pr-3">
<input
type="number"
min={0}
placeholder="0=süresiz"
value={expiryDraft[k.id] ?? ""}
onChange={(e) =>
setExpiryDraft((prev) => ({
...prev,
[k.id]: e.target.value,
}))
}
className="w-28 rounded border border-gray-300 px-2 py-1 text-sm"
/>
</td>
<td className="py-2">
<button
type="button"
disabled={keysPatching === k.id}
onClick={() =>
patchKeyExpiry(
keysModalUser.id,
k.id,
expiryDraft[k.id] ?? ""
)
}
className="rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{keysPatching === k.id ? "…" : "Kaydet"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</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>
);
}

318
app/api-docs/page.tsx Normal file
View File

@@ -0,0 +1,318 @@
"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">
İki yöntem desteklenir: <strong>JWT</strong> (login/register ile alınan token) veya
hesabınız için oluşturduğunuz <strong>API anahtarı</strong> (
<code className="text-sm">img_</code> ile başlar). İkisi de aynı header ile
gönderilir:
</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 &lt;jwt_token_veya_img_..._api_key&gt;
</code>
</div>
<p className="mt-4 text-gray-600 dark:text-gray-300">
API anahtarını web arayüzünde profil sayfasından oluşturabilirsiniz; isteğe bağlı
gün sınırı koyabilir veya süresiz bırakabilirsiniz. Admin, kullanıcı anahtarlarının
süresini panelden güncelleyebilir.
</p>
</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>
{/* API Keys */}
<div className="mb-8 border-l-4 border-teal-500 pl-4">
<div className="mb-2 flex flex-wrap items-center gap-2">
<span className="rounded bg-teal-100 px-2 py-1 text-xs font-semibold text-teal-800 dark:bg-teal-900/30 dark:text-teal-400">
GET
</span>
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
/api/v1/api-keys
</code>
</div>
<p className="mb-2 text-gray-600 dark:text-gray-400">
Oturum veya Bearer ile: kendi API anahtarlarınızı listeler (tam değer dönmez).
</p>
<div className="mb-4 flex flex-wrap items-center gap-2">
<span className="rounded bg-teal-100 px-2 py-1 text-xs font-semibold text-teal-800 dark:bg-teal-900/30 dark:text-teal-400">
POST
</span>
<code className="text-sm font-mono text-gray-700 dark:text-gray-300">
/api/v1/api-keys
</code>
</div>
<p className="text-gray-600 dark:text-gray-400">
Body:{" "}
<code className="text-xs">
{`{ "name": "Etiket", "expiresInDays": 30 }`}
</code>{" "}
<code className="text-xs">expiresInDays</code> yok/null/0 ise süresiz.
Yanıtta tam anahtar yalnızca bir kez gelir.
</p>
</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>
);
}

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

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

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

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

View 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
View File

@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
registerEnabled: process.env.REGISTER_ENABLE === "true",
});
}

View 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 { deleteFromR2 } from "@/app/lib/r2-storage";
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];
// R2'den dosyayı sil
try {
await deleteFromR2(imageData.fileName);
} catch (error) {
console.error("R2'den silme hatası:", error);
// Hata olsa bile devam et, veritabanından sil
}
// 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 }
);
}
}

53
app/api/images/route.ts Normal file
View File

@@ -0,0 +1,53 @@
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;
}
}
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));
return NextResponse.json({
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(),
})),
});
} catch (error: any) {
return NextResponse.json(
{ message: "Resimler yüklenemedi" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,154 @@
import { NextRequest, NextResponse } from "next/server";
import sharp from "sharp";
import { db } from "@/db";
import { images } from "@/db/schema";
import { nanoid } from "nanoid";
import { auth } from "@/app/lib/auth";
import { uploadToR2, getContentType } from "@/app/lib/r2-storage";
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 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}`;
// 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,
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({
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 }
);
}
}

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

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
app/globals.css Normal file
View 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
View 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>
);
}

214
app/lib/api-auth.ts Normal file
View File

@@ -0,0 +1,214 @@
import { NextRequest } 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";
import { auth } from "@/app/lib/auth";
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;
*/
/**
* Cookie oturumu (Better Auth) veya Bearer (JWT / API key) ile doğrula.
* Web arayüzünden yapılan isteklerde session; script/istemci için Authorization kullanılır.
*/
export async function authenticateWebOrAPIRequest(
request: NextRequest
): Promise<AuthResult> {
const session = await auth.api.getSession({
headers: request.headers,
});
if (session?.user) {
const u = session.user as { id: string };
try {
const users = await db
.select()
.from(user)
.where(eq(user.id, u.id))
.limit(1);
if (users.length === 0) {
return { authenticated: false, error: "Kullanıcı bulunamadı." };
}
const userData = users[0];
return {
authenticated: true,
userId: u.id,
email: userData.email,
role: (userData.role as UserRole) || "user",
};
} catch (error) {
console.error("Session user lookup error:", error);
return {
authenticated: false,
error: "Kimlik doğrulama sırasında bir hata oluştu.",
};
}
}
return authenticateAPIRequest(request);
}
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.",
};
}
}

51
app/lib/api-key-utils.ts Normal file
View File

@@ -0,0 +1,51 @@
/** Kullanıcı/admin API key oluşturma ve güncelleme için ortak süre kuralları */
export const MAX_API_KEY_NAME_LEN = 120;
export const MAX_EXPIRES_DAYS = 3650;
export function expiresAtFromDays(days: number): Date {
return new Date(Date.now() + days * 86_400_000);
}
/**
* Body'den süre çıkarır: yok/null/0 = süresiz; 1..MAX_EXPIRES_DAYS = o kadar gün.
* Geçersiz sayıda null döner (çağıran 400 verebilir).
*/
export function parseExpiresInDaysOptional(
raw: unknown
): { ok: true; value: number | null } | { ok: false; error: string } {
if (raw === undefined || raw === null) {
return { ok: true, value: null };
}
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return { ok: false, error: "expiresInDays sayı olmalıdır." };
}
const d = Math.floor(raw);
if (d === 0) {
return { ok: true, value: null };
}
if (d < 1 || d > MAX_EXPIRES_DAYS) {
return {
ok: false,
error: `expiresInDays 0 (süresiz) veya 1${MAX_EXPIRES_DAYS} arası olmalıdır.`,
};
}
return { ok: true, value: d };
}
/** Süresiz: daysRemaining null. Süreli: kalan tam gün sayısı (bitiş anına kadar; dolmuşsa 0). */
export function getDaysRemaining(expiresAt: Date | null | undefined): number | null {
if (expiresAt == null) return null;
const ms = expiresAt.getTime() - Date.now();
if (ms <= 0) return 0;
return Math.ceil(ms / 86_400_000);
}
/** Kullanıcı ve admin arayüzleri için kısa Türkçe ibare */
export function getExpiryRemainingLabel(expiresAt: Date | null | undefined): string {
if (expiresAt == null) return "Süresiz";
const d = getDaysRemaining(expiresAt);
if (d === 0) return "Süresi doldu";
if (d === 1) return "1 gün kaldı";
return `${d} gün kaldı`;
}

37
app/lib/auth.ts Normal file
View 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
},
},
},
});

54
app/lib/jwt.ts Normal file
View File

@@ -0,0 +1,54 @@
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
}
/** Liste/detay için tam anahtarı göstermez (img_xxxx…yyyy) */
export function maskApiKey(key: string): string {
if (key.length < 12) return "img_••••";
return `${key.slice(0, 7)}${key.slice(-4)}`;
}

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

81
app/lib/r2-storage.ts Normal file
View File

@@ -0,0 +1,81 @@
import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
// R2 configuration
const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID || "";
const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID || "";
const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY || "";
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME || "";
const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || "";
// S3 client configuration for Cloudflare R2
const s3Client = new S3Client({
region: "auto",
endpoint: `https://${R2_ACCOUNT_ID}.eu.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
},
});
export interface UploadOptions {
buffer: Buffer;
fileName: string;
contentType: string;
}
/**
* Upload a file to R2
*/
export async function uploadToR2(options: UploadOptions): Promise<string> {
const { buffer, fileName, contentType } = options;
try {
const command = new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: fileName,
Body: buffer,
ContentType: contentType,
});
await s3Client.send(command);
// Return the public URL
return `${R2_PUBLIC_URL}/${fileName}`;
} catch (error) {
console.error("R2 upload error:", error);
throw new Error(`R2'ye yükleme başarısız: ${error}`);
}
}
/**
* Delete a file from R2
*/
export async function deleteFromR2(fileName: string): Promise<void> {
try {
const command = new DeleteObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: fileName,
});
await s3Client.send(command);
} catch (error) {
console.error("R2 delete error:", error);
throw new Error(`R2'den silme başarısız: ${error}`);
}
}
/**
* Get content type from file extension
*/
export function getContentType(format: string): string {
const contentTypeMap: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
webp: "image/webp",
avif: "image/avif",
};
return contentTypeMap[format] || "application/octet-stream";
}

142
app/login/page.tsx Normal file
View 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
View 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>
);
}

391
app/profile/page.tsx Normal file
View File

@@ -0,0 +1,391 @@
"use client";
import { useEffect, useState, useCallback } 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;
}
interface ApiKeyRow {
id: string;
name: string;
keyPreview: string;
expiresAt: string | null;
daysRemaining: number | null;
remainingLabel: string;
lastUsedAt: string | null;
isActive: boolean;
createdAt: string;
}
export default function ProfilePage() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [apiKeys, setApiKeys] = useState<ApiKeyRow[]>([]);
const [keysLoading, setKeysLoading] = useState(false);
const [newKeyName, setNewKeyName] = useState("");
const [newKeyDays, setNewKeyDays] = useState("");
const [createBusy, setCreateBusy] = useState(false);
const router = useRouter();
const loadApiKeys = useCallback(async () => {
setKeysLoading(true);
try {
const res = await fetch("/api/v1/api-keys", { credentials: "include" });
const json = await res.json();
if (res.ok && json.data?.keys) {
setApiKeys(json.data.keys);
}
} catch {
/* ignore */
} finally {
setKeysLoading(false);
}
}, []);
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]);
useEffect(() => {
if (user) {
loadApiKeys();
}
}, [user, loadApiKeys]);
const createApiKey = async () => {
const name = newKeyName.trim();
if (!name) return;
setCreateBusy(true);
try {
const body: { name: string; expiresInDays?: number | null } = { name };
const d = newKeyDays.trim();
if (d !== "") {
const n = parseInt(d, 10);
if (!Number.isFinite(n) || n < 0) {
alert("Geçerli bir gün sayısı girin veya boş bırakın (süresiz).");
setCreateBusy(false);
return;
}
body.expiresInDays = n === 0 ? null : n;
}
const res = await fetch("/api/v1/api-keys", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
alert(json.error || "Oluşturulamadı");
return;
}
const raw = json.data?.key as string | undefined;
const rem = json.data?.remainingLabel as string | undefined;
if (raw) {
await navigator.clipboard.writeText(raw).catch(() => {});
const extra = rem ? ` Süre: ${rem}.` : "";
alert(
`API anahtarı oluşturuldu ve panoya kopyalandı. Bu tam değeri yalnızca bir kez görebilirsiniz.${extra}`
);
}
setNewKeyName("");
setNewKeyDays("");
await loadApiKeys();
} finally {
setCreateBusy(false);
}
};
const revokeKey = async (id: string) => {
if (!confirm("Bu API anahtarını iptal etmek istediğinize emin misiniz?")) return;
const res = await fetch(`/api/v1/api-keys/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) {
const j = await res.json();
alert(j.error || "İptal edilemedi");
return;
}
await loadApiKeys();
};
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 className="rounded-md border border-gray-200 p-6 dark:border-zinc-700">
<h2 className="mb-2 text-xl font-semibold text-black dark:text-zinc-50">
API anahtarları
</h2>
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Resim API&apos;sine JWT yerine{" "}
<code className="rounded bg-gray-100 px-1 dark:bg-zinc-800">
Authorization: Bearer img_
</code>{" "}
ile erişin. Anahtarı burada oluşturun; süre kısıtı opsiyoneldir (boş
= süresiz). Admin gerekirse süreyi değiştirebilir.
</p>
<div className="mb-6 flex flex-wrap items-end gap-3 rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/50">
<div className="min-w-[200px] flex-1">
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
İsim
</label>
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="örn. Mobil uygulama"
className="w-full rounded border border-gray-300 bg-white px-3 py-2 text-black dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-50"
/>
</div>
<div className="w-40">
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
Geçerlilik (gün)
</label>
<input
type="number"
min={0}
placeholder="Süresiz"
value={newKeyDays}
onChange={(e) => setNewKeyDays(e.target.value)}
className="w-full rounded border border-gray-300 bg-white px-3 py-2 text-black dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-50"
/>
</div>
<button
type="button"
disabled={createBusy || !newKeyName.trim()}
onClick={createApiKey}
className="rounded-md bg-emerald-600 px-4 py-2 text-white hover:bg-emerald-700 disabled:opacity-50"
>
{createBusy ? "…" : "Anahtar oluştur"}
</button>
</div>
{keysLoading ? (
<p className="text-sm text-gray-500">Anahtarlar yükleniyor</p>
) : apiKeys.length === 0 ? (
<p className="text-sm text-gray-500">Henüz API anahtarı yok.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-zinc-600">
<th className="py-2 pr-4 font-medium">İsim</th>
<th className="py-2 pr-4 font-medium">Önizleme</th>
<th className="py-2 pr-4 font-medium">Bitiş tarihi</th>
<th className="py-2 pr-4 font-medium">Kalan süre</th>
<th className="py-2 pr-4 font-medium">Durum</th>
<th className="py-2 font-medium" />
</tr>
</thead>
<tbody>
{apiKeys.map((k) => (
<tr
key={k.id}
className="border-b border-gray-100 dark:border-zinc-800"
>
<td className="py-2 pr-4">{k.name}</td>
<td className="py-2 pr-4 font-mono text-xs">{k.keyPreview}</td>
<td className="py-2 pr-4 text-xs">
{k.expiresAt
? new Date(k.expiresAt).toLocaleString("tr-TR")
: "—"}
</td>
<td className="py-2 pr-4">
<span
className={
k.remainingLabel === "Süresi doldu"
? "font-medium text-red-600 dark:text-red-400"
: k.remainingLabel === "Süresiz"
? "text-gray-600 dark:text-gray-400"
: "font-medium text-emerald-700 dark:text-emerald-400"
}
>
{k.remainingLabel}
</span>
</td>
<td className="py-2 pr-4">
{k.isActive ? (
<span className="text-green-600 dark:text-green-400">Aktif</span>
) : (
<span className="text-gray-500">İptal</span>
)}
</td>
<td className="py-2">
{k.isActive && (
<button
type="button"
onClick={() => revokeKey(k.id)}
className="text-red-600 hover:underline dark:text-red-400"
>
İptal et
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}

211
app/register/page.tsx Normal file
View 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
View 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>
);
}

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