From d707d3963219d3967b82d3ab2549aeefc6424267 Mon Sep 17 00:00:00 2001 From: Braidata <121073472+braidata@users.noreply.github.com> Date: Sat, 29 Nov 2025 00:01:27 -0300 Subject: [PATCH] Protect admin users API with session check --- pages/admin/users.tsx | 518 +++++++++++++++++++++++++++++++++++++++ pages/api/admin/users.ts | 97 ++++++++ 2 files changed, 615 insertions(+) create mode 100644 pages/admin/users.tsx create mode 100644 pages/api/admin/users.ts diff --git a/pages/admin/users.tsx b/pages/admin/users.tsx new file mode 100644 index 0000000..90ee8fb --- /dev/null +++ b/pages/admin/users.tsx @@ -0,0 +1,518 @@ +import { FormEvent, useEffect, useMemo, useState } from "react"; +import { useSession } from "next-auth/react"; +import usePermissions from "../../hooks/usePermissions"; + +interface UserRecord { + id: number; + name: string; + email: string; + ownerId: number; + permissions?: string | null; + rol?: string | null; + team?: string | null; + rut?: string | null; + image?: string | null; +} + +const initialForm: Partial & { password?: string } = { + name: "", + email: "", + ownerId: undefined, + password: "", + permissions: "", + rol: "", + team: "", + rut: "", +}; + +const AdminUsersPage = () => { + const { status } = useSession(); + const { access } = usePermissions([{ permissions: ["all"], roles: ["admin"] }]); + + const [users, setUsers] = useState([]); + const [roles, setRoles] = useState([]); + const [permissions, setPermissions] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [formValues, setFormValues] = useState(initialForm); + const [editingUser, setEditingUser] = useState(null); + const [modalOpen, setModalOpen] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const hasAccess = access["all|admin"]; + + const fetchUsers = async () => { + try { + setLoading(true); + const response = await fetch("/api/admin/users"); + if (!response.ok) throw new Error("No se pudo cargar la información de usuarios"); + const data = await response.json(); + setUsers(data.users || []); + setRoles(data.roles || []); + setPermissions(data.permissions || []); + } catch (err: any) { + setError(err.message || "Error cargando usuarios"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (status === "authenticated") { + fetchUsers(); + } + }, [status]); + + const filteredUsers = useMemo(() => { + const term = searchTerm.trim().toLowerCase(); + if (!term) return users; + return users.filter( + (user) => + user.name.toLowerCase().includes(term) || + user.email.toLowerCase().includes(term) || + (user.team || "").toLowerCase().includes(term) + ); + }, [users, searchTerm]); + + const handleChange = (field: keyof typeof formValues, value: any) => { + setFormValues((prev) => ({ ...prev, [field]: value })); + }; + + const resetForm = () => { + setFormValues(initialForm); + }; + + const handleCreate = async (e: FormEvent) => { + e.preventDefault(); + setSaving(true); + setError(null); + setSuccessMessage(null); + + try { + const response = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formValues), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "No se pudo crear el usuario"); + } + + resetForm(); + setSuccessMessage("Usuario creado exitosamente"); + await fetchUsers(); + } catch (err: any) { + setError(err.message || "Error creando usuario"); + } finally { + setSaving(false); + } + }; + + const openEditModal = (user: UserRecord) => { + setEditingUser(user); + setModalOpen(true); + }; + + const handleEditChange = (field: keyof UserRecord, value: any) => { + setEditingUser((prev) => (prev ? { ...prev, [field]: value } : prev)); + }; + + const handleUpdate = async () => { + if (!editingUser) return; + setSaving(true); + setError(null); + setSuccessMessage(null); + + try { + const response = await fetch("/api/admin/users", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editingUser), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "No se pudo actualizar el usuario"); + } + + setModalOpen(false); + setSuccessMessage("Usuario actualizado"); + await fetchUsers(); + } catch (err: any) { + setError(err.message || "Error actualizando usuario"); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm("¿Seguro que deseas eliminar este usuario?")) return; + setSaving(true); + setError(null); + setSuccessMessage(null); + + try { + const response = await fetch("/api/admin/users", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + + if (!response.ok && response.status !== 204) { + const errorData = await response.json(); + throw new Error(errorData.message || "No se pudo eliminar el usuario"); + } + + setSuccessMessage("Usuario eliminado"); + await fetchUsers(); + } catch (err: any) { + setError(err.message || "Error eliminando usuario"); + } finally { + setSaving(false); + } + }; + + if (status === "loading") { + return ( +
+ Validando sesión... +
+ ); + } + + if (status === "authenticated" && hasAccess === undefined) { + return ( +
+ Comprobando permisos... +
+ ); + } + + if (!hasAccess) { + return ( +
+

Acceso restringido

+

+ Necesitas rol admin y permiso all para ingresar. +

+
+ ); + } + + return ( +
+
+
+
+

Panel administrativo

+

Gestión de usuarios

+

Crea, busca y edita usuarios usando los roles y permisos existentes.

+
+
+ setSearchTerm(e.target.value)} + /> +
+
+ + {error && ( +
+ {error} +
+ )} + {successMessage && ( +
+ {successMessage} +
+ )} + +
+
+ + handleChange("name", e.target.value)} + /> +
+
+ + handleChange("email", e.target.value)} + /> +
+
+ + handleChange("ownerId", e.target.value)} + /> +
+
+ + handleChange("password", e.target.value)} + /> +
+
+ + +
+
+ + +
+
+ + handleChange("team", e.target.value)} + /> +
+
+ + handleChange("rut", e.target.value)} + /> +
+
+
+ + +
+
+ +
+
+

