Feat: Implement User Create and Edit pages
This commit is contained in:
47
src/app/dashboard/settings/users/[id]/actions.ts
Normal file
47
src/app/dashboard/settings/users/[id]/actions.ts
Normal 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')
|
||||
}
|
||||
115
src/app/dashboard/settings/users/[id]/edit-user-form.tsx
Normal file
115
src/app/dashboard/settings/users/[id]/edit-user-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
40
src/app/dashboard/settings/users/[id]/page.tsx
Normal file
40
src/app/dashboard/settings/users/[id]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
src/app/dashboard/settings/users/new/actions.ts
Normal file
52
src/app/dashboard/settings/users/new/actions.ts
Normal 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')
|
||||
}
|
||||
17
src/app/dashboard/settings/users/new/page.tsx
Normal file
17
src/app/dashboard/settings/users/new/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
130
src/app/dashboard/settings/users/new/user-form.tsx
Normal file
130
src/app/dashboard/settings/users/new/user-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user