aboutsummaryrefslogtreecommitdiff
path: root/api/src/modules/users
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/modules/users')
-rw-r--r--api/src/modules/users/users.controller.ts129
-rw-r--r--api/src/modules/users/users.routes.ts56
-rw-r--r--api/src/modules/users/users.schema.ts25
3 files changed, 210 insertions, 0 deletions
diff --git a/api/src/modules/users/users.controller.ts b/api/src/modules/users/users.controller.ts
new file mode 100644
index 0000000..c3c06fd
--- /dev/null
+++ b/api/src/modules/users/users.controller.ts
@@ -0,0 +1,129 @@
+import type { FastifyRequest, FastifyReply } from 'fastify';
+import bcrypt from 'bcryptjs';
+import { users } from '@/db/schema.js';
+import { eq, and, ne } from 'drizzle-orm';
+import type { CreateUserByAdminBody, UpdateUserBody } from './users.schema.js';
+
+export async function listUsersHandler(request: FastifyRequest, reply: FastifyReply) {
+ const db = request.server.db;
+ const allUsers = await db.query.users.findMany({
+ columns: {
+ password: false, // Don't return passwords
+ },
+ orderBy: (users, { desc }) => [desc(users.createdAt)],
+ });
+
+ return reply.send(allUsers);
+}
+
+export async function createUserHandler(
+ request: FastifyRequest<{ Body: CreateUserByAdminBody }>,
+ reply: FastifyReply,
+) {
+ const db = request.server.db;
+ const { name, email, password, role } = request.body;
+
+ // Verifica se email já existe
+ const existing = await db.query.users.findFirst({
+ where: eq(users.email, email),
+ });
+
+ if (existing) {
+ return reply.conflict('Este e-mail já está cadastrado.');
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 12);
+
+ const [newUser] = await db.insert(users).values({
+ name,
+ email,
+ password: hashedPassword,
+ role,
+ isActive: true,
+ }).returning({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ role: users.role,
+ isActive: users.isActive,
+ createdAt: users.createdAt,
+ });
+
+ return reply.code(201).send(newUser);
+}
+
+export async function updateUserHandler(
+ request: FastifyRequest<{ Params: { id: number }; Body: UpdateUserBody }>,
+ reply: FastifyReply,
+) {
+ const db = request.server.db;
+ const { id } = request.params;
+ const { name, email, password, role, isActive } = request.body;
+
+ const existingUser = await db.query.users.findFirst({
+ where: eq(users.id, id),
+ });
+
+ if (!existingUser) {
+ return reply.notFound('Usuário não encontrado.');
+ }
+
+ if (email && email !== existingUser.email) {
+ const existingEmail = await db.query.users.findFirst({
+ where: and(eq(users.email, email), ne(users.id, id)),
+ });
+ if (existingEmail) {
+ return reply.conflict('Este e-mail já está em uso por outro usuário.');
+ }
+ }
+
+ const updates: Partial<typeof users.$inferInsert> = {
+ name,
+ email,
+ role,
+ isActive,
+ updatedAt: new Date().toISOString(),
+ };
+
+ if (password && password.trim() !== '') {
+ updates.password = await bcrypt.hash(password, 12);
+ }
+
+ const [updatedUser] = await db.update(users)
+ .set(updates)
+ .where(eq(users.id, id))
+ .returning({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ role: users.role,
+ isActive: users.isActive,
+ createdAt: users.createdAt,
+ });
+
+ return reply.send(updatedUser);
+}
+
+export async function deleteUserHandler(
+ request: FastifyRequest<{ Params: { id: number } }>,
+ reply: FastifyReply,
+) {
+ const db = request.server.db;
+ const { id } = request.params;
+
+ const existingUser = await db.query.users.findFirst({
+ where: eq(users.id, id),
+ });
+
+ if (!existingUser) {
+ return reply.notFound('Usuário não encontrado.');
+ }
+
+ if (id === request.user?.id) {
+ return reply.badRequest('Você não pode excluir a própria conta.');
+ }
+
+ await db.delete(users).where(eq(users.id, id));
+
+ return reply.code(204).send();
+}
diff --git a/api/src/modules/users/users.routes.ts b/api/src/modules/users/users.routes.ts
new file mode 100644
index 0000000..c7f1846
--- /dev/null
+++ b/api/src/modules/users/users.routes.ts
@@ -0,0 +1,56 @@
+import type { FastifyInstance } from 'fastify';
+import { createUserByAdminBodySchema, updateUserBodySchema, userIdParamsSchema } from './users.schema.js';
+import { listUsersHandler, createUserHandler, updateUserHandler, deleteUserHandler } from './users.controller.js';
+
+export default async function usersRoutes(fastify: FastifyInstance) {
+ // Middleware customizado para garantir que apenas admins possam acessar
+ const requireAdmin = async (request: any, reply: any) => {
+ if (request.user?.role !== 'admin') {
+ return reply.forbidden('Apenas administradores podem gerenciar usuários.');
+ }
+ };
+
+ // ── GET /users ─────────────────────────────────────────────────────────────
+ fastify.get('/', {
+ schema: {
+ tags: ['Users'],
+ summary: 'Lista todos os usuários (admin)',
+ },
+ preHandler: [fastify.authenticate, requireAdmin],
+ handler: listUsersHandler,
+ });
+
+ // ── POST /users ────────────────────────────────────────────────────────────
+ fastify.post('/', {
+ schema: {
+ tags: ['Users'],
+ summary: 'Cria um novo usuário (admin)',
+ body: createUserByAdminBodySchema,
+ },
+ preHandler: [fastify.authenticate, requireAdmin],
+ handler: createUserHandler,
+ });
+
+ // ── PATCH /users/:id ───────────────────────────────────────────────────────
+ fastify.patch('/:id', {
+ schema: {
+ tags: ['Users'],
+ summary: 'Atualiza um usuário (admin)',
+ params: userIdParamsSchema,
+ body: updateUserBodySchema,
+ },
+ preHandler: [fastify.authenticate, requireAdmin],
+ handler: updateUserHandler,
+ });
+
+ // ── DELETE /users/:id ──────────────────────────────────────────────────────
+ fastify.delete('/:id', {
+ schema: {
+ tags: ['Users'],
+ summary: 'Exclui um usuário (admin)',
+ params: userIdParamsSchema,
+ },
+ preHandler: [fastify.authenticate, requireAdmin],
+ handler: deleteUserHandler,
+ });
+}
diff --git a/api/src/modules/users/users.schema.ts b/api/src/modules/users/users.schema.ts
new file mode 100644
index 0000000..ff3f459
--- /dev/null
+++ b/api/src/modules/users/users.schema.ts
@@ -0,0 +1,25 @@
+import { z } from 'zod';
+import { USER_ROLES } from '@/db/schema.js';
+
+export const createUserByAdminBodySchema = z.object({
+ name: z.string().min(2).max(100).trim(),
+ email: z.string().email().toLowerCase().trim(),
+ password: z.string().min(8).max(100),
+ role: z.enum(USER_ROLES),
+});
+
+export type CreateUserByAdminBody = z.infer<typeof createUserByAdminBodySchema>;
+
+export const updateUserBodySchema = z.object({
+ name: z.string().min(2).max(100).trim().optional(),
+ email: z.string().email().toLowerCase().trim().optional(),
+ password: z.string().min(8).max(100).optional().or(z.literal('')),
+ role: z.enum(USER_ROLES).optional(),
+ isActive: z.boolean().optional(),
+});
+
+export type UpdateUserBody = z.infer<typeof updateUserBodySchema>;
+
+export const userIdParamsSchema = z.object({
+ id: z.coerce.number().positive(),
+});