Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function POST(request: Request) {
)
}

const clienteRole = await prisma.role.findUnique({ where: { nombre: "Cliente" } })
const clienteRole = await prisma.role.findUnique({ where: { name: "Cliente" } })

const hashed = await bcrypt.hash(password, 12)

Expand Down
4 changes: 2 additions & 2 deletions app/api/roles/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export async function PUT(request: Request, { params }: RouteContext) {
const rolActualizado = await prisma.role.update({
where: { id: parsedId.data },
data: {
nombre: parsed.data.nombre,
descripcion: parsed.data.descripcion,
name: parsed.data.name,
description: parsed.data.description,
},
});

Expand Down
8 changes: 4 additions & 4 deletions app/api/roles/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export async function GET(request: Request) {
const where: Prisma.RoleWhereInput = {};
if (search !== '') {
where.OR = [
{ nombre: { contains: search } },
{ descripcion: { contains: search } },
{ name: { contains: search } },
{ description: { contains: search } },
];
}

Expand Down Expand Up @@ -59,8 +59,8 @@ export async function POST(request: Request) {

const nuevoRol = await prisma.role.create({
data: {
nombre: parsed.data.nombre,
descripcion: parsed.data.descripcion,
name: parsed.data.name,
description: parsed.data.description,
},
});

Expand Down
8 changes: 4 additions & 4 deletions app/api/users/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
import prisma from '../../../../lib/prisma';

export const runtime = 'nodejs'
import { updateUserSchema, idParamSchema } from '../../../../lib/validations';
import { updateUserSchema, userIdParamSchema } from '../../../../lib/validations';
import { requireAdmin } from '../../../../lib/api-auth';

type RouteContext = { params: Promise<{ id: string }> };
Expand All @@ -14,7 +14,7 @@ export async function PUT(request: Request, { params }: RouteContext) {

try {
const { id } = await params;
const parsedId = idParamSchema.safeParse(id);
const parsedId = userIdParamSchema.safeParse(id);
if (!parsedId.success) {
return NextResponse.json({ error: "ID inválido" }, { status: 400 });
}
Expand Down Expand Up @@ -45,7 +45,7 @@ export async function DELETE(request: Request, { params }: RouteContext) {

try {
const { id } = await params;
const parsedId = idParamSchema.safeParse(id);
const parsedId = userIdParamSchema.safeParse(id);
if (!parsedId.success) {
return NextResponse.json({ error: "ID inválido" }, { status: 400 });
}
Expand All @@ -68,7 +68,7 @@ export async function PATCH(request: Request, { params }: RouteContext) {

try {
const { id } = await params;
const parsedId = idParamSchema.safeParse(id);
const parsedId = userIdParamSchema.safeParse(id);
if (!parsedId.success) {
return NextResponse.json({ error: "ID inválido" }, { status: 400 });
}
Expand Down
6 changes: 3 additions & 3 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ export default async function DashboardPage() {

const [totalUsuarios, totalRoles, usuariosActivos, usuariosPendientes, nuevosClientes, recientes] =
await Promise.all([
prisma.user.count({ where: { role: { nombre: "Cliente" }, deletedAt: null } }),
prisma.user.count({ where: { role: { name: "Cliente" }, deletedAt: null } }),
prisma.role.count(),
prisma.user.count({ where: { emailVerified: true, deletedAt: null } }),
prisma.user.count({ where: { emailVerified: false, deletedAt: null } }),
prisma.user.count({
where: {
createdAt: { gte: sieteDiasAtras },
role: { nombre: "Cliente" },
role: { name: "Cliente" },
deletedAt: null,
},
}),
Expand Down Expand Up @@ -196,7 +196,7 @@ export default async function DashboardPage() {
</TableCell>
<TableCell className="text-muted-foreground">{usuario.email}</TableCell>
<TableCell>
<Badge variant="outline">{usuario.role?.nombre ?? "Sin rol"}</Badge>
<Badge variant="outline">{usuario.role?.name ?? "Sin rol"}</Badge>
</TableCell>
<TableCell className="hidden text-muted-foreground sm:table-cell">
{dateFormatter.format(usuario.createdAt)}
Expand Down
42 changes: 21 additions & 21 deletions app/dashboard/roles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ import {
} from "@/components/ui/pagination";
import { toast } from "sonner";

type Role = { id: number; nombre: string; descripcion: string | null; creadoEn: string };
type RoleForm = { nombre: string; descripcion: string };
type EditingRole = { id: number; nombre: string; descripcion: string } | null;
type ConfirmDelete = { id: number; nombre: string } | null;
type Role = { id: number; name: string; description: string | null; createdAt: string };
type RoleForm = { name: string; description: string };
type EditingRole = { id: number; name: string; description: string } | null;
type ConfirmDelete = { id: number; name: string } | null;

const LIMIT = 5;

Expand All @@ -50,7 +50,7 @@ const getPageNumbers = (current: number, total: number): (number | "ellipsis")[]
return pages;
};

const emptyForm: RoleForm = { nombre: "", descripcion: "" };
const emptyForm: RoleForm = { name: "", description: "" };

export default function RolesPage() {
const [roles, setRoles] = useState<Role[]>([]);
Expand Down Expand Up @@ -115,7 +115,7 @@ export default function RolesPage() {
const res = await fetch(`/api/roles/${editingRole.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nombre: editingRole.nombre, descripcion: editingRole.descripcion }),
body: JSON.stringify({ name: editingRole.name, description: editingRole.description }),
});
if (!res.ok) {
const data = await res.json();
Expand Down Expand Up @@ -181,10 +181,10 @@ export default function RolesPage() {
roles.map((role) => (
<TableRow key={role.id}>
<TableCell>{role.id}</TableCell>
<TableCell className="font-medium">{role.nombre}</TableCell>
<TableCell className="hidden text-muted-foreground md:table-cell">{role.descripcion || "—"}</TableCell>
<TableCell className="font-medium">{role.name}</TableCell>
<TableCell className="hidden text-muted-foreground md:table-cell">{role.description || "—"}</TableCell>
<TableCell className="hidden text-muted-foreground sm:table-cell">
{new Date(role.creadoEn).toLocaleDateString("es-MX")}
{new Date(role.createdAt).toLocaleDateString("es-MX")}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
Expand All @@ -195,8 +195,8 @@ export default function RolesPage() {
onClick={() =>
setEditingRole({
id: role.id,
nombre: role.nombre,
descripcion: role.descripcion ?? "",
name: role.name,
description: role.description ?? "",
})
}
>
Expand All @@ -207,7 +207,7 @@ export default function RolesPage() {
size="icon-sm"
aria-label="Eliminar"
className="hover:text-destructive"
onClick={() => setConfirmDelete({ id: role.id, nombre: role.nombre })}
onClick={() => setConfirmDelete({ id: role.id, name: role.name })}
>
<Trash2 size={14} />
</Button>
Expand Down Expand Up @@ -274,17 +274,17 @@ export default function RolesPage() {
<Label htmlFor="create-nombre">Nombre</Label>
<Input
id="create-nombre"
value={createForm.nombre}
onChange={(e) => setCreateForm((f) => ({ ...f, nombre: e.target.value }))}
value={createForm.name}
onChange={(e) => setCreateForm((f) => ({ ...f, name: e.target.value }))}
required
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="create-descripcion">Descripción</Label>
<Textarea
id="create-descripcion"
value={createForm.descripcion}
onChange={(e) => setCreateForm((f) => ({ ...f, descripcion: e.target.value }))}
value={createForm.description}
onChange={(e) => setCreateForm((f) => ({ ...f, description: e.target.value }))}
/>
</div>
<DialogFooter>
Expand All @@ -308,19 +308,19 @@ export default function RolesPage() {
<Label htmlFor="edit-nombre">Nombre</Label>
<Input
id="edit-nombre"
value={editingRole?.nombre || ""}
value={editingRole?.name || ""}
onChange={(e) =>
setEditingRole((r) => (r ? { ...r, nombre: e.target.value } : null))
setEditingRole((r) => (r ? { ...r, name: e.target.value } : null))
}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="edit-descripcion">Descripción</Label>
<Textarea
id="edit-descripcion"
value={editingRole?.descripcion || ""}
value={editingRole?.description || ""}
onChange={(e) =>
setEditingRole((r) => (r ? { ...r, descripcion: e.target.value } : null))
setEditingRole((r) => (r ? { ...r, description: e.target.value } : null))
}
/>
</div>
Expand All @@ -340,7 +340,7 @@ export default function RolesPage() {
<AlertDialogHeader>
<AlertDialogTitle className="text-center">¿Eliminar rol?</AlertDialogTitle>
<AlertDialogDescription className="text-center">
Se eliminará el rol &quot;{confirmDelete?.nombre}&quot; de forma permanente.
Se eliminará el rol &quot;{confirmDelete?.name}&quot; de forma permanente.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex justify-center gap-2 pt-2">
Expand Down
8 changes: 4 additions & 4 deletions app/dashboard/usuarios/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import { toast } from "sonner";
import { cn } from "@/lib/utils";

type EditingUser = { id: number; name: string; email: string };
type ConfirmDelete = { id: number; name: string } | null;
type EditingUser = { id: string; name: string; email: string };
type ConfirmDelete = { id: string; name: string } | null;

const LIMIT = 5;

Expand Down Expand Up @@ -80,7 +80,7 @@
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchUsuarios(currentPage);
}, [showTrash]);

Check warning on line 83 in app/dashboard/usuarios/page.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has missing dependencies: 'currentPage' and 'fetchUsuarios'. Either include them or remove the dependency array

const handleSearchChange = (value: string) => {
setSearchTerm(value);
Expand Down Expand Up @@ -118,13 +118,13 @@
}
};

const handleDelete = async (id: number) => {
const handleDelete = async (id: string) => {
await fetch(`/api/users/${id}`, { method: "DELETE" });
fetchUsuarios(currentPage);
toast.success("Usuario movido a la papelera");
};

const handleRestore = async (id: number) => {
const handleRestore = async (id: string) => {
await fetch(`/api/users/${id}`, { method: "PATCH" });
fetchUsuarios(currentPage);
toast.success("Usuario restaurado");
Expand Down
8 changes: 4 additions & 4 deletions lib/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ export const authConfig = {
const existingUser = await prisma.user.findUnique({ where: { email } })

if (!existingUser) {
const clienteRole = await prisma.role.findUnique({ where: { nombre: "Cliente" } })
const clienteRole = await prisma.role.findUnique({ where: { name: "Cliente" } })
await prisma.user.create({
data: {
name: user.name ?? profile?.name ?? email,
email,
password: null,
emailVerified: true,
roleId: clienteRole?.id ?? null,
roleId: clienteRole?.id,
},
})
} else if (!existingUser.emailVerified) {
Expand All @@ -40,8 +40,8 @@ export const authConfig = {
where: { email: token.email },
include: { role: true },
})
if (dbUser) token.id = String(dbUser.id)
token.role = dbUser?.role?.nombre ?? null
if (dbUser) token.id = dbUser.id
token.role = dbUser?.role?.name ?? null
}
return token
},
Expand Down
2 changes: 1 addition & 1 deletion lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({

if (!user.emailVerified) throw new EmailNotVerifiedError()

return { id: String(user.id), email: user.email, name: user.name }
return { id: user.id, email: user.email, name: user.name }
},
}),
Google,
Expand Down
9 changes: 6 additions & 3 deletions lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export const resetPasswordSchema = z.object({
password: passwordSchema,
})

/** Valida params dinámicos [id]: deben ser enteros positivos. */
/** Valida params dinámicos [id] de Role: deben ser enteros positivos. */
export const idParamSchema = z.coerce.number().int().positive()

/** Valida params dinámicos [id] de User: deben ser UUID. */
export const userIdParamSchema = z.uuid()

export const createUserSchema = z.object({
name: z.string().min(2, "Mínimo 2 caracteres").max(100),
email: z.string().email("Email inválido"),
Expand All @@ -33,8 +36,8 @@ export const createUserSchema = z.object({
export const updateUserSchema = createUserSchema.partial()

export const createRoleSchema = z.object({
nombre: z.string().min(2, "Mínimo 2 caracteres").max(100),
descripcion: z.string().max(255).optional(),
name: z.string().min(2, "Mínimo 2 caracteres").max(100),
description: z.string().max(255).optional(),
})

export const updateRoleSchema = createRoleSchema.partial()
Expand Down
4 changes: 2 additions & 2 deletions prisma.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { defineConfig } from "@prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
seed: "npx tsx prisma/seed.ts",
seed: "npx tsx prisma/seed-roles.ts && npx tsx prisma/seed.ts",
},
datasource: {
url: process.env.DATABASE_URL!,
url: process.env.DIRECT_URL ?? process.env.DATABASE_URL!,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_roleId_fkey";

-- DropIndex
DROP INDEX "Role_nombre_key";

-- AlterTable
ALTER TABLE "Role" DROP COLUMN "creadoEn",
DROP COLUMN "descripcion",
DROP COLUMN "nombre",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "description" TEXT,
ADD COLUMN "name" TEXT NOT NULL;

-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
DROP COLUMN "id",
ADD COLUMN "id" UUID NOT NULL,
ALTER COLUMN "roleId" SET NOT NULL,
ALTER COLUMN "roleId" SET DEFAULT 2,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");

-- CreateIndex
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");

-- CreateIndex
CREATE INDEX "User_roleId_idx" ON "User"("roleId");

-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
14 changes: 8 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
id String @id @default(uuid()) @db.Uuid
name String
email String @unique
password String?
Expand All @@ -16,16 +16,18 @@ model User {
verifyTokenExp DateTime?
resetToken String? @unique
resetTokenExp DateTime?
roleId Int?
role Role? @relation(fields: [roleId], references: [id])
roleId Int @default(2)
role Role @relation(fields: [roleId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
deletedAt DateTime?

@@index([roleId])
}

model Role {
id Int @id @default(autoincrement())
nombre String @unique
descripcion String?
creadoEn DateTime @default(now())
name String @unique
description String?
createdAt DateTime @default(now())
users User[]
}
Loading
Loading