diff --git a/sigap-website/app/protected/(admin)/dashboard/page.tsx b/sigap-website/app/protected/(admin)/dashboard/page.tsx index e69de29..105382e 100644 --- a/sigap-website/app/protected/(admin)/dashboard/page.tsx +++ b/sigap-website/app/protected/(admin)/dashboard/page.tsx @@ -0,0 +1,18 @@ + + + export default function DashboardPage() { + return ( + <> +
+
+
+
+
+
+
+
+
+ + ); + } + \ No newline at end of file diff --git a/sigap-website/app/protected/(admin)/dashboard/user-management/action.ts b/sigap-website/app/protected/(admin)/dashboard/user-management/action.ts new file mode 100644 index 0000000..a445707 --- /dev/null +++ b/sigap-website/app/protected/(admin)/dashboard/user-management/action.ts @@ -0,0 +1,158 @@ +"use server" + +import { CreateUserParams, InviteUserParams, UpdateUserParams, User } from "@/src/models/users/users.model" +import { createClient } from "@supabase/supabase-js" + + +// Initialize Supabase client with admin key +const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SERVICE_ROLE_SECRET!, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, +}) + +// Fetch all users +export async function fetchUsers(): Promise { + const { data, error } = await supabase.auth.admin.listUsers() + + if (error) { + console.error("Error fetching users:", error) + throw new Error(error.message) + } + + return data.users.map(user => ({ + ...user, + updated_at: user.updated_at || "", + })) as User[] +} + +// Create a new user +export async function createUser(params: CreateUserParams): Promise { + const { data, error } = await supabase.auth.admin.createUser({ + email: params.email, + password: params.password, + phone: params.phone, + user_metadata: params.user_metadata, + email_confirm: params.email_confirm, + }) + + if (error) { + console.error("Error creating user:", error) + throw new Error(error.message) + } + + return { + ...data.user, + updated_at: data.user.updated_at || "", + } as User +} + +// Update an existing user +export async function updateUser(userId: string, params: UpdateUserParams): Promise { + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + email: params.email, + phone: params.phone, + password: params.password, + user_metadata: params.user_metadata, + }) + + if (error) { + console.error("Error updating user:", error) + throw new Error(error.message) + } + + return { + ...data.user, + updated_at: data.user.updated_at || "", + } as User +} + +// Delete a user +export async function deleteUser(userId: string): Promise { + const { error } = await supabase.auth.admin.deleteUser(userId) + + if (error) { + console.error("Error deleting user:", error) + throw new Error(error.message) + } +} + +// Send password recovery email +export async function sendPasswordRecovery(email: string): Promise { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`, + }) + + if (error) { + console.error("Error sending password recovery:", error) + throw new Error(error.message) + } +} + +// Send magic link +export async function sendMagicLink(email: string): Promise { + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + }, + }) + + if (error) { + console.error("Error sending magic link:", error) + throw new Error(error.message) + } +} + +// Ban a user +export async function banUser(userId: string): Promise { + // Ban for 100 years (effectively permanent) + const banUntil = new Date() + banUntil.setFullYear(banUntil.getFullYear() + 100) + + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + ban_duration: "100y", + }) + + if (error) { + console.error("Error banning user:", error) + throw new Error(error.message) + } + + return { + ...data.user, + updated_at: data.user.updated_at || "", + } as User +} + +// Unban a user +export async function unbanUser(userId: string): Promise { + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + ban_duration: "none", + }) + + if (error) { + console.error("Error unbanning user:", error) + throw new Error(error.message) + } + + return { + ...data.user, + updated_at: data.user.updated_at || "", + } as User +} + +// Invite a user +export async function inviteUser(params: InviteUserParams): Promise { + const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, { + data: params.user_metadata, + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + }) + + if (error) { + console.error("Error inviting user:", error) + throw new Error(error.message) + } +} + diff --git a/sigap-website/app/protected/(admin)/dashboard/user-management/page.tsx b/sigap-website/app/protected/(admin)/dashboard/user-management/page.tsx new file mode 100644 index 0000000..28d5170 --- /dev/null +++ b/sigap-website/app/protected/(admin)/dashboard/user-management/page.tsx @@ -0,0 +1,26 @@ +import UserManagement from "@/components/admin/users/user-management"; +import { UserStats } from "@/components/admin/users/user-stats"; + + +export default function UsersPage() { + return ( + <> +
+
+

User Management

+

+ Manage user accounts and permissions +

+
+
+
+
+ +
+
+ +
+
+ + ); +} diff --git a/sigap-website/components/admin/navigations/nav-main.tsx b/sigap-website/components/admin/navigations/nav-main.tsx index ef1f497..51efb9c 100644 --- a/sigap-website/components/admin/navigations/nav-main.tsx +++ b/sigap-website/components/admin/navigations/nav-main.tsx @@ -17,7 +17,8 @@ import { } from "@/components/ui/sidebar"; import * as TablerIcons from "@tabler/icons-react"; -import { useNavigations } from "@/app/_hooks/use-navigations"; + +import { useNavigations } from "@/hooks/use-navigations"; interface SubSubItem { title: string; diff --git a/sigap-website/components/admin/navigations/nav-pre-main.tsx b/sigap-website/components/admin/navigations/nav-pre-main.tsx index 75dbb8b..5b7b4a8 100644 --- a/sigap-website/components/admin/navigations/nav-pre-main.tsx +++ b/sigap-website/components/admin/navigations/nav-pre-main.tsx @@ -9,7 +9,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; -import { useNavigations } from "@/app/_hooks/use-navigations"; +import { useNavigations } from "@/hooks/use-navigations"; import { Search, Bot, Home } from "lucide-react"; import { IconHome, IconRobot, IconSearch } from "@tabler/icons-react"; diff --git a/sigap-website/components/admin/users/add-user-dialog.tsx b/sigap-website/components/admin/users/add-user-dialog.tsx new file mode 100644 index 0000000..2ca4540 --- /dev/null +++ b/sigap-website/components/admin/users/add-user-dialog.tsx @@ -0,0 +1,156 @@ +"use client" + +import { useState } from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { createUser } from "@/app/protected/(admin)/dashboard/user-management/action" +import { toast } from "sonner" + + +interface AddUserDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onUserAdded: () => void +} + +export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) { + const [loading, setLoading] = useState(false) + const [formData, setFormData] = useState({ + email: "", + password: "", + phone: "", + metadata: "{}", + emailConfirm: true, + }) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData((prev) => ({ ...prev, [name]: value })) + } + + const handleSwitchChange = (checked: boolean) => { + setFormData((prev) => ({ ...prev, emailConfirm: checked })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + + try { + let metadata = {} + try { + metadata = JSON.parse(formData.metadata) + } catch (error) { + toast.error("Invalid JSON. Please check your metadata format.") + setLoading(false) + return + } + + await createUser({ + email: formData.email, + password: formData.password, + phone: formData.phone, + user_metadata: metadata, + email_confirm: formData.emailConfirm, + }) + + toast.success("User created successfully.") + onUserAdded() + onOpenChange(false) + setFormData({ + email: "", + password: "", + phone: "", + metadata: "{}", + emailConfirm: true, + }) + } catch (error) { + toast.error("Failed to create user.") + } finally { + setLoading(false) + } + } + + return ( + + + + Add User + + Create a new user account with email and password. + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +