Files
image-apiv3/app/profile/page.tsx
Beyhan Oğur 031582ea2c first commit
2026-04-26 22:11:03 +03:00

392 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}