frontend: kullanıcı yönetimi sayfası
/users: kullanıcı listesi, ekleme formu, silme (kendi hesabını veya son kalan tek kullanıcıyı silmeye izin verilmiyor). Nav'a link eklendi. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
<Link href="/">Kanal Durumu</Link>
|
<Link href="/">Kanal Durumu</Link>
|
||||||
<Link href="/segments">Segment & Aday Kütüphanesi</Link>
|
<Link href="/segments">Segment & Aday Kütüphanesi</Link>
|
||||||
<Link href="/settings">Ayarlar</Link>
|
<Link href="/settings">Ayarlar</Link>
|
||||||
|
<Link href="/users">Kullanıcılar</Link>
|
||||||
<span style={{ marginLeft: "auto", display: "flex", gap: "1rem", alignItems: "center" }}>
|
<span style={{ marginLeft: "auto", display: "flex", gap: "1rem", alignItems: "center" }}>
|
||||||
<span className="mono">{session.username}</span>
|
<span className="mono">{session.username}</span>
|
||||||
<form action={logout}>
|
<form action={logout}>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { prisma } from "@streamclipper/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { getSession } from "../../lib/auth";
|
||||||
|
|
||||||
|
export async function addUser(formData: FormData) {
|
||||||
|
const username = String(formData.get("username") ?? "").trim();
|
||||||
|
const password = String(formData.get("password") ?? "");
|
||||||
|
|
||||||
|
if (!username || password.length < 8) {
|
||||||
|
throw new Error("Kullanıcı adı gerekli, şifre en az 8 karakter olmalı.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(password, 12);
|
||||||
|
await prisma.user.create({ data: { username, passwordHash } });
|
||||||
|
|
||||||
|
revalidatePath("/users");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(userId: string) {
|
||||||
|
const session = await getSession();
|
||||||
|
if (session?.sub === userId) {
|
||||||
|
throw new Error("Kendi hesabını silemezsin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUsers = await prisma.user.count();
|
||||||
|
if (totalUsers <= 1) {
|
||||||
|
throw new Error("Son kalan kullanıcı silinemez.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.delete({ where: { id: userId } });
|
||||||
|
revalidatePath("/users");
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { prisma } from "@streamclipper/db";
|
||||||
|
import { getSession } from "../../lib/auth";
|
||||||
|
import { addUser, deleteUser } from "./actions";
|
||||||
|
import { DeleteButton } from "../components/DeleteButton";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const fieldStyle = {
|
||||||
|
background: "var(--bg)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "0.5rem 0.7rem",
|
||||||
|
color: "var(--text)",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function UsersPage() {
|
||||||
|
const [users, session] = await Promise.all([
|
||||||
|
prisma.user.findMany({ orderBy: { createdAt: "asc" } }),
|
||||||
|
getSession(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Kullanıcılar</h1>
|
||||||
|
|
||||||
|
<form
|
||||||
|
action={addUser}
|
||||||
|
className="card add-channel-form"
|
||||||
|
style={{ marginBottom: "1.5rem" }}
|
||||||
|
>
|
||||||
|
<input name="username" placeholder="Kullanıcı adı" required style={fieldStyle} />
|
||||||
|
<input name="password" type="password" placeholder="Şifre (en az 8 karakter)" required minLength={8} style={fieldStyle} />
|
||||||
|
<button type="submit">Kullanıcı Ekle</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Kullanıcı Adı</th>
|
||||||
|
<th>Oluşturulma</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((u) => (
|
||||||
|
<tr key={u.id}>
|
||||||
|
<td>
|
||||||
|
{u.username}
|
||||||
|
{u.id === session?.sub && <span className="badge muted" style={{ marginLeft: "0.5rem" }}>sen</span>}
|
||||||
|
</td>
|
||||||
|
<td className="mono">{u.createdAt.toLocaleString("tr-TR")}</td>
|
||||||
|
<td>
|
||||||
|
{u.id !== session?.sub && users.length > 1 && (
|
||||||
|
<form action={deleteUser.bind(null, u.id)}>
|
||||||
|
<DeleteButton confirmText={`${u.username} kullanıcısını silmek istediğine emin misin?`} />
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user