diff --git a/prisma/migrations/20240915043817_init/migration.sql b/migrations/20240915043817_init/migration.sql similarity index 100% rename from prisma/migrations/20240915043817_init/migration.sql rename to migrations/20240915043817_init/migration.sql diff --git a/prisma/migrations/20240915175636_admin_users/migration.sql b/migrations/20240915175636_admin_users/migration.sql similarity index 100% rename from prisma/migrations/20240915175636_admin_users/migration.sql rename to migrations/20240915175636_admin_users/migration.sql diff --git a/prisma/migrations/20240915181843_add_unique_email/migration.sql b/migrations/20240915181843_add_unique_email/migration.sql similarity index 100% rename from prisma/migrations/20240915181843_add_unique_email/migration.sql rename to migrations/20240915181843_add_unique_email/migration.sql diff --git a/prisma/migrations/20240916021807_add_image/migration.sql b/migrations/20240916021807_add_image/migration.sql similarity index 100% rename from prisma/migrations/20240916021807_add_image/migration.sql rename to migrations/20240916021807_add_image/migration.sql diff --git a/prisma/migrations/20240924005213_/migration.sql b/migrations/20240924005213_/migration.sql similarity index 100% rename from prisma/migrations/20240924005213_/migration.sql rename to migrations/20240924005213_/migration.sql diff --git a/migrations/20240924014531_/migration.sql b/migrations/20240924014531_/migration.sql new file mode 100644 index 0000000..bbee4b9 --- /dev/null +++ b/migrations/20240924014531_/migration.sql @@ -0,0 +1,26 @@ +-- CreateEnum +CREATE TYPE "ProposalStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- AlterEnum +ALTER TYPE "UserRole" ADD VALUE 'PENDING'; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'PENDING'; + +-- CreateTable +CREATE TABLE "Proposal" ( + "id" SERIAL NOT NULL, + "companyName" TEXT NOT NULL, + "companyDocument" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "status" "ProposalStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Proposal_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Proposal_userId_key" ON "Proposal"("userId"); + +-- AddForeignKey +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/migrations/migration_lock.toml b/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/migrations/20240924023020_init/migration.sql b/prisma/migrations/20240924023020_init/migration.sql new file mode 100644 index 0000000..fac6e62 --- /dev/null +++ b/prisma/migrations/20240924023020_init/migration.sql @@ -0,0 +1,83 @@ +-- CreateEnum +CREATE TYPE "ProposalStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('PENDING', 'USER', 'ADMIN'); + +-- CreateTable +CREATE TABLE "Product" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "description" TEXT, + "imageUrl" TEXT, + "companyId" INTEGER NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Company" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "document" TEXT NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Unit" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "companyId" INTEGER NOT NULL, + + CONSTRAINT "Unit_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Proposal" ( + "id" SERIAL NOT NULL, + "companyName" TEXT NOT NULL, + "companyDocument" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "status" "ProposalStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Proposal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'PENDING', + "companyId" INTEGER, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_name_key" ON "Company"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_document_key" ON "Company"("document"); + +-- CreateIndex +CREATE UNIQUE INDEX "Proposal_userId_key" ON "Proposal"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Unit" ADD CONSTRAINT "Unit_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240924025207_update_register/migration.sql b/prisma/migrations/20240924025207_update_register/migration.sql new file mode 100644 index 0000000..c115e3f --- /dev/null +++ b/prisma/migrations/20240924025207_update_register/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `proposalValue` to the `Proposal` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Proposal" ADD COLUMN "proposalValue" DOUBLE PRECISION NOT NULL, +ADD COLUMN "unitId" INTEGER; + +-- AddForeignKey +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_unitId_fkey" FOREIGN KEY ("unitId") REFERENCES "Unit"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e628ad0..af152de 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,6 +4,9 @@ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -26,31 +29,53 @@ model Product { model Company { id Int @id @default(autoincrement()) name String @unique - document String @unique // Certifique-se de que esta linha existe + document String @unique users User[] // Relação com os usuários units Unit[] products Product[] } - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String - password String - role UserRole - company Company? @relation(fields: [companyId], references: [id]) - companyId Int? -} - model Unit { id Int @id @default(autoincrement()) name String company Company @relation(fields: [companyId], references: [id]) companyId Int + + Proposal Proposal[] +} + +model Proposal { + id Int @id @default(autoincrement()) + companyName String + companyDocument String + user User @relation(fields: [userId], references: [id], name: "ProposalToUser") + userId Int @unique + status ProposalStatus @default(PENDING) + createdAt DateTime @default(now()) + proposalValue Float + unitId Int? // Unidade da empresa relacionada a esta proposta + unit Unit? @relation(fields: [unitId], references: [id]) +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String + password String + role UserRole @default(PENDING) + proposal Proposal? @relation(name: "ProposalToUser") + company Company? @relation(fields: [companyId], references: [id]) + companyId Int? +} + +enum ProposalStatus { + PENDING + APPROVED + REJECTED } enum UserRole { + PENDING // Usuários que ainda não foram aprovados USER ADMIN -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index 599cccf..cedca35 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import paymentRoutes from './routes/paymentRoutes.js' import productRoutes from './routes/productRoutes.js' import createUserRouter from './routes/createUserRoutes.js' import createUnitRouter from './routes/createUnitRoutes.js' +import proposalRoutes from './routes/proposalRoutes.js' const app = express() @@ -19,9 +20,10 @@ app.get('/', (_req, res) => { // Defina as rotas para produtos e pagamentos app.use('/api/products', productRoutes) app.use('/api/payments', paymentRoutes) -app.use('/api', authRoutes) // Adicione '/api' como prefixo para as rotas de autenticação +app.use('/auth', authRoutes) // Adicione '/api' como prefixo para as rotas de autenticação app.use('/api', createUserRouter) // Adicione '/api' como prefixo para as rotas de autenticação app.use('/api', createUnitRouter) // Adicione '/api' como prefixo para as rotas de autenticação +app.use('/proposal', proposalRoutes) // Adicione '/api' como prefixo para as rotas de autenticação export default app diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 1e73bf2..aeabfc2 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient, UserRole } from '@prisma/client' // Importação do enum UserRole import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' import type { Request, Response } from 'express' @@ -7,89 +7,120 @@ import * as companyService from '../services/companyService' const prisma = new PrismaClient() const saltRounds = 10 +// Função de registro de usuário com proposta export const register = async (req: Request, res: Response) => { - const { email, password, name, role, companyName, companyDocument } = req.body + const { email, password, name, companyName, companyDocument, proposalValue, unitName } = req.body; try { // Verificar se o usuário já existe const existingUser = await prisma.user.findUnique({ where: { email }, - }) + }); if (existingUser) { - return res.status(400).json({ message: 'User already exists' }) + return res.status(400).json({ message: 'User already exists' }); } // Verificar se a empresa já existe const existingCompany = await prisma.company.findUnique({ - where: { name: companyName, document: companyDocument }, - }) + where: { document: companyDocument }, + }); - let companyId: number + let companyId: number; if (!existingCompany) { - // Criar a empresa + // Criar a empresa se não existir const newCompany = await companyService.createCompany({ name: companyName, - document: companyDocument, // Documento da empresa (CNPJ ou outro) - }) - - companyId = newCompany.id + document: companyDocument, + }); + companyId = newCompany.id; } else { - companyId = existingCompany.id + companyId = existingCompany.id; + } + + // Verificar ou criar a unidade da empresa + let unit = await prisma.unit.findFirst({ + where: { + name: unitName, + companyId: companyId + }, + }); + + if (!unit) { + // Criar unidade se não existir + unit = await prisma.unit.create({ + data: { + name: unitName, + company: { connect: { id: companyId } } + } + }); } // Criptografar a senha - const hashedPassword = await bcrypt.hash(password, saltRounds) + const hashedPassword = await bcrypt.hash(password, saltRounds); - // Criar um novo usuário e associá-lo à empresa + // Criar o usuário associado à proposta const newUser = await prisma.user.create({ data: { email, password: hashedPassword, name, - role: role || 'ADMIN', // O primeiro usuário será ADMIN - company: { connect: { id: companyId } }, // Associa o usuário à empresa - }, - }) + role: UserRole.PENDING, // Use o enum UserRole corretamente + companyId: companyId, // Associar o usuário à empresa + } + }); - // Gerar token JWT - const token = jwt.sign( - { id: newUser.id, email: newUser.email, role: newUser.role }, - process.env.JWT_SECRET || 'your_jwt_secret', - { expiresIn: '1d' } - ) + // Criar a proposta de usuário + const newProposal = await prisma.proposal.create({ + data: { + companyName: companyName, + companyDocument: companyDocument, + proposalValue: proposalValue, // Valor da proposta + unitId: unit.id, // Associar a unidade à proposta + userId: newUser.id // Associar a proposta ao usuário criado + }, + }); res.status(201).json({ - message: 'User and company created successfully', + message: 'User and proposal created, pending approval', user: newUser, - token, - }) + proposal: newProposal, + }); } catch (error) { - console.error(error) - res.status(500).json({ message: 'Server error' }) + console.error(error); + res.status(500).json({ message: 'Server error' }); } -} - +}; +// Função de login de usuário export const login = async (req: Request, res: Response) => { - const { email, password } = req.body + const { email, password } = req.body; try { // Verificar se o usuário existe const user = await prisma.user.findUnique({ where: { email }, - }) + }); if (!user) { - return res.status(401).json({ message: 'Invalid credentials' }) + return res.status(401).json({ message: 'Invalid credentials' }); + } + + // Verificar se a proposta do usuário foi aprovada + const proposal = await prisma.proposal.findUnique({ + where: { userId: user.id }, + }); + + if (!proposal || proposal.status !== 'APPROVED') { + return res.status(403).json({ message: 'Proposal is not approved' }); } // Verificar se a senha está correta - const isPasswordValid = await bcrypt.compare(password, user.password) + const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { - return res.status(401).json({ message: 'Invalid credentials' }) + return res.status(401).json({ message: 'Invalid credentials' }); } // Gerar token JWT @@ -97,7 +128,7 @@ export const login = async (req: Request, res: Response) => { { id: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET || 'your_jwt_secret', { expiresIn: '1d' } - ) + ); // Retornar o token e as informações do usuário res.status(200).json({ @@ -108,9 +139,9 @@ export const login = async (req: Request, res: Response) => { email: user.email, role: user.role, }, - }) + }); } catch (error) { - console.error(error) - res.status(500).json({ message: 'Server error' }) + console.error(error); + res.status(500).json({ message: 'Server error' }); } -} \ No newline at end of file +}; diff --git a/src/controllers/proposalController.ts b/src/controllers/proposalController.ts new file mode 100644 index 0000000..c7fe9e6 --- /dev/null +++ b/src/controllers/proposalController.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client' +import type { Request, Response } from 'express' + +const prisma = new PrismaClient() +export const approveProposal = async (req: Request, res: Response) => { + const { proposalId } = req.body; + + try { + const proposal = await prisma.proposal.findUnique({ + where: { id: proposalId }, + include: { user: true }, + }); + + if (!proposal) { + return res.status(404).json({ message: 'Proposal not found' }); + } + + // Atualizar o status da proposta para APROVADA + await prisma.proposal.update({ + where: { id: proposalId }, + data: { status: 'APPROVED' }, + }); + + // Atualizar o usuário para 'USER' após aprovação + await prisma.user.update({ + where: { id: proposal.userId }, + data: { role: 'USER' }, + }); + + res.status(200).json({ message: 'Proposal approved and user activated' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Server error' }); + } +}; + +export const rejectProposal = async (req: Request, res: Response) => { + const { proposalId } = req.body; + + try { + const proposal = await prisma.proposal.findUnique({ + where: { id: proposalId }, + }); + + if (!proposal) { + return res.status(404).json({ message: 'Proposal not found' }); + } + + // Atualizar o status da proposta para REJEITADA + await prisma.proposal.update({ + where: { id: proposalId }, + data: { status: 'REJECTED' }, + }); + + res.status(200).json({ message: 'Proposal rejected' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Server error' }); + } +}; +// Função para listar propostas +export const listProposals = async (req: Request, res: Response) => { + try { + // Buscar todas as propostas, incluindo informações de usuário, unidade e empresa + const proposals = await prisma.proposal.findMany({ + include: { + user: true, // Inclui informações do usuário relacionado à proposta + unit: true // Inclui a unidade relacionada à proposta + }, + }); + + res.status(200).json(proposals); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Server error' }); + } +}; diff --git a/src/routes/proposalRoutes.ts b/src/routes/proposalRoutes.ts new file mode 100644 index 0000000..769fc17 --- /dev/null +++ b/src/routes/proposalRoutes.ts @@ -0,0 +1,15 @@ +import express from 'express' +import { approveProposal,listProposals,rejectProposal } from '@/controllers/proposalController' + +const proposalRoutes = express.Router() + +// Rota para aprovar proposta +proposalRoutes.post('/approve', approveProposal) + +// Rota para rejeitar proposta +proposalRoutes.post('/reject', rejectProposal) + +proposalRoutes.get('/proposals', listProposals) + + +export default proposalRoutes