Feat: Implement User Create and Edit pages

This commit is contained in:
2025-12-03 22:38:44 +03:00
parent fdc242b076
commit 97eebbd725
8 changed files with 441 additions and 3 deletions

View File

@@ -0,0 +1,47 @@
'use server'
import { createClient } from "@/lib/supabase/server"
import { createAdminClient } from "@/lib/supabase/admin"
import { revalidatePath } from "next/cache"
import { logAction } from "@/lib/logger"
export async function updateUser(userId: string, data: { full_name: string, role: string }) {
const supabase = await createClient()
// 1. Update Profile (Regular client might work if RLS allows updating own profile or if user is admin)
// However, changing role usually requires admin privileges or specific RLS.
// Let's try with regular client first, if it fails, try admin client.
const { error } = await supabase
.from('profiles')
.update({
full_name: data.full_name,
role: data.role
})
.eq('id', userId)
if (error) {
// If regular update fails (likely due to RLS on role column), try admin client
const supabaseAdmin = await createAdminClient()
if (supabaseAdmin) {
const { error: adminError } = await supabaseAdmin
.from('profiles')
.update({
full_name: data.full_name,
role: data.role
})
.eq('id', userId)
if (adminError) return { error: adminError.message }
} else {
return { error: "Yetkisiz işlem veya Service Role Key eksik." }
}
}
await logAction('update_user', 'user', userId, {
full_name: data.full_name,
role: data.role
})
revalidatePath('/dashboard/settings/users')
}

View File