Usuarios existentes

+ + {users.length} en total + +
+ + {loading ? ( +
Cargando usuarios...
+ ) : ( +
+ + + + + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + + + + ))} + +
IDNombreEmailRolPermisosEquipoAcciones
#{user.id}{user.name}{user.email}{user.rol || "—"}{user.permissions || "—"}{user.team || "—"} +
+ + +
+
+
+ )} +
+ + {modalOpen && editingUser && ( +
+
+
+
+

Editar usuario

+

{editingUser.name}

+
+ +
+ +
+
+ + handleEditChange("name", e.target.value)} + /> +
+
+ + handleEditChange("email", e.target.value)} + /> +
+
+ + handleEditChange("ownerId", Number(e.target.value))} + /> +
+
+ + +
+
+ + +
+
+ + handleEditChange("team", e.target.value)} + /> +
+
+ + handleEditChange("rut", e.target.value)} + /> +
+
+ +
+ + +
+
+
+ )} +
+ ); +}; + +export default AdminUsersPage; diff --git a/pages/api/admin/users.ts b/pages/api/admin/users.ts new file mode 100644 index 0000000..10b09b8 --- /dev/null +++ b/pages/api/admin/users.ts @@ -0,0 +1,97 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { PrismaClient } from "@prisma/client"; +import { getServerSession } from "next-auth/next"; +import authOptions from "./auth/[...nextauth]"; + +const prisma = new PrismaClient(); + +const buildUserPayload = (body: any) => { + const payload: any = {}; + + if (body.name !== undefined) payload.name = body.name; + if (body.email !== undefined) payload.email = body.email; + if (body.ownerId !== undefined && body.ownerId !== null && body.ownerId !== "") { + const numericOwnerId = Number(body.ownerId); + if (!Number.isNaN(numericOwnerId)) payload.ownerId = numericOwnerId; + } + if (body.password !== undefined) payload.password = body.password || null; + if (body.rol !== undefined) payload.rol = body.rol || null; + if (body.permissions !== undefined) payload.permissions = body.permissions || null; + if (body.team !== undefined) payload.team = body.team || null; + if (body.rut !== undefined) payload.rut = body.rut || null; + if (body.image !== undefined) payload.image = body.image || null; + + return payload; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const session = await getServerSession(req, res, authOptions); + + if (!session) { + return res.status(401).json({ message: "No autorizado" }); + } + + if (req.method === "GET") { + const [users, roles, permissions] = await Promise.all([ + prisma.users.findMany({ orderBy: { id: "desc" } }), + prisma.users.groupBy({ + by: ["rol"], + where: { rol: { not: null } }, + _count: true, + }), + prisma.users.groupBy({ + by: ["permissions"], + where: { permissions: { not: null } }, + _count: true, + }), + ]); + + return res.status(200).json({ + users, + roles: roles.map((item) => item.rol), + permissions: permissions.map((item) => item.permissions), + }); + } + + if (req.method === "POST") { + const payload = buildUserPayload(req.body); + + if (!payload.name || !payload.email || payload.ownerId === undefined) { + return res.status(400).json({ message: "Nombre, email y ownerId son obligatorios" }); + } + + const user = await prisma.users.create({ data: payload }); + return res.status(201).json({ user }); + } + + if (req.method === "PUT") { + const { id } = req.body; + if (!id) { + return res.status(400).json({ message: "El id del usuario es obligatorio" }); + } + + const payload = buildUserPayload(req.body); + const user = await prisma.users.update({ where: { id: Number(id) }, data: payload }); + return res.status(200).json({ user }); + } + + if (req.method === "DELETE") { + const { id } = req.body || req.query; + if (!id) { + return res.status(400).json({ message: "El id del usuario es obligatorio" }); + } + + await prisma.users.delete({ where: { id: Number(id) } }); + return res.status(204).end(); + } + + res.setHeader("Allow", ["GET", "POST", "PUT", "DELETE"]); + return res.status(405).end("Method Not Allowed"); + } catch (error: any) { + console.error("Error en /api/admin/users:", error); + return res.status(500).json({ message: "Error interno del servidor", error: error?.message }); + } finally { + await prisma.$disconnect(); + } +}