├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── database │ └── data-source.ts ├── entities │ ├── Article.ts │ ├── Category.ts │ └── User.ts ├── middleware │ ├── authMiddleware.ts │ └── validateRequest.ts ├── routes │ ├── articleRoutes.ts │ ├── authRoutes.ts │ ├── categoryRoutes.ts │ └── userRoutes.ts ├── server.ts ├── services │ ├── DatabaseCleanupService.ts │ └── LoggerService.ts └── utils │ └── FormatDate.ts ├── swagger.yaml └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .env 4 | *.log 5 | database.sqlite -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API - CMS For QA's (Gestão de Conteúdo) 2 | 3 | Uma API RESTful para gestão de conteúdo com autenticação JWT, permitindo gerenciar usuários, categorias e artigos. 4 | 5 | ## Requisitos 6 | 7 | - Node.js (versão 14 ou superior) 8 | - npm ou yarn 9 | 10 | ## Instalação 11 | 12 | 1. Clone o repositório 13 | 2. Instale as dependências: 14 | ``` 15 | npm install 16 | ``` 17 | 18 | 3. Inicie o servidor de desenvolvimento: 19 | ``` 20 | npm run dev 21 | ``` 22 | 23 | O servidor estará rodando em `http://localhost:3000` 24 | 25 | ## Autenticação 26 | 27 | A API utiliza autenticação JWT (JSON Web Token). Para acessar endpoints protegidos: 28 | 29 | 1. Faça login através do endpoint `/auth/login` 30 | 2. Use o token retornado no header `Authorization: Bearer ` 31 | 32 | Exemplo de login: 33 | 34 | `POST /auth/login` 35 | ```json 36 | { 37 | "nome": "Usuario", 38 | "email": "usuario@email.com" 39 | } 40 | ``` 41 | 42 | ## Endpoints 43 | 44 | ### Autenticação 45 | - `POST /auth/login`: Login do usuário (retorna token JWT) 46 | 47 | ### Usuários 48 | 49 | - `POST /usuarios`: Criar usuário (público) 50 | ```json 51 | { 52 | "nome": "Usuario", 53 | "email": "usuario@email.com" 54 | } 55 | ``` 56 | - `GET /usuarios`: Listar usuários (com filtros opcionais) 57 | - Query params: `nome`, `email` 58 | - `GET /usuarios/:id`: Buscar usuário por ID 59 | - `PUT /usuarios/:id`: Atualizar usuário 60 | - `DELETE /usuarios/:id`: Excluir usuário 61 | 62 | ### Categorias 63 | 64 | - `POST /categorias`: Criar categoria 65 | ```json 66 | { 67 | "nome": "Tecnologia", 68 | "descricao": "Artigos sobre tecnologia" 69 | } 70 | ``` 71 | - `GET /categorias`: Listar categorias 72 | - Query params: `nome` 73 | - `GET /categorias/:id`: Buscar categoria por ID 74 | - `PUT /categorias/:id`: Atualizar categoria 75 | - `DELETE /categorias/:id`: Excluir categoria 76 | 77 | ### Artigos 78 | 79 | - `POST /artigos`: Criar artigo 80 | ```json 81 | { 82 | "titulo": "Introdução aos Testes Automatizados", 83 | "conteudo": "Exemplos de ferramentas de testes automatizados...", 84 | "nomeAutor": "Usuario", 85 | "nomeCategoria": "Tecnologia", 86 | "dataPublicacao": "2024-03-21T10:00:00Z" 87 | } 88 | ``` 89 | - `GET /artigos`: Listar artigos (com paginação e filtros) 90 | - Query params: 91 | - `categoriaId`: UUID da categoria 92 | - `autorId`: UUID do autor 93 | - `page`: Número da página (default: 1) 94 | - `limit`: Itens por página (default: 10) 95 | - `GET /artigos/:id`: Buscar artigo por ID 96 | - `PUT /artigos/:id`: Atualizar artigo 97 | - `DELETE /artigos/:id`: Excluir artigo 98 | 99 | ## Modelos de Dados 100 | 101 | ### Usuário (User) 102 | - `id`: UUID (automático) 103 | - `nome`: string (obrigatório) 104 | - `email`: string (obrigatório, único) 105 | - `dataCriacao`: datetime (automático) 106 | - `artigos`: array de Artigos 107 | 108 | ### Categoria (Category) 109 | - `id`: UUID (automático) 110 | - `nome`: string (obrigatório, único) 111 | - `descricao`: string (opcional) 112 | - `dataCriacao`: datetime (automático) 113 | - `artigos`: array de Artigos 114 | 115 | ### Artigo (Article) 116 | - `id`: UUID (automático) 117 | - `titulo`: string (obrigatório, máx 100 caracteres) 118 | - `conteudo`: texto (obrigatório) 119 | - `autorId`: UUID (obrigatório, referência User) 120 | - `categoriaId`: UUID (obrigatório, referência Category) 121 | - `dataPublicacao`: datetime 122 | - `dataCriacao`: datetime (automático) 123 | - `autor`: objeto User 124 | - `categoria`: objeto Category 125 | 126 | ## Regras de Negócio 127 | 128 | 1. Todos os endpoints (exceto criação de usuário e login) requerem autenticação JWT 129 | 2. Não é possível excluir um usuário que possui artigos vinculados 130 | 3. Não é possível excluir uma categoria que possui artigos vinculados 131 | 4. Email do usuário deve ser único 132 | 5. Nome da categoria deve ser único 133 | 6. Título do artigo deve ter no máximo 100 caracteres 134 | 7. Ao criar um artigo, o autor e a categoria são referenciados por nome 135 | 136 | ## Respostas de Erro 137 | 138 | A API retorna erros no seguinte formato: 139 | ```json 140 | { 141 | "erro": "Mensagem principal do erro", 142 | "errors": [ 143 | { 144 | "msg": "Detalhamento do erro", 145 | "param": "Campo relacionado", 146 | "location": "Localização do erro" 147 | } 148 | ] 149 | } 150 | ``` 151 |
152 |

< Contato >