@@ -0,0 +1,115 @@
'use client'
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { updateUser } from "./actions"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
const formSchema = z.object({
full_name: z.string().min(2, "İsim en az 2 karakter olmalıdır."),
role: z.enum(["admin", "staff"]),
})
interface EditUserFormProps {
user: {
id: string
full_name: string
role: "admin" | "staff"
email?: string
}
}
export function EditUserForm({ user }: EditUserFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
full_name: user.full_name,
role: user.role,
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true)
try {
const result = await updateUser(user.id, values)
if (result?.error) {
toast.error(result.error)
} else {
toast.success("Kullanıcı başarıyla güncellendi")
router.push('/dashboard/settings/users')
router.refresh()
}
} catch (error) {
toast.error("Bir hata oluştu")
} finally {
setLoading(false)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-2">
<FormLabel>E-posta</FormLabel>
<Input value={user.email || 'Bilinmiyor'} disabled className="bg-muted" />
<p className="text-xs text-muted-foreground">E-posta adresi değiştirilemez.</p>
</div>
<FormField
control={form.control}
name="full_name"
render={({ field }) => (
<FormItem>
<FormLabel>İsim Soyisim</FormLabel>
<FormControl>
<Input placeholder="Ahmet Yılmaz" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Rol</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Rol Seçin" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="staff">Personel</SelectItem>
<SelectItem value="admin">Yönetici</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="w-full">
{loading ? "Güncelleniyor..." : "Güncelle"}
</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,40 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { EditUserForm } from "./edit-user-form"
import { createClient } from "@/lib/supabase/server"
import { notFound } from "next/navigation"
export default async function EditUserPage({ params }: { params: { id: string } }) {
const supabase = await createClient()
// Fetch profile
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', params.id)
.single()
if (!profile) {
notFound()
}
// We can't easily get email from profiles if it's not stored there.
// But for editing, we mostly care about name and role.
return (
<div className="max-w-2xl mx-auto">
<Card>
<CardHeader>
<CardTitle>Kullanıcı Düzenle</CardTitle>
</CardHeader>
<CardContent>
<EditUserForm user={{
id: profile.id,
full_name: profile.full_name,
role: profile.role,
email: undefined // Email is not in profiles table currently
}} />
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,52 @@
'use server'
import { createAdminClient } from "@/lib/supabase/admin"
import { createClient } from "@/lib/supabase/server" // For regular client if needed
import { revalidatePath } from "next/cache"
import { logAction } from "@/lib/logger"
export async function createUser(data: any) {
const supabaseAdmin = await createAdminClient()
if (!supabaseAdmin) {
return { error: "Kullanıcı oluşturmak için .env dosyasına SUPABASE_SERVICE_ROLE_KEY eklenmelidir." }
}
// 1. Create Auth User
const { data: userData, error: userError } = await supabaseAdmin.auth.admin.createUser({
email: data.email,
password: data.password,
email_confirm: true,
user_metadata: {
full_name: data.full_name
}
})
if (userError) {
return { error: userError.message }
}
if (!userData.user) {
return { error: "Kullanıcı oluşturulamadı." }
}
// 2. Update Profile Role (Trigger creates profile as 'staff' by default, we might need to update it if 'admin')
if (data.role === 'admin') {
const { error: profileError } = await supabaseAdmin
.from('profiles')
.update({ role: 'admin' })
.eq('id', userData.user.id)
if (profileError) {
// Log error but don't fail completely as user is created
console.error("Error updating role:", profileError)
}
}
await logAction('create_user', 'user', userData.user.id, {
email: data.email,
role: data.role
})
revalidatePath('/dashboard/settings/users')
}

View File

@@ -0,0 +1,17 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { UserForm } from "./user-form"
export default function NewUserPage() {
return (
<div className="max-w-2xl mx-auto">
<Card>
<CardHeader>
<CardTitle>Yeni Kullanıcı Oluştur</CardTitle>
</CardHeader>
<CardContent>
<UserForm />
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,130 @@
'use client'
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { createUser } from "./actions"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
const formSchema = z.object({
email: z.string().email("Geçerli bir e-posta adresi giriniz."),
password: z.string().min(6, "Şifre en az 6 karakter olmalıdır."),
full_name: z.string().min(2, "İsim en az 2 karakter olmalıdır."),
role: z.enum(["admin", "staff"]),
})
export function UserForm() {
const router = useRouter()
const [loading, setLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
full_name: "",
role: "staff",
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true)
try {
const result = await createUser(values)
if (result?.error) {
toast.error(result.error)
} else {
toast.success("Kullanıcı başarıyla oluşturuldu")
router.push('/dashboard/settings/users')
router.refresh()
}
} catch (error) {
toast.error("Bir hata oluştu")
} finally {
setLoading(false)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>E-posta</FormLabel>
<FormControl>
<Input placeholder="ornek@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Şifre</FormLabel>
<FormControl>
<Input type="password" placeholder="******" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="full_name"
render={({ field }) => (
<FormItem>
<FormLabel>İsim Soyisim</FormLabel>
<FormControl>
<Input placeholder="Ahmet Yılmaz" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Rol</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Rol Seçin" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="staff">Personel</SelectItem>
<SelectItem value="admin">Yönetici</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="w-full">
{loading ? "Oluşturuluyor..." : "Kullanıcı Oluştur"}
</Button>
</form>
</Form>
)
}

View File

@@ -34,11 +34,11 @@ export default async function UsersPage() {
<h2 className="text-3xl font-bold tracking-tight">Kullanıcı Yönetimi</h2>
<p className="text-muted-foreground">Sisteme erişimi olan kullanıcıları yönetin.</p>
</div>
{/* <Link href="/dashboard/settings/users/new">
<Link href="/dashboard/settings/users/new">
<Button>
<Plus className="mr-2 h-4 w-4" /> Yeni Kullanıcı
</Button>
</Link> */}
</Link>
</div>
<div className="rounded-md border hidden md:block">
@@ -78,7 +78,9 @@ export default async function UsersPage() {
{new Date(profile.created_at).toLocaleDateString('tr-TR')}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" disabled>Düzenle</Button>
<Link href={`/dashboard/settings/users/${profile.id}`}>
<Button variant="ghost" size="sm">Düzenle</Button>
</Link>
</TableCell>
</TableRow>
))