feat: add user management to admin panel (add, delete, change password)
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { LayoutDashboard, Image as ImageIcon, Briefcase, Mail, LogOut, Settings } from 'lucide-react';
|
import { LayoutDashboard, Image as ImageIcon, Briefcase, Mail, LogOut, Settings, Users } from 'lucide-react';
|
||||||
import { logout } from '../actions';
|
import { logout } from '../actions';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -12,6 +12,7 @@ const navItems = [
|
|||||||
{ name: 'Hizmetler', href: '/admin/services', icon: Briefcase },
|
{ name: 'Hizmetler', href: '/admin/services', icon: Briefcase },
|
||||||
{ name: 'İletişim Mesajları', href: '/admin/contact', icon: Mail },
|
{ name: 'İletişim Mesajları', href: '/admin/contact', icon: Mail },
|
||||||
{ name: 'Site Ayarları', href: '/admin/settings', icon: Settings },
|
{ name: 'Site Ayarları', href: '/admin/settings', icon: Settings },
|
||||||
|
{ name: 'Kullanıcılar', href: '/admin/users', icon: Users },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
async function requireAuth() {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session) redirect('/admin/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(formData: FormData) {
|
||||||
|
await requireAuth();
|
||||||
|
|
||||||
|
const username = formData.get('username') as string;
|
||||||
|
const password = formData.get('password') as string;
|
||||||
|
const confirmPassword = formData.get('confirmPassword') as string;
|
||||||
|
|
||||||
|
if (!username || !password) return;
|
||||||
|
if (password !== confirmPassword) return;
|
||||||
|
if (password.length < 6) return;
|
||||||
|
|
||||||
|
const exists = await prisma.user.findUnique({ where: { username } });
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
const hashed = await bcrypt.hash(password, 10);
|
||||||
|
await prisma.user.create({
|
||||||
|
data: { username, password: hashed },
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath('/admin/users');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(formData: FormData) {
|
||||||
|
await requireAuth();
|
||||||
|
|
||||||
|
const id = formData.get('id') as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
// En az 1 kullanıcı kalsın
|
||||||
|
const count = await prisma.user.count();
|
||||||
|
if (count <= 1) return;
|
||||||
|
|
||||||
|
await prisma.user.delete({ where: { id } });
|
||||||
|
revalidatePath('/admin/users');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(formData: FormData) {
|
||||||
|
await requireAuth();
|
||||||
|
|
||||||
|
const userId = formData.get('userId') as string;
|
||||||
|
const newPassword = formData.get('newPassword') as string;
|
||||||
|
const confirmPassword = formData.get('confirmPassword') as string;
|
||||||
|
|
||||||
|
if (!userId || !newPassword) return;
|
||||||
|
if (newPassword !== confirmPassword) return;
|
||||||
|
if (newPassword.length < 6) return;
|
||||||
|
|
||||||
|
const hashed = await bcrypt.hash(newPassword, 10);
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { password: hashed },
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath('/admin/users');
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { UserPlus, Trash2, KeyRound, ShieldCheck } from 'lucide-react';
|
||||||
|
import { createUser, deleteUser, changePassword } from './actions';
|
||||||
|
|
||||||
|
export default async function UsersAdminPage() {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session) redirect('/admin/login');
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold mb-8">Kullanıcı Yönetimi</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Sol: Kullanıcı listesi */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Yeni kullanıcı ekle */}
|
||||||
|
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<UserPlus className="w-5 h-5 text-blue-500" />
|
||||||
|
Yeni Kullanıcı Ekle
|
||||||
|
</h2>
|
||||||
|
<form action={createUser} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Kullanıcı Adı
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
placeholder="kullanici_adi"
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
minLength={6}
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Şifre Tekrar
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
minLength={6}
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Kullanıcı Ekle
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mevcut kullanıcılar */}
|
||||||
|
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-green-500" />
|
||||||
|
Mevcut Kullanıcılar ({users.length})
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{users.map((user: any) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="flex items-center justify-between p-4 rounded-lg border border-slate-100 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800 dark:text-white">{user.username}</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
|
{new Date(user.createdAt).toLocaleDateString('tr-TR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{users.length > 1 && (
|
||||||
|
<form action={deleteUser}>
|
||||||
|
<input type="hidden" name="id" value={user.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
title="Kullanıcıyı Sil"
|
||||||
|
className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!confirm(`"${user.username}" kullanıcısı silinsin mi?`)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{users.length === 0 && (
|
||||||
|
<p className="text-slate-500 text-sm text-center py-4">Kullanıcı bulunamadı.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sağ: Şifre değiştir */}
|
||||||
|
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 h-fit">
|
||||||
|
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<KeyRound className="w-5 h-5 text-amber-500" />
|
||||||
|
Şifre Değiştir
|
||||||
|
</h2>
|
||||||
|
<form action={changePassword} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Kullanıcı
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="userId"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Kullanıcı seçin…</option>
|
||||||
|
{users.map((user: any) => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.username}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Yeni Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="newPassword"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
minLength={6}
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Yeni Şifre Tekrar
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
minLength={6}
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full px-4 py-2.5 bg-amber-500 hover:bg-amber-600 text-white font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Şifreyi Güncelle
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user