153 |
154 | 👤 Autor: João Vitor Gomes
155 | 📧 Email: bgomes.joaovitor@gmail.com 156 |
157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-api", 3 | "version": "1.0.0", 4 | "description": "API de Gestão de Conteúdo", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "start": "node dist/server.js", 8 | "dev": "ts-node-dev --respawn --transpile-only src/server.ts", 9 | "build": "tsc", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@types/bcrypt": "^5.0.2", 14 | "bcrypt": "^5.1.1", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.4.1", 17 | "express": "^4.18.2", 18 | "express-validator": "^7.0.1", 19 | "jsonwebtoken": "^9.0.2", 20 | "reflect-metadata": "^0.2.1", 21 | "sqlite3": "^5.1.7", 22 | "swagger-ui-express": "^5.0.0", 23 | "typeorm": "^0.3.20", 24 | "yamljs": "^0.3.0" 25 | }, 26 | "devDependencies": { 27 | "@types/cors": "^2.8.17", 28 | "@types/express": "^4.17.21", 29 | "@types/jsonwebtoken": "^9.0.5", 30 | "@types/node": "^20.11.16", 31 | "@types/swagger-ui-express": "^4.1.6", 32 | "@types/yamljs": "^0.2.34", 33 | "ts-node-dev": "^2.0.0", 34 | "typescript": "^5.3.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/database/data-source.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { DataSource } from "typeorm"; 3 | import { User } from "../entities/User"; 4 | import { Category } from "../entities/Category"; 5 | import { Article } from "../entities/Article"; 6 | 7 | export const AppDataSource = new DataSource({ 8 | type: "sqlite", 9 | database: "database.sqlite", 10 | synchronize: true, 11 | logging: false, 12 | entities: [User, Category, Article], 13 | migrations: [], 14 | subscribers: [], 15 | }); -------------------------------------------------------------------------------- /src/entities/Article.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from "typeorm"; 2 | import { User } from "./User"; 3 | import { Category } from "./Category"; 4 | 5 | @Entity("articles") 6 | export class Article { 7 | @PrimaryGeneratedColumn("uuid") 8 | id: string; 9 | 10 | @Column({ length: 100 }) 11 | titulo: string; 12 | 13 | @Column("text") 14 | conteudo: string; 15 | 16 | @Column({ name: "autor_id" }) 17 | autorId: string; 18 | 19 | @Column({ name: "categoria_id" }) 20 | categoriaId: string; 21 | 22 | @Column({ name: "data_publicacao" }) 23 | dataPublicacao: Date; 24 | 25 | @CreateDateColumn({ name: "data_criacao" }) 26 | dataCriacao: Date; 27 | 28 | @ManyToOne(() => User, user => user.artigos) 29 | @JoinColumn({ name: "autor_id" }) 30 | autor: User; 31 | 32 | @ManyToOne(() => Category, category => category.artigos) 33 | @JoinColumn({ name: "categoria_id" }) 34 | categoria: Category; 35 | } -------------------------------------------------------------------------------- /src/entities/Category.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, OneToMany, Index, CreateDateColumn } from "typeorm"; 2 | import { Article } from "./Article"; 3 | 4 | @Entity("categories") 5 | export class Category { 6 | @PrimaryGeneratedColumn("uuid") 7 | id: string; 8 | 9 | @Column() 10 | @Index({ unique: true }) 11 | nome: string; 12 | 13 | @Column({ nullable: true }) 14 | descricao: string; 15 | 16 | @CreateDateColumn({ name: "data_criacao" }) 17 | dataCriacao: Date; 18 | 19 | @OneToMany(() => Article, article => article.categoria) 20 | artigos: Article[]; 21 | } -------------------------------------------------------------------------------- /src/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany, Index } from "typeorm"; 2 | import { Article } from "./Article"; 3 | 4 | @Entity("users") 5 | export class User { 6 | @PrimaryGeneratedColumn("uuid") 7 | id: string; 8 | 9 | @Column({ name: "nome_completo" }) 10 | nomeCompleto: string; 11 | 12 | @Column({ name: "nome_usuario" }) 13 | @Index({ unique: true }) 14 | nomeUsuario: string; 15 | 16 | @Column() 17 | @Index({ unique: true }) 18 | email: string; 19 | 20 | @Column({ select: false }) 21 | senha: string; 22 | 23 | @CreateDateColumn({ name: "data_criacao" }) 24 | dataCriacao: Date; 25 | 26 | @OneToMany(() => Article, article => article.autor) 27 | artigos: Article[]; 28 | } -------------------------------------------------------------------------------- /src/middleware/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { LoggerService } from "../services/LoggerService"; 4 | 5 | const JWT_SECRET = process.env.JWT_SECRET || "sua_chave_secreta_aqui"; 6 | 7 | export interface AuthRequest extends Request { 8 | userId?: string; 9 | } 10 | 11 | export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => { 12 | const authHeader = req.headers.authorization; 13 | 14 | if (!authHeader) { 15 | LoggerService.warn("Tentativa de acesso sem token", { path: req.path, method: req.method }); 16 | return res.status(401).json({ erro: "Token não fornecido" }); 17 | } 18 | 19 | const parts = authHeader.split(" "); 20 | 21 | if (parts.length !== 2) { 22 | LoggerService.warn("Token mal formatado", { path: req.path, method: req.method, token: authHeader }); 23 | return res.status(401).json({ erro: "Token mal formatado" }); 24 | } 25 | 26 | const [scheme, token] = parts; 27 | 28 | if (!/^Bearer$/i.test(scheme)) { 29 | LoggerService.warn("Tipo de autenticação inválido", { path: req.path, method: req.method, scheme}); 30 | return res.status(401).json({ erro: "Token mal formatado" }); 31 | } 32 | 33 | try { 34 | const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; 35 | req.userId = decoded.id; 36 | LoggerService.info("Usuário autenticado com sucesso", { userId: decoded.id, }); 37 | return next(); 38 | } catch (error) { 39 | LoggerService.error("Token inválido", { path: req.path, method: req.method, error }); 40 | return res.status(401).json({ erro: "Token inválido" }); 41 | } 42 | }; -------------------------------------------------------------------------------- /src/middleware/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { validationResult } from "express-validator"; 3 | import { LoggerService } from "../services/LoggerService"; 4 | 5 | export const validateRequest = ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ): Response | void => { 10 | const errors = validationResult(req); 11 | if (!errors.isEmpty()) { 12 | const validationErrors = errors.array(); 13 | LoggerService.warn(`Validação falhou na rota ${req.method} ${req.path}`, { errors: validationErrors }); 14 | return res.status(400).json({ errors: validationErrors }); 15 | } 16 | LoggerService.info(`Validação bem-sucedida na rota ${req.method} ${req.path}`); 17 | next(); 18 | }; -------------------------------------------------------------------------------- /src/routes/articleRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import { AppDataSource } from "../database/data-source"; 4 | import { Article } from "../entities/Article"; 5 | import { User } from "../entities/User"; 6 | import { Category } from "../entities/Category"; 7 | import { validateRequest } from "../middleware/validateRequest"; 8 | import { LoggerService } from "../services/LoggerService"; 9 | 10 | const router = Router(); 11 | const articleRepository = AppDataSource.getRepository(Article); 12 | const userRepository = AppDataSource.getRepository(User); 13 | const categoryRepository = AppDataSource.getRepository(Category); 14 | 15 | router.post("/", 16 | [ 17 | body("titulo") 18 | .notEmpty().withMessage("Título é obrigatório") 19 | .isLength({ max: 100 }).withMessage("Título deve ter no máximo 100 caracteres"), 20 | body("conteudo").notEmpty().withMessage("Conteúdo é obrigatório"), 21 | body("nomeAutor").notEmpty().withMessage("Nome do autor é obrigatório"), 22 | body("nomeCategoria").notEmpty().withMessage("Nome da categoria é obrigatório"), 23 | body("dataPublicacao") 24 | .optional() 25 | .isISO8601().withMessage("Data de publicação deve estar no formato ISO8601"), 26 | validateRequest 27 | ], 28 | async (req: Request, res: Response) => { 29 | try { 30 | const { titulo, conteudo, nomeAutor, nomeCategoria, dataPublicacao } = req.body; 31 | LoggerService.info(`Iniciando criação de artigo: ${titulo}`); 32 | 33 | const autor = await userRepository.findOne({ 34 | where: { nomeUsuario: nomeAutor } 35 | }); 36 | 37 | if (!autor) { 38 | LoggerService.warn(`Autor não encontrado: ${nomeAutor}`); 39 | return res.status(404).json({ erro: "Autor não encontrado" }); 40 | } 41 | 42 | const categoria = await categoryRepository.findOne({ 43 | where: { nome: nomeCategoria } 44 | }); 45 | 46 | if (!categoria) { 47 | LoggerService.warn(`Categoria não encontrada: ${nomeCategoria}`); 48 | return res.status(404).json({ erro: "Categoria não encontrada" }); 49 | } 50 | 51 | const article = new Article(); 52 | article.titulo = titulo; 53 | article.conteudo = conteudo; 54 | article.autor = autor; 55 | article.categoria = categoria; 56 | article.dataPublicacao = dataPublicacao ? new Date(dataPublicacao) : new Date(); 57 | 58 | await articleRepository.save(article); 59 | LoggerService.info(`Artigo criado com sucesso: ${article.id}`); 60 | 61 | return res.status(201).json(article); 62 | } catch (error) { 63 | LoggerService.error("Erro ao criar artigo", error); 64 | return res.status(500).json({ erro: "Erro ao criar artigo" }); 65 | } 66 | } 67 | ); 68 | 69 | router.get("/", async (req: Request, res: Response) => { 70 | try { 71 | const { categoria_id, autor_id, page = 1, limit = 10 } = req.query; 72 | LoggerService.info(`Listando artigos - page: ${page}, limit: ${limit}`, { categoria_id, autor_id }); 73 | 74 | const skip = (Number(page) - 1) * Number(limit); 75 | let where: any = {}; 76 | 77 | if (categoria_id) { 78 | where.categoriaId = categoria_id; 79 | } 80 | if (autor_id) { 81 | where.autorId = autor_id; 82 | } 83 | 84 | const [articles, total] = await articleRepository.findAndCount({ 85 | where, 86 | skip, 87 | take: Number(limit), 88 | relations: ["autor", "categoria"], 89 | order: { dataPublicacao: "DESC" } 90 | }); 91 | 92 | LoggerService.info(`Artigos listados com sucesso. Total: ${total}`); 93 | return res.json({ 94 | data: articles, 95 | total, 96 | page: Number(page), 97 | lastPage: Math.ceil(total / Number(limit)) 98 | }); 99 | } catch (error) { 100 | LoggerService.error("Erro ao listar artigos", error); 101 | return res.status(500).json({ erro: "Erro ao listar artigos" }); 102 | } 103 | }); 104 | 105 | router.get("/:id", async (req: Request, res: Response) => { 106 | try { 107 | LoggerService.info(`Buscando artigo: ${req.params.id}`); 108 | const article = await articleRepository.findOne({ 109 | where: { id: req.params.id }, 110 | relations: ["autor", "categoria"] 111 | }); 112 | if (!article) { 113 | LoggerService.warn(`Artigo não encontrado: ${req.params.id}`); 114 | return res.status(404).json({ erro: "Artigo não encontrado" }); 115 | } 116 | LoggerService.info(`Artigo encontrado: ${req.params.id}`); 117 | return res.json(article); 118 | } catch (error) { 119 | LoggerService.error(`Erro ao buscar artigo: ${req.params.id}`, error); 120 | return res.status(500).json({ erro: "Erro ao buscar artigo" }); 121 | } 122 | }); 123 | 124 | router.put("/:id", 125 | [ 126 | body("titulo") 127 | .optional() 128 | .isLength({ max: 100 }).withMessage("Título deve ter no máximo 100 caracteres"), 129 | body("conteudo") 130 | .optional() 131 | .notEmpty().withMessage("Conteúdo não pode ser vazio"), 132 | validateRequest 133 | ], 134 | async (req: Request, res: Response) => { 135 | try { 136 | LoggerService.info(`Iniciando atualização do artigo: ${req.params.id}`); 137 | const article = await articleRepository.findOne({ 138 | where: { id: req.params.id }, 139 | relations: ["autor", "categoria"] 140 | }); 141 | 142 | if (!article) { 143 | LoggerService.warn(`Artigo não encontrado para atualização: ${req.params.id}`); 144 | return res.status(404).json({ erro: "Artigo não encontrado" }); 145 | } 146 | 147 | const changes = Object.entries(req.body).some(([key, value]) => 148 | article[key as keyof Article] !== value 149 | ); 150 | 151 | if (!changes) { 152 | LoggerService.info(`Nenhuma alteração necessária para o artigo: ${req.params.id}`); 153 | return res.status(200).json({ mensagem: "Não houve alterações" }); 154 | } 155 | 156 | articleRepository.merge(article, req.body); 157 | await articleRepository.save(article); 158 | LoggerService.info(`Artigo atualizado com sucesso: ${req.params.id}`); 159 | 160 | return res.json(article); 161 | } catch (error) { 162 | LoggerService.error(`Erro ao atualizar artigo: ${req.params.id}`, error); 163 | return res.status(500).json({ erro: "Erro ao atualizar artigo" }); 164 | } 165 | } 166 | ); 167 | 168 | router.delete("/:id", async (req: Request, res: Response) => { 169 | try { 170 | LoggerService.info(`Iniciando exclusão do artigo: ${req.params.id}`); 171 | const article = await articleRepository.findOne({ where: { id: req.params.id } }); 172 | if (!article) { 173 | LoggerService.warn(`Artigo não encontrado para exclusão: ${req.params.id}`); 174 | return res.status(404).json({ erro: "Artigo não encontrado" }); 175 | } 176 | 177 | await articleRepository.remove(article); 178 | LoggerService.info(`Artigo excluído com sucesso: ${req.params.id}`); 179 | return res.status(204).send(); 180 | } catch (error) { 181 | LoggerService.error(`Erro ao excluir artigo: ${req.params.id}`, error); 182 | return res.status(500).json({ erro: "Erro ao excluir artigo" }); 183 | } 184 | }); 185 | 186 | export default router; -------------------------------------------------------------------------------- /src/routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import jwt from "jsonwebtoken"; 4 | import bcrypt from "bcrypt"; 5 | import { AppDataSource } from "../database/data-source"; 6 | import { User } from "../entities/User"; 7 | import { validateRequest } from "../middleware/validateRequest"; 8 | import { LoggerService } from "../services/LoggerService"; 9 | 10 | const router = Router(); 11 | const userRepository = AppDataSource.getRepository(User); 12 | 13 | const JWT_SECRET = process.env.JWT_SECRET || "sua_chave_secreta_aqui"; 14 | 15 | router.post("/login", 16 | [ 17 | body("email").isEmail().withMessage("Email inválido"), 18 | body("senha").notEmpty().withMessage("Senha é obrigatória"), 19 | validateRequest 20 | ], 21 | async (req: Request, res: Response) => { 22 | try { 23 | const { email, senha } = req.body; 24 | LoggerService.info("Tentativa de login", { email }); 25 | 26 | const user = await userRepository.findOne({ 27 | where: { email }, 28 | select: ["id", "email", "senha", "nomeCompleto", "nomeUsuario"] 29 | }); 30 | 31 | if (!user) { 32 | LoggerService.warn("Tentativa de login com email ou senha inválidos", { email }); 33 | return res.status(401).json({ erro: "Email ou senha inválidos" }); 34 | } 35 | 36 | const senhaValida = await bcrypt.compare(senha, user.senha); 37 | if (!senhaValida) { 38 | LoggerService.warn("Tentativa de login com email ou senha inválidos", { email }); 39 | return res.status(401).json({ erro: "Email ou senha inválidos" }); 40 | } 41 | 42 | const token = jwt.sign({ id: user.id }, JWT_SECRET, { 43 | expiresIn: "1d" 44 | }); 45 | 46 | const { senha: _, ...userWithoutPassword } = user; 47 | 48 | LoggerService.info("Login realizado com sucesso", { userId: user.id }); 49 | 50 | return res.json({ 51 | user: userWithoutPassword, 52 | token 53 | }); 54 | } catch (error) { 55 | LoggerService.error("Erro ao realizar login", error); 56 | return res.status(500).json({ erro: "Erro ao realizar login" }); 57 | } 58 | } 59 | ); 60 | 61 | export default router; -------------------------------------------------------------------------------- /src/routes/categoryRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import { AppDataSource } from "../database/data-source"; 4 | import { Category } from "../entities/Category"; 5 | import { validateRequest } from "../middleware/validateRequest"; 6 | import { LoggerService } from "../services/LoggerService"; 7 | 8 | const router = Router(); 9 | const categoryRepository = AppDataSource.getRepository(Category); 10 | 11 | interface CreateCategoryRequest { 12 | nome: string; 13 | descricao?: string; 14 | } 15 | 16 | interface UpdateCategoryRequest { 17 | descricao: string; 18 | } 19 | 20 | router.post("/", 21 | [ 22 | body("nome").notEmpty().withMessage("Nome é obrigatório"), 23 | validateRequest 24 | ], 25 | async (req: Request<{}, {}, CreateCategoryRequest>, res: Response) => { 26 | try { 27 | const { nome, descricao } = req.body; 28 | LoggerService.info(`Iniciando criação de categoria: ${nome}`); 29 | 30 | const existingCategory = await categoryRepository.findOne({ where: { nome } }); 31 | if (existingCategory) { 32 | LoggerService.warn(`Tentativa de criar categoria com nome duplicado: ${nome}`); 33 | return res.status(400).json({ erro: "Nome de categoria já existe" }); 34 | } 35 | 36 | const category = categoryRepository.create({ nome, descricao }); 37 | await categoryRepository.save(category); 38 | 39 | LoggerService.info(`Categoria criada com sucesso: ${category.id}`); 40 | return res.status(201).json(category); 41 | } catch (error) { 42 | LoggerService.error("Erro ao criar categoria", error); 43 | return res.status(500).json({ erro: "Erro ao criar categoria" }); 44 | } 45 | } 46 | ); 47 | 48 | router.get("/", async (req: Request<{}, {}, {}, { nome?: string }>, res: Response) => { 49 | try { 50 | const { nome } = req.query; 51 | LoggerService.info("Listando categorias", nome ? { filtroNome: nome } : undefined); 52 | 53 | let where = {}; 54 | if (nome) { 55 | where = { nome: nome }; 56 | } 57 | 58 | const categories = await categoryRepository.find({ where }); 59 | LoggerService.info(`Categorias listadas com sucesso. Total: ${categories.length}`); 60 | return res.json(categories); 61 | } catch (error) { 62 | LoggerService.error("Erro ao listar categorias", error); 63 | return res.status(500).json({ erro: "Erro ao listar categorias" }); 64 | } 65 | }); 66 | 67 | router.get("/:id", async (req: Request<{ id: string }>, res: Response) => { 68 | try { 69 | LoggerService.info(`Buscando categoria: ${req.params.id}`); 70 | const category = await categoryRepository.findOne({ where: { id: req.params.id } }); 71 | if (!category) { 72 | LoggerService.warn(`Categoria não encontrada: ${req.params.id}`); 73 | return res.status(404).json({ erro: "Categoria não encontrada" }); 74 | } 75 | LoggerService.info(`Categoria encontrada: ${req.params.id}`); 76 | return res.json(category); 77 | } catch (error) { 78 | LoggerService.error(`Erro ao buscar categoria: ${req.params.id}`, error); 79 | return res.status(500).json({ erro: "Erro ao buscar categoria" }); 80 | } 81 | }); 82 | 83 | router.put("/:id", 84 | [ 85 | body("descricao").optional().notEmpty().withMessage("Descrição não pode ser vazia"), 86 | validateRequest 87 | ], 88 | async (req: Request<{ id: string }, {}, UpdateCategoryRequest>, res: Response) => { 89 | try { 90 | LoggerService.info(`Iniciando atualização da categoria: ${req.params.id}`); 91 | const category = await categoryRepository.findOne({ where: { id: req.params.id } }); 92 | if (!category) { 93 | LoggerService.warn(`Categoria não encontrada para atualização: ${req.params.id}`); 94 | return res.status(404).json({ erro: "Categoria não encontrada" }); 95 | } 96 | 97 | categoryRepository.merge(category, { descricao: req.body.descricao }); 98 | await categoryRepository.save(category); 99 | 100 | LoggerService.info(`Categoria atualizada com sucesso: ${req.params.id}`); 101 | return res.json(category); 102 | } catch (error) { 103 | LoggerService.error(`Erro ao atualizar categoria: ${req.params.id}`, error); 104 | return res.status(500).json({ erro: "Erro ao atualizar categoria" }); 105 | } 106 | } 107 | ); 108 | 109 | router.delete("/:id", async (req: Request<{ id: string }>, res: Response) => { 110 | try { 111 | LoggerService.info(`Iniciando exclusão da categoria: ${req.params.id}`); 112 | const category = await categoryRepository.findOne({ 113 | where: { id: req.params.id }, 114 | relations: ["artigos"] 115 | }); 116 | 117 | if (!category) { 118 | LoggerService.warn(`Categoria não encontrada para exclusão: ${req.params.id}`); 119 | return res.status(404).json({ erro: "Categoria não encontrada" }); 120 | } 121 | 122 | if (category.artigos && category.artigos.length > 0) { 123 | LoggerService.warn(`Tentativa de excluir categoria com artigos vinculados: ${req.params.id}`); 124 | return res.status(400).json({ erro: "Não é possível excluir categoria com artigos vinculados" }); 125 | } 126 | 127 | await categoryRepository.remove(category); 128 | LoggerService.info(`Categoria excluída com sucesso: ${req.params.id}`); 129 | return res.status(204).send(); 130 | } catch (error) { 131 | LoggerService.error(`Erro ao excluir categoria: ${req.params.id}`, error); 132 | return res.status(500).json({ erro: "Erro ao excluir categoria" }); 133 | } 134 | }); 135 | 136 | export default router; -------------------------------------------------------------------------------- /src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import bcrypt from "bcrypt"; 4 | import { AppDataSource } from "../database/data-source"; 5 | import { User } from "../entities/User"; 6 | import { validateRequest } from "../middleware/validateRequest"; 7 | import { QueryFailedError } from "typeorm"; 8 | import { LoggerService } from "../services/LoggerService"; 9 | 10 | const router = Router(); 11 | const userRepository = AppDataSource.getRepository(User); 12 | 13 | interface CreateUserRequest { 14 | nomeCompleto: string; 15 | nomeUsuario: string; 16 | email: string; 17 | senha: string; 18 | } 19 | 20 | interface UpdateUserRequest { 21 | nomeCompleto?: string; 22 | nomeUsuario?: string; 23 | email?: string; 24 | senha?: string; 25 | } 26 | 27 | router.post("/", 28 | [ 29 | body("nomeCompleto").notEmpty().withMessage("Nome completo é obrigatório"), 30 | body("nomeUsuario").notEmpty().withMessage("Nome de usuário é obrigatório"), 31 | body("email").isEmail().withMessage("Email inválido"), 32 | body("senha") 33 | .isLength({ min: 6 }).withMessage("Senha deve ter no mínimo 6 caracteres") 34 | .matches(/\d/).withMessage("Senha deve conter pelo menos um número") 35 | .matches(/[A-Z]/).withMessage("Senha deve conter pelo menos uma letra maiúscula"), 36 | validateRequest 37 | ], 38 | async (req: Request<{}, {}, CreateUserRequest>, res: Response) => { 39 | try { 40 | const [existingUserByUsername, existingUserByEmail] = await Promise.all([ 41 | userRepository.findOne({ where: { nomeUsuario: req.body.nomeUsuario } }), 42 | userRepository.findOne({ where: { email: req.body.email } }) 43 | ]); 44 | 45 | const errors = []; 46 | if (existingUserByUsername) { 47 | errors.push({ campo: "nomeUsuario", mensagem: "Nome de usuário já está em uso" }); 48 | } 49 | if (existingUserByEmail) { 50 | errors.push({ campo: "email", mensagem: "E-mail já está em uso" }); 51 | } 52 | 53 | if (errors.length > 0) { 54 | LoggerService.warn("Tentativa de criar usuário com dados duplicados", 55 | { 56 | nomeUsuario: req.body.nomeUsuario, 57 | email: req.body.email 58 | }); 59 | return res.status(400).json({ erros: errors }); 60 | } 61 | 62 | const hashedPassword = await bcrypt.hash(req.body.senha, 10); 63 | 64 | const user = userRepository.create({ 65 | nomeCompleto: req.body.nomeCompleto, 66 | nomeUsuario: req.body.nomeUsuario, 67 | email: req.body.email, 68 | senha: hashedPassword 69 | }); 70 | 71 | await userRepository.save(user); 72 | LoggerService.info("Novo usuário criado com sucesso", 73 | { 74 | id: user.id, 75 | nomeUsuario: user.nomeUsuario 76 | }); 77 | 78 | const { senha: _, ...userWithoutPassword } = user as User & { senha: string }; 79 | return res.status(201).json(userWithoutPassword); 80 | } catch (error) { 81 | LoggerService.error("Erro ao criar usuário", error); 82 | return res.status(500).json({ erro: "Erro ao criar usuário" }); 83 | } 84 | } 85 | ); 86 | 87 | router.get("/", async (req: Request<{}, {}, {}, { nomeUsuario?: string; email?: string }>, res: Response) => { 88 | try { 89 | const { nomeUsuario, email } = req.query; 90 | LoggerService.info("Buscando usuários", { filtros: { nomeUsuario, email } }); 91 | 92 | let where = {}; 93 | if (nomeUsuario) where = { ...where, nomeUsuario }; 94 | if (email) where = { ...where, email }; 95 | 96 | const users = await userRepository.find({ where }); 97 | LoggerService.info("Quantidade de usuários encontrados", { users: users.length }); 98 | return res.json(users); 99 | } catch (error) { 100 | LoggerService.error("Erro ao listar usuários", error); 101 | return res.status(500).json({ erro: "Erro ao listar usuários" }); 102 | } 103 | }); 104 | 105 | router.get("/:id", async (req: Request<{ id: string }>, res: Response) => { 106 | try { 107 | LoggerService.info("Buscando usuário por ID", { id: req.params.id }); 108 | const user = await userRepository.findOne({ where: { id: req.params.id } }); 109 | 110 | if (!user) { 111 | LoggerService.warn("Usuário não encontrado", { id: req.params.id }); 112 | return res.status(404).json({ erro: "Usuário não encontrado" }); 113 | } 114 | return res.json(user); 115 | } catch (error) { 116 | LoggerService.error("Erro ao buscar usuário", error); 117 | return res.status(500).json({ erro: "Erro ao buscar usuário" }); 118 | } 119 | }); 120 | 121 | router.put("/:id", 122 | [ 123 | body("nomeCompleto").optional().notEmpty().withMessage("Nome completo não pode ser vazio"), 124 | body("nomeUsuario").optional().notEmpty().withMessage("Nome de usuário não pode ser vazio"), 125 | body("email").optional().isEmail().withMessage("Email inválido"), 126 | body("senha") 127 | .optional() 128 | .isLength({ min: 6 }).withMessage("Senha deve ter no mínimo 6 caracteres") 129 | .matches(/\d/).withMessage("Senha deve conter pelo menos um número") 130 | .matches(/[A-Z]/).withMessage("Senha deve conter pelo menos uma letra maiúscula"), 131 | validateRequest 132 | ], 133 | async (req: Request<{ id: string }, {}, UpdateUserRequest>, res: Response) => { 134 | try { 135 | LoggerService.info("Iniciando atualização de usuário", { id: req.params.id }); 136 | const user = await userRepository.findOne({ 137 | where: { id: req.params.id }, 138 | select: ["id", "nomeCompleto", "nomeUsuario", "email", "senha"] 139 | }); 140 | 141 | if (!user) { 142 | LoggerService.warn("Tentativa de atualizar usuário inexistente", { id: req.params.id }); 143 | return res.status(404).json({ erro: "Usuário não encontrado" }); 144 | } 145 | 146 | const updateData: Partial = {}; 147 | let hasChanges = false; 148 | 149 | if (req.body.nomeCompleto && req.body.nomeCompleto !== user.nomeCompleto) { 150 | updateData.nomeCompleto = req.body.nomeCompleto; 151 | hasChanges = true; 152 | } 153 | 154 | if (req.body.nomeUsuario && req.body.nomeUsuario !== user.nomeUsuario) { 155 | const existingUser = await userRepository.findOne({ 156 | where: { nomeUsuario: req.body.nomeUsuario } 157 | }); 158 | 159 | if (existingUser) { 160 | return res.status(400).json({ erro: "Nome de usuário já está em uso" }); 161 | } 162 | updateData.nomeUsuario = req.body.nomeUsuario; 163 | hasChanges = true; 164 | } 165 | 166 | if (req.body.email && req.body.email !== user.email) { 167 | updateData.email = req.body.email; 168 | hasChanges = true; 169 | } 170 | 171 | if (req.body.senha) { 172 | updateData.senha = await bcrypt.hash(req.body.senha, 10); 173 | hasChanges = true; 174 | } 175 | 176 | if (!hasChanges) { 177 | LoggerService.info("Nenhuma alteração necessária para o usuário", { id: req.params.id }); 178 | return res.status(200).json({ mensagem: "Não houve alterações" }); 179 | } 180 | 181 | Object.assign(user, updateData); 182 | await userRepository.save(user); 183 | LoggerService.info("Usuário atualizado com sucesso", 184 | { 185 | id: user.id, 186 | campos: Object.keys(updateData) 187 | }); 188 | 189 | const { senha: _, ...userWithoutPassword } = user as User & { senha: string }; 190 | return res.json(userWithoutPassword); 191 | } catch (error) { 192 | LoggerService.error("Erro ao atualizar usuário", error); 193 | if (error instanceof QueryFailedError && error.message.includes('duplicate key')) { 194 | if (error.message.includes('email')) { 195 | return res.status(400).json({ erro: "Email já está em uso" }); 196 | } 197 | return res.status(400).json({ erro: "Nome de usuário já está em uso" }); 198 | } 199 | return res.status(500).json({ erro: "Erro ao atualizar usuário" }); 200 | } 201 | } 202 | ); 203 | 204 | router.delete("/:id", async (req: Request<{ id: string }>, res: Response) => { 205 | try { 206 | LoggerService.info("Iniciando exclusão de usuário", { id: req.params.id }); 207 | const user = await userRepository.findOne({ 208 | where: { id: req.params.id }, 209 | relations: ["artigos"] 210 | }); 211 | 212 | if (!user) { 213 | LoggerService.warn("Tentativa de excluir usuário inexistente", { id: req.params.id }); 214 | return res.status(404).json({ erro: "Usuário não encontrado" }); 215 | } 216 | 217 | if (user.artigos && user.artigos.length > 0) { 218 | LoggerService.warn("Tentativa de excluir usuário com artigos vinculados", 219 | { 220 | id: req.params.id, 221 | numeroArtigos: user.artigos.length 222 | }); 223 | return res.status(400).json({ erro: "Não é possível excluir usuário com artigos vinculados" }); 224 | } 225 | 226 | await userRepository.remove(user); 227 | LoggerService.info("Usuário excluído com sucesso", { id: req.params.id }); 228 | return res.status(204).send(); 229 | } catch (error) { 230 | LoggerService.error("Erro ao excluir usuário", error); 231 | return res.status(500).json({ erro: "Erro ao excluir usuário" }); 232 | } 233 | }); 234 | 235 | export default router; -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import express from "express"; 3 | import cors from "cors"; 4 | import swaggerUi from "swagger-ui-express"; 5 | import YAML from "yamljs"; 6 | import path from "path"; 7 | import { AppDataSource } from "./database/data-source"; 8 | import userRoutes from "./routes/userRoutes"; 9 | import categoryRoutes from "./routes/categoryRoutes"; 10 | import articleRoutes from "./routes/articleRoutes"; 11 | import authRoutes from "./routes/authRoutes"; 12 | import { authMiddleware } from "./middleware/authMiddleware"; 13 | import { Router } from "express"; 14 | import { DatabaseCleanupService } from "./services/DatabaseCleanupService"; 15 | import { LoggerService } from "./services/LoggerService"; 16 | 17 | const app = express(); 18 | 19 | app.use(cors()); 20 | app.use(express.json()); 21 | 22 | const swaggerDocument = YAML.load(path.resolve(__dirname, "../swagger.yaml")); 23 | app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); 24 | 25 | app.use("/auth", authRoutes); 26 | 27 | const userRouter = Router(); 28 | app.use("/usuarios", userRouter); 29 | 30 | userRouter.post("/", userRoutes); 31 | 32 | userRouter.use(authMiddleware); 33 | userRouter.get("/", userRoutes); 34 | userRouter.get("/:id", userRoutes); 35 | userRouter.put("/:id", userRoutes); 36 | userRouter.delete("/:id", userRoutes); 37 | 38 | app.use("/categorias", authMiddleware, categoryRoutes); 39 | app.use("/artigos", authMiddleware, articleRoutes); 40 | 41 | const PORT = process.env.PORT || 3000; 42 | 43 | AppDataSource.initialize() 44 | .then(async () => { 45 | const cleanupService = new DatabaseCleanupService(); 46 | await cleanupService.verificarEstadoAtual(); 47 | LoggerService.info("Banco de dados inicializado com sucesso"); 48 | 49 | app.listen(PORT, () => { 50 | LoggerService.info(`Servidor iniciado na porta ${PORT}`); 51 | LoggerService.info(`Documentação disponível em http://localhost:${PORT}/api-docs`); 52 | }); 53 | }) 54 | .catch((error) => { 55 | LoggerService.error("Erro ao inicializar o servidor", error); 56 | process.exit(1); 57 | }); -------------------------------------------------------------------------------- /src/services/DatabaseCleanupService.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from "../database/data-source"; 2 | import { EventSubscriber, EntitySubscriberInterface, InsertEvent } from "typeorm"; 3 | import { LoggerService } from "./LoggerService"; 4 | 5 | @EventSubscriber() 6 | export class DatabaseCleanupService implements EntitySubscriberInterface { 7 | private static LIMITE_REGISTROS = 500; 8 | private static REGISTROS_PARA_REMOVER = 250; 9 | private tableCounters: Map; 10 | 11 | constructor() { 12 | this.tableCounters = new Map(); 13 | AppDataSource.subscribers.push(this); 14 | LoggerService.info("Serviço de limpeza automática do banco de dados iniciado", { 15 | limiteRegistros: DatabaseCleanupService.LIMITE_REGISTROS, 16 | registrosParaRemover: DatabaseCleanupService.REGISTROS_PARA_REMOVER 17 | }); 18 | } 19 | 20 | afterInsert(event: InsertEvent): void { 21 | const tableName = event.metadata.tableName; 22 | const currentCount = (this.tableCounters.get(tableName) || 0) + 1; 23 | this.tableCounters.set(tableName, currentCount); 24 | 25 | if (currentCount >= DatabaseCleanupService.LIMITE_REGISTROS) { 26 | LoggerService.info("Limite de registros atingido, iniciando limpeza", { 27 | tabela: tableName, 28 | registrosAtuais: currentCount 29 | }); 30 | this.limparRegistrosAntigos(event.metadata.target, tableName); 31 | this.tableCounters.set(tableName, 0); 32 | } 33 | } 34 | 35 | private async limparRegistrosAntigos(entity: any, tableName: string): Promise { 36 | try { 37 | const repository = AppDataSource.getRepository(entity); 38 | 39 | const registrosAntigos = await repository 40 | .createQueryBuilder() 41 | .orderBy("COALESCE(created_at, data_registro)", "ASC") 42 | .take(DatabaseCleanupService.REGISTROS_PARA_REMOVER) 43 | .getMany(); 44 | 45 | if (registrosAntigos.length > 0) { 46 | await repository.remove(registrosAntigos); 47 | LoggerService.info("Registros antigos removidos com sucesso", { 48 | tabela: tableName, 49 | quantidadeRemovida: registrosAntigos.length 50 | }); 51 | } 52 | } catch (error) { 53 | LoggerService.error(`Erro ao limpar registros da tabela ${tableName}`, error); 54 | } 55 | } 56 | 57 | async verificarEstadoAtual(): Promise { 58 | const entities = AppDataSource.entityMetadatas; 59 | 60 | for (const entity of entities) { 61 | try { 62 | const repository = AppDataSource.getRepository(entity.target); 63 | const count = await repository.count(); 64 | 65 | if (count >= DatabaseCleanupService.LIMITE_REGISTROS) { 66 | LoggerService.warn("Tabela excedeu limite de registros", { 67 | tabela: entity.tableName, 68 | registrosAtuais: count, 69 | limite: DatabaseCleanupService.LIMITE_REGISTROS 70 | }); 71 | await this.limparRegistrosAntigos(entity.target, entity.tableName); 72 | } 73 | 74 | this.tableCounters.set(entity.tableName, count % DatabaseCleanupService.LIMITE_REGISTROS); 75 | LoggerService.info("Estado atual da tabela verificado", { 76 | tabela: entity.tableName, 77 | registrosAtuais: count 78 | }); 79 | } catch (error) { 80 | LoggerService.error(`Erro ao verificar tabela ${entity.tableName}`, error); 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/services/LoggerService.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from '../utils/FormatDate'; 2 | 3 | export class LoggerService { 4 | private static readonly BLUE = '\x1b[34m'; 5 | private static readonly RED = '\x1b[31m'; 6 | private static readonly YELLOW = '\x1b[33m'; 7 | private static readonly GRAY = '\x1b[90m'; 8 | private static readonly RESET = '\x1b[0m'; 9 | 10 | static info(message: string, metadata?: any): void { 11 | const timestamp = formatDate(new Date()); 12 | console.log(`[${this.BLUE}INFO${this.RESET}] ${this.GRAY}${timestamp}${this.RESET} - ${message}`, metadata ? metadata : ''); 13 | } 14 | 15 | static error(message: string, error?: any): void { 16 | const timestamp = formatDate(new Date()); 17 | console.error(`[${this.RED}ERROR${this.RESET}] ${this.GRAY}${timestamp}${this.RESET} - ${message}`, error ? error : ''); 18 | } 19 | 20 | static warn(message: string, metadata?: any): void { 21 | const timestamp = formatDate(new Date()); 22 | console.warn(`[${this.YELLOW}WARN${this.RESET}] ${this.GRAY}${timestamp}${this.RESET} - ${message}`, metadata ? metadata : ''); 23 | } 24 | } -------------------------------------------------------------------------------- /src/utils/FormatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (date: Date): string => { 2 | const dia = date.getDate().toString().padStart(2, '0'); 3 | const mes = (date.getMonth() + 1).toString().padStart(2, '0'); 4 | const ano = date.getFullYear(); 5 | const hora = date.getHours().toString().padStart(2, '0'); 6 | const minuto = date.getMinutes().toString().padStart(2, '0'); 7 | const segundo = date.getSeconds().toString().padStart(2, '0'); 8 | const milisegundos = date.getMilliseconds().toString().padStart(3, '0'); 9 | 10 | return `${dia}-${mes}-${ano} ${hora}:${minuto}:${segundo}.${milisegundos}`; 11 | } -------------------------------------------------------------------------------- /swagger.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: CMS For QA's (API de Gestão de Conteúdo) 4 | description: Uma API para gerenciamento de conteúdo com usuários, categorias e artigos 5 | version: 1.0.0 6 | contact: 7 | email: bgomes.joaovitor@gmail.com 8 | 9 | servers: 10 | - url: http://localhost:3000 11 | description: Servidor de Desenvolvimento 12 | 13 | components: 14 | securitySchemes: 15 | bearerAuth: 16 | type: http 17 | scheme: bearer 18 | bearerFormat: JWT 19 | 20 | schemas: 21 | Login: 22 | type: object 23 | properties: 24 | user: 25 | type: object 26 | properties: 27 | id: 28 | type: string 29 | format: uuid 30 | nomeCompleto: 31 | type: string 32 | nomeUsuario: 33 | type: string 34 | email: 35 | type: string 36 | format: email 37 | dataCriacao: 38 | type: string 39 | format: date-time 40 | token: 41 | type: string 42 | 43 | User: 44 | type: object 45 | properties: 46 | id: 47 | type: string 48 | format: uuid 49 | nomeCompleto: 50 | type: string 51 | nomeUsuario: 52 | type: string 53 | email: 54 | type: string 55 | format: email 56 | dataCriacao: 57 | type: string 58 | format: date-time 59 | artigos: 60 | type: array 61 | items: 62 | $ref: '#/components/schemas/Article' 63 | 64 | Category: 65 | type: object 66 | properties: 67 | id: 68 | type: string 69 | format: uuid 70 | nome: 71 | type: string 72 | descricao: 73 | type: string 74 | dataCriacao: 75 | type: string 76 | format: date-time 77 | artigos: 78 | type: array 79 | items: 80 | $ref: '#/components/schemas/Article' 81 | 82 | Article: 83 | type: object 84 | properties: 85 | id: 86 | type: string 87 | format: uuid 88 | titulo: 89 | type: string 90 | maxLength: 100 91 | conteudo: 92 | type: string 93 | autorId: 94 | type: string 95 | format: uuid 96 | categoriaId: 97 | type: string 98 | format: uuid 99 | dataPublicacao: 100 | type: string 101 | format: date-time 102 | dataCriacao: 103 | type: string 104 | format: date-time 105 | autor: 106 | $ref: '#/components/schemas/User' 107 | categoria: 108 | $ref: '#/components/schemas/Category' 109 | 110 | Error: 111 | type: object 112 | properties: 113 | erro: 114 | type: string 115 | errors: 116 | type: array 117 | items: 118 | type: object 119 | properties: 120 | msg: 121 | type: string 122 | param: 123 | type: string 124 | location: 125 | type: string 126 | 127 | tags: 128 | - name: Autenticação 129 | description: Operações de autenticação 130 | - name: Usuários 131 | description: Operações relacionadas a usuários 132 | - name: Categorias 133 | description: Operações relacionadas a categorias 134 | - name: Artigos 135 | description: Operações relacionadas a artigos 136 | 137 | paths: 138 | /auth/login: 139 | post: 140 | tags: 141 | - Autenticação 142 | summary: Realizar login 143 | description: Autentica um usuário e retorna um token JWT 144 | requestBody: 145 | required: true 146 | content: 147 | application/json: 148 | schema: 149 | type: object 150 | required: 151 | - email 152 | - senha 153 | properties: 154 | email: 155 | type: string 156 | format: email 157 | example: "usuario@email.com" 158 | senha: 159 | type: string 160 | format: password 161 | example: "Senha123" 162 | responses: 163 | '200': 164 | description: Login realizado com sucesso 165 | content: 166 | application/json: 167 | schema: 168 | $ref: '#/components/schemas/Login' 169 | '401': 170 | description: Email ou senha inválidos 171 | content: 172 | application/json: 173 | schema: 174 | $ref: '#/components/schemas/Error' 175 | '400': 176 | description: Dados inválidos 177 | content: 178 | application/json: 179 | schema: 180 | $ref: '#/components/schemas/Error' 181 | '500': 182 | description: Erro interno do servidor 183 | content: 184 | application/json: 185 | schema: 186 | $ref: '#/components/schemas/Error' 187 | 188 | /usuarios: 189 | post: 190 | tags: 191 | - Usuários 192 | summary: Criar um novo usuário 193 | description: Cria um novo usuário no sistema (não requer autenticação) 194 | requestBody: 195 | required: true 196 | content: 197 | application/json: 198 | schema: 199 | type: object 200 | required: 201 | - nomeCompleto 202 | - nomeUsuario 203 | - email 204 | - senha 205 | properties: 206 | nomeCompleto: 207 | type: string 208 | example: "João da Silva" 209 | nomeUsuario: 210 | type: string 211 | example: "joaosilva" 212 | email: 213 | type: string 214 | format: email 215 | example: "joao@email.com" 216 | senha: 217 | type: string 218 | format: password 219 | example: "Senha123" 220 | responses: 221 | '201': 222 | description: Usuário criado com sucesso 223 | content: 224 | application/json: 225 | schema: 226 | $ref: '#/components/schemas/User' 227 | '400': 228 | description: Dados inválidos ou usuário já existe 229 | content: 230 | application/json: 231 | schema: 232 | $ref: '#/components/schemas/Error' 233 | '500': 234 | description: Erro interno do servidor 235 | 236 | get: 237 | security: 238 | - bearerAuth: [] 239 | tags: 240 | - Usuários 241 | summary: Listar usuários 242 | description: Retorna uma lista de usuários com filtros opcionais 243 | parameters: 244 | - in: query 245 | name: nomeUsuario 246 | schema: 247 | type: string 248 | description: Filtrar por nome de usuário 249 | - in: query 250 | name: email 251 | schema: 252 | type: string 253 | description: Filtrar por email 254 | responses: 255 | '200': 256 | description: Lista de usuários 257 | content: 258 | application/json: 259 | schema: 260 | type: array 261 | items: 262 | $ref: '#/components/schemas/User' 263 | '401': 264 | description: Não autorizado 265 | '500': 266 | description: Erro interno do servidor 267 | 268 | /usuarios/{id}: 269 | get: 270 | security: 271 | - bearerAuth: [] 272 | tags: 273 | - Usuários 274 | summary: Buscar usuário por ID 275 | parameters: 276 | - name: id 277 | in: path 278 | required: true 279 | schema: 280 | type: string 281 | format: uuid 282 | responses: 283 | '200': 284 | description: Usuário encontrado 285 | content: 286 | application/json: 287 | schema: 288 | $ref: '#/components/schemas/User' 289 | '401': 290 | description: Não autorizado 291 | '404': 292 | description: Usuário não encontrado 293 | 294 | put: 295 | security: 296 | - bearerAuth: [] 297 | tags: 298 | - Usuários 299 | summary: Atualizar usuário 300 | parameters: 301 | - name: id 302 | in: path 303 | required: true 304 | schema: 305 | type: string 306 | format: uuid 307 | requestBody: 308 | required: true 309 | content: 310 | application/json: 311 | schema: 312 | type: object 313 | properties: 314 | nomeCompleto: 315 | type: string 316 | nomeUsuario: 317 | type: string 318 | email: 319 | type: string 320 | format: email 321 | senha: 322 | type: string 323 | format: password 324 | responses: 325 | '200': 326 | description: Usuário atualizado ou sem alterações 327 | content: 328 | application/json: 329 | schema: 330 | oneOf: 331 | - $ref: '#/components/schemas/User' 332 | - type: object 333 | properties: 334 | mensagem: 335 | type: string 336 | example: "Não houve alterações" 337 | '400': 338 | description: Dados inválidos ou conflito com usuário existente 339 | content: 340 | application/json: 341 | schema: 342 | type: object 343 | properties: 344 | erro: 345 | type: string 346 | example: "Nome de usuário já está em uso" 347 | '401': 348 | description: Não autorizado 349 | '404': 350 | description: Usuário não encontrado 351 | 352 | delete: 353 | security: 354 | - bearerAuth: [] 355 | tags: 356 | - Usuários 357 | summary: Excluir usuário 358 | parameters: 359 | - name: id 360 | in: path 361 | required: true 362 | schema: 363 | type: string 364 | format: uuid 365 | responses: 366 | '204': 367 | description: Usuário excluído com sucesso 368 | '401': 369 | description: Não autorizado 370 | '404': 371 | description: Usuário não encontrado 372 | '400': 373 | description: Não é possível excluir (possui artigos vinculados) 374 | 375 | /categorias: 376 | post: 377 | security: 378 | - bearerAuth: [] 379 | tags: 380 | - Categorias 381 | summary: Criar uma nova categoria 382 | requestBody: 383 | required: true 384 | content: 385 | application/json: 386 | schema: 387 | type: object 388 | required: 389 | - nome 390 | properties: 391 | nome: 392 | type: string 393 | example: "Tecnologia" 394 | descricao: 395 | type: string 396 | example: "Artigos sobre tecnologia" 397 | responses: 398 | '201': 399 | description: Categoria criada com sucesso 400 | content: 401 | application/json: 402 | schema: 403 | $ref: '#/components/schemas/Category' 404 | '400': 405 | description: Dados inválidos 406 | content: 407 | application/json: 408 | schema: 409 | $ref: '#/components/schemas/Error' 410 | '401': 411 | description: Não autorizado 412 | '500': 413 | description: Erro interno do servidor 414 | 415 | get: 416 | security: 417 | - bearerAuth: [] 418 | tags: 419 | - Categorias 420 | summary: Listar categorias 421 | parameters: 422 | - in: query 423 | name: nome 424 | schema: 425 | type: string 426 | description: Filtrar por nome 427 | responses: 428 | '200': 429 | description: Lista de categorias 430 | content: 431 | application/json: 432 | schema: 433 | type: array 434 | items: 435 | $ref: '#/components/schemas/Category' 436 | '401': 437 | description: Não autorizado 438 | '500': 439 | description: Erro interno do servidor 440 | 441 | /categorias/{id}: 442 | get: 443 | security: 444 | - bearerAuth: [] 445 | tags: 446 | - Categorias 447 | summary: Buscar categoria por ID 448 | parameters: 449 | - name: id 450 | in: path 451 | required: true 452 | schema: 453 | type: string 454 | format: uuid 455 | responses: 456 | '200': 457 | description: Categoria encontrada 458 | content: 459 | application/json: 460 | schema: 461 | $ref: '#/components/schemas/Category' 462 | '401': 463 | description: Não autorizado 464 | '404': 465 | description: Categoria não encontrada 466 | '500': 467 | description: Erro interno do servidor 468 | 469 | put: 470 | security: 471 | - bearerAuth: [] 472 | tags: 473 | - Categorias 474 | summary: Atualizar categoria 475 | parameters: 476 | - name: id 477 | in: path 478 | required: true 479 | schema: 480 | type: string 481 | format: uuid 482 | requestBody: 483 | required: true 484 | content: 485 | application/json: 486 | schema: 487 | type: object 488 | properties: 489 | nome: 490 | type: string 491 | descricao: 492 | type: string 493 | responses: 494 | '200': 495 | description: Categoria atualizada 496 | content: 497 | application/json: 498 | schema: 499 | $ref: '#/components/schemas/Category' 500 | '400': 501 | description: Dados inválidos 502 | content: 503 | application/json: 504 | schema: 505 | $ref: '#/components/schemas/Error' 506 | '401': 507 | description: Não autorizado 508 | '404': 509 | description: Categoria não encontrada 510 | '500': 511 | description: Erro interno do servidor 512 | 513 | delete: 514 | security: 515 | - bearerAuth: [] 516 | tags: 517 | - Categorias 518 | summary: Excluir categoria 519 | parameters: 520 | - name: id 521 | in: path 522 | required: true 523 | schema: 524 | type: string 525 | format: uuid 526 | responses: 527 | '204': 528 | description: Categoria excluída com sucesso 529 | '400': 530 | description: Não é possível excluir (possui artigos vinculados) 531 | content: 532 | application/json: 533 | schema: 534 | $ref: '#/components/schemas/Error' 535 | '401': 536 | description: Não autorizado 537 | '404': 538 | description: Categoria não encontrada 539 | '500': 540 | description: Erro interno do servidor 541 | 542 | /artigos: 543 | post: 544 | security: 545 | - bearerAuth: [] 546 | tags: 547 | - Artigos 548 | summary: Criar um novo artigo 549 | description: Cria um novo artigo usando o nome do autor e nome da categoria 550 | requestBody: 551 | required: true 552 | content: 553 | application/json: 554 | schema: 555 | type: object 556 | required: 557 | - titulo 558 | - conteudo 559 | - nomeAutor 560 | - nomeCategoria 561 | properties: 562 | titulo: 563 | type: string 564 | maxLength: 100 565 | example: "Introdução aos Testes Automatizados" 566 | conteudo: 567 | type: string 568 | example: "Exemplos de ferramentas de testes automatizados..." 569 | nomeAutor: 570 | type: string 571 | example: "Usuario" 572 | nomeCategoria: 573 | type: string 574 | example: "Tecnologia" 575 | dataPublicacao: 576 | type: string 577 | format: date-time 578 | responses: 579 | '201': 580 | description: Artigo criado com sucesso 581 | content: 582 | application/json: 583 | schema: 584 | $ref: '#/components/schemas/Article' 585 | '400': 586 | description: Dados inválidos 587 | content: 588 | application/json: 589 | schema: 590 | $ref: '#/components/schemas/Error' 591 | '401': 592 | description: Não autorizado 593 | '404': 594 | description: Autor ou categoria não encontrados 595 | '500': 596 | description: Erro interno do servidor 597 | 598 | get: 599 | security: 600 | - bearerAuth: [] 601 | tags: 602 | - Artigos 603 | summary: Listar artigos 604 | parameters: 605 | - in: query 606 | name: categoriaId 607 | schema: 608 | type: string 609 | format: uuid 610 | description: Filtrar por categoria 611 | - in: query 612 | name: autorId 613 | schema: 614 | type: string 615 | format: uuid 616 | description: Filtrar por autor 617 | - in: query 618 | name: page 619 | schema: 620 | type: integer 621 | default: 1 622 | description: Número da página 623 | - in: query 624 | name: limit 625 | schema: 626 | type: integer 627 | default: 10 628 | description: Itens por página 629 | responses: 630 | '200': 631 | description: Lista de artigos 632 | content: 633 | application/json: 634 | schema: 635 | type: object 636 | properties: 637 | data: 638 | type: array 639 | items: 640 | $ref: '#/components/schemas/Article' 641 | total: 642 | type: integer 643 | page: 644 | type: integer 645 | lastPage: 646 | type: integer 647 | '401': 648 | description: Não autorizado 649 | '500': 650 | description: Erro interno do servidor 651 | 652 | /artigos/{id}: 653 | get: 654 | security: 655 | - bearerAuth: [] 656 | tags: 657 | - Artigos 658 | summary: Buscar artigo por ID 659 | parameters: 660 | - name: id 661 | in: path 662 | required: true 663 | schema: 664 | type: string 665 | format: uuid 666 | responses: 667 | '200': 668 | description: Artigo encontrado 669 | content: 670 | application/json: 671 | schema: 672 | $ref: '#/components/schemas/Article' 673 | '401': 674 | description: Não autorizado 675 | '404': 676 | description: Artigo não encontrado 677 | 678 | put: 679 | security: 680 | - bearerAuth: [] 681 | tags: 682 | - Artigos 683 | summary: Atualizar artigo 684 | parameters: 685 | - name: id 686 | in: path 687 | required: true 688 | schema: 689 | type: string 690 | format: uuid 691 | requestBody: 692 | required: true 693 | content: 694 | application/json: 695 | schema: 696 | type: object 697 | properties: 698 | titulo: 699 | type: string 700 | maxLength: 100 701 | conteudo: 702 | type: string 703 | responses: 704 | '200': 705 | description: Artigo atualizado 706 | '401': 707 | description: Não autorizado 708 | '404': 709 | description: Artigo não encontrado 710 | '400': 711 | description: Dados inválidos 712 | 713 | delete: 714 | security: 715 | - bearerAuth: [] 716 | tags: 717 | - Artigos 718 | summary: Excluir artigo 719 | parameters: 720 | - name: id 721 | in: path 722 | required: true 723 | schema: 724 | type: string 725 | format: uuid 726 | responses: 727 | '204': 728 | description: Artigo excluído com sucesso 729 | '401': 730 | description: Não autorizado 731 | '404': 732 | description: Artigo não encontrado -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "lib": ["es2018", "esnext.asynciterable"], 6 | "skipLibCheck": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "moduleResolution": "node", 10 | "removeComments": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true, 21 | "emitDecoratorMetadata": true, 22 | "experimentalDecorators": true, 23 | "resolveJsonModule": true, 24 | "baseUrl": "." 25 | }, 26 | "exclude": ["node_modules"], 27 | "include": ["./src/**/*.ts"] 28 | } --------------------------------------------------------------------------------