├── src ├── graphql │ └── .keep ├── modules │ ├── auth │ │ ├── google │ │ │ └── .keep │ │ ├── auth.types.ts │ │ ├── local │ │ │ ├── index.ts │ │ │ └── local.controller.ts │ │ └── auth.services.ts │ ├── user │ │ ├── user.types.ts │ │ ├── user.seeder.ts │ │ ├── user.services.ts │ │ ├── user.schema.ts │ │ ├── index.ts │ │ ├── user.controller.ts │ │ └── user.model.ts │ └── healthcheck │ │ └── index.ts ├── config │ ├── seed.ts │ ├── express.ts │ ├── socket.ts │ ├── database.ts │ └── swagger.ts ├── logger │ └── index.ts ├── middleware │ ├── errorHandler.ts │ ├── validateRequest.ts │ └── morgan.ts ├── index.ts ├── routes.ts └── app.ts ├── commitlint.config.js ├── .husky └── commit-msg ├── renovate.json ├── .env.example ├── .editorconfig ├── tsconfig.json ├── CHANGELOG.md ├── .eslintrc.json ├── .github └── pull_request_template.md ├── LICENSE ├── .gitignore ├── package.json └── README.md /src/graphql/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/auth/google/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | MONGO_DB_URI=mongodb://localhost:27017/your_database_name 3 | NODE_ENV=development 4 | BASE_URL=https://your_app_.up.railway.app/ 5 | SECRET_TOKEN_APP=your_secret_token 6 | -------------------------------------------------------------------------------- /src/config/seed.ts: -------------------------------------------------------------------------------- 1 | import seedUser from '../modules/user/user.seeder'; 2 | 3 | async function seed() { 4 | await Promise.all([ 5 | seedUser(), 6 | ]); 7 | } 8 | 9 | export default seed; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/modules/auth/auth.types.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | import { UserDocument } from '../user/user.model'; 4 | 5 | export interface AuthRequest extends Request { 6 | user?: UserDocument; 7 | } 8 | 9 | export type Role = 'ADMIN' | 'INSTRUCTOR' | 'MAKER'; 10 | 11 | export type Roles = Role[]; 12 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import dayjs from 'dayjs'; 3 | 4 | const logger = pino({ 5 | transport: { 6 | target: 'pino-pretty', 7 | timestamp: () => `,'time':'${dayjs().format('DD/MM/YYYY')}'`, 8 | options: { 9 | colorize: true, 10 | }, 11 | }, 12 | }); 13 | 14 | export default logger; 15 | -------------------------------------------------------------------------------- /src/middleware/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequestHandler } from 'express'; 2 | 3 | import log from '../logger'; 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | const errorHandler: ErrorRequestHandler = (err, req, res, next) => { 7 | log.error(err.stack); 8 | res.status(500).json({ msg: err.message }); 9 | }; 10 | 11 | export default errorHandler; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "sourceMap": false, 6 | "outDir": "./dist/", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true, 12 | }, 13 | "include": ["src/**/*"], 14 | } 15 | -------------------------------------------------------------------------------- /src/config/express.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from 'express'; 2 | import cors from 'cors'; 3 | 4 | import morganMiddleware from '../middleware/morgan'; 5 | 6 | function configExpress(app: Application) { 7 | app.use(cors()); 8 | app.use(express.json()); 9 | app.use(express.urlencoded({ extended: false })); 10 | app.use(morganMiddleware); 11 | } 12 | 13 | export default configExpress; 14 | -------------------------------------------------------------------------------- /src/modules/auth/local/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { 4 | handleLoginUser, 5 | handleValidateUser, 6 | } from './local.controller'; 7 | 8 | const router = Router(); 9 | 10 | // Login 11 | // POST /auth/local/login 12 | router.post('/login', handleLoginUser); 13 | // Verify email 14 | // POST /auth/local/activate/kbnsdffkhjdshkfsdfsdf 15 | router.get('/activate/:token', handleValidateUser); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | import { server } from './app'; 7 | import log from './logger'; 8 | 9 | function startServer() { 10 | const PORT = process.env.PORT as string || 8080; 11 | server.listen(PORT, () => { 12 | log.info(`Server running 🤖🚀 at http://localhost:${PORT}/`); 13 | }); 14 | } 15 | 16 | setImmediate(startServer); 17 | 18 | export default server; 19 | -------------------------------------------------------------------------------- /src/modules/user/user.types.ts: -------------------------------------------------------------------------------- 1 | export type userProfileType = { 2 | firstName: string; 3 | lastName: string; 4 | email: string; 5 | avatar: string; 6 | role: string; 7 | } 8 | 9 | export type paymentType = { 10 | customerId: string; 11 | cards: { 12 | paymentMethodId: string; 13 | brand: string; 14 | country: string; 15 | expMonth: number; 16 | expYear: number; 17 | funding: string; 18 | last4: string; 19 | }[]; 20 | } 21 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application routes 3 | */ 4 | import { Application } from 'express'; 5 | 6 | import healthcheck from './modules/healthcheck'; 7 | import user from './modules/user'; 8 | import authLocal from './modules/auth/local'; 9 | 10 | function routes(app: Application) { 11 | app.use('/api/healthcheck', healthcheck); 12 | app.use('/api/users', user); 13 | 14 | app.use('/auth/local', authLocal); 15 | } 16 | 17 | export default routes; 18 | -------------------------------------------------------------------------------- /src/config/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { Server as HttpServer } from 'http'; 3 | 4 | export type Socket = { 5 | io: Server | void; 6 | } 7 | 8 | export const socket: Socket = { 9 | io: undefined, 10 | }; 11 | 12 | export function connectSocket(server: HttpServer) { 13 | const options = { 14 | cors: { 15 | origin: true, 16 | }, 17 | }; 18 | const io = new Server(server, options); 19 | 20 | socket.io = io; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/healthcheck/index.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from 'express'; 2 | 3 | const router = Router(); 4 | 5 | /** 6 | * @openapi 7 | * /api/healthcheck: 8 | * get: 9 | * tags: 10 | * - HealthCheck 11 | * description: Get a 200 response if the server is up and running 12 | * responses: 13 | * 200: 14 | * description: App is up and running 15 | */ 16 | router.get('/', (req: Request, res: Response) => res.sendStatus(200)); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /src/middleware/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { AnyZodObject } from 'zod'; 3 | 4 | const validate = (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | schema.parse({ 7 | body: req.body, 8 | query: req.query, 9 | params: req.params, 10 | }); 11 | next(); 12 | return null; 13 | } catch (e: any) { 14 | return res.status(400).send(e.errors); 15 | } 16 | }; 17 | 18 | export default validate; 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (2023-01-03) 2 | 3 | * fix: :bug: login user ([70cdad0](https://github.com/makeitrealcamp/nodejs-template/commit/70cdad0)) 4 | * fix: :bug: run project ([aced5fc](https://github.com/makeitrealcamp/nodejs-template/commit/aced5fc)) 5 | * chore: :sparkles: code base ([81a7c4f](https://github.com/makeitrealcamp/nodejs-template/commit/81a7c4f)) 6 | * chore: :wrench: add changelog file ([c11d887](https://github.com/makeitrealcamp/nodejs-template/commit/c11d887)) 7 | * Initial commit ([b882cde](https://github.com/makeitrealcamp/nodejs-template/commit/b882cde)) 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "rules": { 18 | "import/extensions": "off" 19 | }, 20 | "settings": { 21 | "import/resolver": { 22 | "node": { 23 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/config/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import seed from './seed'; 4 | import log from '../logger'; 5 | 6 | async function connectDB() { 7 | const dbUri = process.env.MONGO_DB_URI as string; 8 | const env = process.env.NODE_ENV; 9 | 10 | try { 11 | mongoose.set('strictQuery', false); 12 | await mongoose.connect(dbUri); 13 | 14 | if (env !== 'production') { 15 | log.info('Populating database with seed data...'); 16 | seed(); 17 | } 18 | 19 | log.info('MongoDB Connected'); 20 | } catch (error) { 21 | log.error(error); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | export default connectDB; 27 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { createServer } from 'http'; 3 | 4 | import expressConfig from './config/express'; 5 | import connectDB from './config/database'; 6 | import swaggerDocs from './config/swagger'; 7 | import routes from './routes'; 8 | import { connectSocket } from './config/socket'; 9 | import errorHandler from './middleware/errorHandler'; 10 | 11 | // setup server 12 | const app = express(); 13 | export const server = createServer(app); 14 | 15 | const env = process.env.NODE_ENV; 16 | const port = process.env.PORT || 8080; 17 | 18 | if (env !== 'test') { 19 | connectDB(); 20 | } 21 | 22 | // setup express 23 | expressConfig(app); 24 | // Socket 25 | connectSocket(server); 26 | // routes 27 | routes(app); 28 | // Swagger 29 | swaggerDocs(app, port as number); 30 | // Error handler 31 | app.use(errorHandler); 32 | 33 | export default app; 34 | -------------------------------------------------------------------------------- /src/modules/user/user.seeder.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | import User from './user.model'; 4 | import log from '../../logger'; 5 | 6 | const users = Array.from({ length: 10 }, () => ( 7 | { 8 | firstName: faker.name.firstName(), 9 | lastName: faker.name.lastName(), 10 | email: faker.internet.email(), 11 | password: faker.internet.password(), 12 | role: faker.helpers.arrayElement(['ADMIN', 'INSTRUCTOR', 'MAKER']), 13 | avatar: faker.image.avatar(), 14 | } 15 | )); 16 | 17 | const seed = async () => { 18 | try { 19 | const usersData = await User.find({}); 20 | 21 | if (usersData.length > 0) { 22 | return; 23 | } 24 | 25 | const result = await User.insertMany(users); 26 | log.info(`User model, successfully seed: ${result.length} documents`); 27 | } catch (error: any) { 28 | log.error(error); 29 | } 30 | }; 31 | 32 | export default seed; 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | [//]: <> (Here should go the description of the Pull Request, what is it? what does?) 3 | 4 | 5 | ### Feeling 6 | [//]: <> (How do you feel about this Pull Request? the solution you deliver how does it make you feel?) 7 | - [ ] 🤙 Quick Fix 8 | - [ ] 👌 Implented, Tested and Done 9 | - [ ] 🤞 I really hope this works, please review it 10 | 11 | ### Ticket (Issue Tracking) 12 | [//]: <> (The ticket number associated with this Pull Request) 13 | 14 | ### How to test? 15 | [//]: <> (Pasos necesarios para probar esta funcionalidad) 16 | 17 | ### Screenshots (if available) 18 | [//]: <> (Steps required to test this functionality) 19 | 20 | ### Scope 21 | - [ ] 🐞 Bugfix (non-breaking changes to fix a bug) 22 | - [ ] 💚 Enhancement (non-breaking change to add/modify functionality to an existing feature) 23 | - [ ] ⚡️ Feature (non-breaking change to add a new feature) 24 | - [ ] ⚠️ Breaking change (change that is not backward compatible, changes to current functionality) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Make it Real 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/middleware/morgan.ts: -------------------------------------------------------------------------------- 1 | import morgan, { StreamOptions } from 'morgan'; 2 | 3 | import logger from '../logger'; 4 | 5 | // Override the stream method by telling 6 | // Morgan to use our custom logger instead of the console.log. 7 | const stream: StreamOptions = { 8 | // Use the http severity 9 | write: (message) => logger.info(message), 10 | }; 11 | 12 | // Skip all the Morgan http log if the 13 | // application is not running in development mode. 14 | // This method is not really needed here since 15 | // we already told to the logger that it should print 16 | // only warning and error messages in production. 17 | const skip = () => { 18 | const env = process.env.NODE_ENV || 'development'; 19 | return env !== 'development'; 20 | }; 21 | 22 | // Build the morgan middleware 23 | const morganMiddleware = morgan( 24 | // Define message format string (this is the default one). 25 | // The message format is made from tokens, and each token is 26 | // defined inside the Morgan library. 27 | // You can create your custom token to show what do you want from a request. 28 | ':method :url :status :res[content-length] - :response-time ms', 29 | // Options: in this case, I overwrote the stream and the skip logic. 30 | // See the methods above. 31 | { stream, skip }, 32 | ); 33 | 34 | export default morganMiddleware; 35 | -------------------------------------------------------------------------------- /src/modules/user/user.services.ts: -------------------------------------------------------------------------------- 1 | import { Model, FilterQuery } from 'mongoose'; 2 | 3 | import User, { UserDocument } from './user.model'; 4 | 5 | export async function createUser( 6 | input: Model>, 7 | ) { 8 | return User.create(input); 9 | } 10 | 11 | export async function getUserByID(id: string) { 12 | try { 13 | return await User.findById(id); 14 | } catch (error: any) { 15 | throw new Error(error); 16 | } 17 | } 18 | 19 | export async function getUser(filter: FilterQuery) { 20 | const user = await User.findOne(filter); 21 | return user; 22 | } 23 | 24 | export async function getUsers(filter?: FilterQuery) { 25 | const users = filter ? await User.find(filter) : await User.find(); 26 | return users; 27 | } 28 | 29 | export async function updateUser( 30 | filter: FilterQuery, 31 | input: Model, 32 | ) { 33 | const user = await User.findOneAndUpdate(filter, input, { new: true }); 34 | return user; 35 | } 36 | 37 | export async function deleteUser(filter: FilterQuery) { 38 | const user = await User.findOneAndDelete(filter); 39 | return user; 40 | } 41 | 42 | export async function getUserByEmail(email: string) { 43 | const user = await User.findOne({ email }); 44 | return user; 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @openapi 3 | * components: 4 | * schemas: 5 | * ListUsersResponse: 6 | * type: array 7 | * items: 8 | * $ref: '#/components/schemas/ListUserResponse' 9 | * ListUserResponse: 10 | * type: object 11 | * properties: 12 | * _id: 13 | * type: string 14 | * email: 15 | * type: string 16 | * name: 17 | * type: string 18 | * createdAt: 19 | * type: string 20 | * updatedAt: 21 | * type: string 22 | * CreateUserRequest: 23 | * type: object 24 | * required: 25 | * - email 26 | * - password 27 | * properties: 28 | * email: 29 | * type: string 30 | * password: 31 | * type: string 32 | * name: 33 | * type: string 34 | * CreateUserResponse: 35 | * type: object 36 | * $ref: '#/components/schemas/ListUserResponse' 37 | * AuthenticateUserRequest: 38 | * type: object 39 | * required: 40 | * - email 41 | * - password 42 | * properties: 43 | * email: 44 | * type: string 45 | * password: 46 | * type: string 47 | * AuthenticateUserResponse: 48 | * type: object 49 | * properties: 50 | * user: 51 | * type: object 52 | * properties: 53 | * _id: 54 | * type: string 55 | * email: 56 | * type: string 57 | * name: 58 | * type: string 59 | * createdAt: 60 | * type: string 61 | * updatedAt: 62 | * type: string 63 | * validPassword: 64 | * type: boolean 65 | */ 66 | -------------------------------------------------------------------------------- /src/modules/auth/local/local.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | import { getUser } from '../../user/user.services'; 4 | import { signToken } from '../auth.services'; 5 | 6 | /** 7 | * Returns a user profile and a JWT token signed by the app secret 8 | * @param req Request Request object 9 | * @param res Response Response object 10 | * @returns Promise Response object 11 | */ 12 | export async function handleLoginUser(req: Request, res: Response, next: NextFunction) { 13 | const { email, password } = req.body; 14 | 15 | try { 16 | const user = await getUser({ email }); 17 | 18 | if (!user) { 19 | return res.status(404).json({ message: 'Invalid email or password' }); 20 | } 21 | 22 | const validPassword = await user.comparePassword(password); 23 | 24 | if (!validPassword) { 25 | return res.status(401).json({ message: 'Invalid email or password' }); 26 | } 27 | 28 | const payload = user.profile; 29 | 30 | // Generate token JWT 31 | const token = signToken(payload); 32 | 33 | return res.status(200).json({ profile: user.profile, token }); 34 | } catch (error: any) { 35 | return next(error); 36 | } 37 | } 38 | 39 | export async function handleValidateUser(req: Request, res: Response, next: NextFunction) { 40 | const { token } = req.params; 41 | 42 | try { 43 | const user = await getUser({ passwordResetToken: token }); 44 | 45 | if (!user) { 46 | return res.status(404).json({ message: 'Invalid token' }); 47 | } 48 | 49 | if (Date.now() > Number(user.resetExpires)) { 50 | return res.status(400).json({ message: 'Token expired' }); 51 | } 52 | 53 | user.isActive = true; 54 | user.resetToken = undefined; 55 | user.resetExpires = undefined; 56 | 57 | await user.save(); 58 | 59 | const jwt = signToken(user.profile); 60 | 61 | return res.status(200).json({ profile: user.profile, token: jwt }); 62 | } catch (error: any) { 63 | return next(error); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /src/config/swagger.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import swaggerJsdoc from 'swagger-jsdoc'; 3 | import swaggerUi from 'swagger-ui-express'; 4 | import { Request, Response, Application } from 'express'; 5 | 6 | import { version } from '../../package.json'; 7 | import log from '../logger'; 8 | 9 | const ext = process.env.NODE_ENV === 'production' ? '.js' : '.ts'; 10 | 11 | const routesApi = path.join(__dirname, `../modules/**/index${ext}`); 12 | const schemasApi = path.join(__dirname, `../modules/**/**.schema${ext}`); 13 | 14 | const options = { 15 | definition: { 16 | openapi: '3.0.1', 17 | info: { 18 | title: 'API Documentation', 19 | version, 20 | description: 'API Documentation for your project', 21 | license: { 22 | name: 'MIT', 23 | url: 'https://choosealicense.com/licenses/mit/', 24 | }, 25 | contact: { 26 | name: 'Make It Real', 27 | url: 'https://makeitreal.camp/', 28 | email: 'info@makeitreal.camp', 29 | }, 30 | }, 31 | components: { 32 | securitySchemes: { 33 | bearerAuth: { 34 | type: 'http', 35 | scheme: 'bearer', 36 | bearerFormat: 'JWT', 37 | in: 'header', 38 | }, 39 | }, 40 | }, 41 | // Only for all endpoints 42 | // security: [ 43 | // { 44 | // bearerAuth: [], 45 | // }, 46 | // ], 47 | servers: [ 48 | { 49 | url: 'http://localhost:8080', 50 | description: 'Local server', 51 | }, 52 | { 53 | url: process.env.BASE_URL || 'http://localhost:8080', 54 | description: 'Production server', 55 | }, 56 | ], 57 | }, 58 | apis: [routesApi, schemasApi], 59 | }; 60 | 61 | const swaggerSpec = swaggerJsdoc(options); 62 | 63 | function swaggerDocs(app: Application, port: number) { 64 | // Swagger Page 65 | app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); 66 | 67 | // Docs in JSON Format 68 | app.get('/docs.json', (req: Request, res: Response) => { 69 | res.setHeader('Content-Type', 'application/json'); 70 | res.send(swaggerSpec); 71 | }); 72 | 73 | const url = `${process.env.BASE_URL}/docs` || `http://localhost:${port}/docs`; 74 | 75 | log.info(`Docs 📃🛠 available at ${url}`); 76 | } 77 | 78 | export default swaggerDocs; 79 | -------------------------------------------------------------------------------- /src/modules/auth/auth.services.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | import { UserDocument } from '../user/user.model'; 5 | import { getUser } from '../user/user.services'; 6 | import { AuthRequest, Roles } from './auth.types'; 7 | 8 | const SECRET = process.env.SECRET_TOKEN_APP as string && 'YOUR_SECRET'; 9 | 10 | /** 11 | * Returns a JWT token signed by the app secret 12 | * @param payload Object | String Data to be signed 13 | * @returns token String 14 | */ 15 | export function signToken(payload: any) { 16 | const token = jwt.sign( 17 | payload, 18 | SECRET, 19 | { expiresIn: '10h' }, 20 | ); 21 | 22 | return token; 23 | } 24 | 25 | /** 26 | * Validates a JWT 27 | * @param token String JWT token 28 | * @returns Object | Boolean 29 | */ 30 | export function verifyToken(token: string) { 31 | try { 32 | const decoded = jwt.verify(token, SECRET) as UserDocument; 33 | 34 | return decoded; 35 | } catch (error) { 36 | return false; 37 | } 38 | } 39 | 40 | /** 41 | * Verifies if the user is authenticated 42 | * @param req 43 | * @param res 44 | * @param next 45 | * @returns 46 | */ 47 | export async function isAuthenticated(req: AuthRequest, res: Response, next: NextFunction) { 48 | const token = req.headers?.authorization?.split(' ')[1]; 49 | 50 | if (!token) { 51 | return res.status(401).json({ message: 'Unauthorized' }); 52 | } 53 | 54 | const decoded = verifyToken(token) as UserDocument; 55 | 56 | if (!decoded) { 57 | return res.status(401).json({ message: 'Unauthorized' }); 58 | } 59 | 60 | const user = await getUser({ email: decoded.email }); 61 | 62 | if (!user) { 63 | return res.status(401).json({ message: 'Unauthorized' }); 64 | } 65 | 66 | req.user = user; 67 | 68 | next(); 69 | return true; 70 | } 71 | 72 | /** 73 | * Verifies if the user has the required role 74 | * @param allowRoles Roles 75 | * @returns 76 | */ 77 | export function hasRole(allowRoles: Roles) { 78 | return (req: AuthRequest, res: Response, next: NextFunction) => { 79 | const { role } = req.user as UserDocument; 80 | 81 | if (!allowRoles.includes(role)) { 82 | return res.status(403).json({ message: 'Forbidden' }); 83 | } 84 | 85 | next(); 86 | return true; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { 4 | createUserHandler, 5 | deleteUserHandler, 6 | getUserHandler, 7 | listUserHandler, 8 | updateUserHandler, 9 | } from './user.controller'; 10 | import { isAuthenticated } from '../auth/auth.services'; 11 | 12 | const router = Router(); 13 | 14 | /** 15 | * @openapi 16 | * /api/users: 17 | * get: 18 | * tags: 19 | * - Users 20 | * summary: Get all users 21 | * description: Get all users from the database 22 | * security: 23 | * - ApiKeyAuth: [] 24 | * responses: 25 | * 200: 26 | * description: Get all users 27 | * content: 28 | * application/json: 29 | * schema: 30 | * $ref: '#/components/schemas/ListUsersResponse' 31 | * 401: 32 | * description: Unauthorized 33 | * 500: 34 | * description: Server error 35 | */ 36 | router.get('/', isAuthenticated, listUserHandler); 37 | router.delete('/:id', isAuthenticated, deleteUserHandler); 38 | 39 | /** 40 | * @openapi 41 | * /api/users/{id}: 42 | * get: 43 | * tags: 44 | * - Users 45 | * summary: Get user 46 | * description: Get user from the database 47 | * security: 48 | * - ApiKeyAuth: [] 49 | * responses: 50 | * 200: 51 | * description: Get user 52 | * content: 53 | * application/json: 54 | * schema: 55 | * $ref: '#/components/schemas/ListUserResponse' 56 | * 401: 57 | * description: Unauthorized 58 | * 500: 59 | * description: Server error 60 | */ 61 | router.get('/:id', isAuthenticated, getUserHandler); 62 | router.patch('/:id', isAuthenticated, updateUserHandler); 63 | 64 | /** 65 | * @openapi 66 | * /api/users: 67 | * post: 68 | * tags: 69 | * - Users 70 | * summary: Create user 71 | * security: 72 | * - ApiKeyAuth: [] 73 | * requestBody: 74 | * required: true 75 | * content: 76 | * application/json: 77 | * schema: 78 | * $ref: '#/components/schemas/CreateUserRequest' 79 | * responses: 80 | * 200: 81 | * description: Create user 82 | * content: 83 | * application/json: 84 | * schema: 85 | * $ref: '#/components/schemas/CreateUserResponse' 86 | * 401: 87 | * description: Unauthorized 88 | * 500: 89 | * description: Server error 90 | */ 91 | router.post('/', createUserHandler); 92 | 93 | export default router; 94 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | import { 4 | createUser, 5 | deleteUser, 6 | getUserByEmail, 7 | getUserByID, 8 | getUsers, 9 | updateUser, 10 | } from './user.services'; 11 | 12 | export async function listUserHandler(req: Request, res: Response, next: NextFunction) { 13 | try { 14 | const users = await getUsers(); 15 | return res.status(200).json(users); 16 | } catch (e: any) { 17 | return next(e); 18 | } 19 | } 20 | 21 | export async function createUserHandler(req: Request, res: Response, next: NextFunction) { 22 | const data = req.body; 23 | try { 24 | const user = await createUser(data); 25 | return res.status(201).json(user); 26 | } catch (e: any) { 27 | return next(e); 28 | } 29 | } 30 | 31 | export async function getUserHandler(req: Request, res: Response, next: NextFunction) { 32 | const { id } = req.params; 33 | 34 | try { 35 | const user = await getUserByID(id); 36 | if (!user) { 37 | res.status(404).json({ msg: 'User not found' }); 38 | } 39 | return res.status(200).json(user); 40 | } catch (e: any) { 41 | return next(e); 42 | } 43 | } 44 | 45 | export async function updateUserHandler(req: Request, res: Response, next: NextFunction) { 46 | const { id } = req.params; 47 | const data = req.body; 48 | 49 | try { 50 | const user = await getUserByID(id); 51 | if (!user) { 52 | res.status(404).json({ msg: 'User not found' }); 53 | } 54 | const updatedUser = await updateUser({ _id: id }, data); 55 | return res.status(200).json(updatedUser); 56 | } catch (e: any) { 57 | return next(e); 58 | } 59 | } 60 | 61 | export async function deleteUserHandler(req: Request, res: Response, next: NextFunction) { 62 | const { id } = req.params; 63 | 64 | try { 65 | const query = { _id: id }; 66 | const user = await deleteUser(query); 67 | if (!user) { 68 | res.status(404).json({ msg: 'User not found' }); 69 | } 70 | return res.status(200).json(user); 71 | } catch (e: any) { 72 | return next(e); 73 | } 74 | } 75 | 76 | export async function authenticateUserHandler(req: Request, res: Response, next: NextFunction) { 77 | const { email, password } = req.body; 78 | 79 | try { 80 | const user = await getUserByEmail(email); 81 | if (!user) { 82 | return res.status(404).json({ msg: 'User not found' }); 83 | } 84 | const validPassword = await user.comparePassword(password); 85 | return res.status(200).json({ user, validPassword }); 86 | } catch (e: any) { 87 | return next(e); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-template", 3 | "version": "1.0.0", 4 | "description": "Template for Node.js projects", 5 | "main": "index.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node ./dist/index.js", 9 | "dev": "ts-node-dev --respawn ./src/index.ts", 10 | "lint": "eslint --ext .ts,.tsx ./", 11 | "lint:fix": "eslint --ext .ts,.tsx ./ --fix", 12 | "test": "NODE_ENV=test jest --testTimeout=10000", 13 | "prepare": "husky install", 14 | "version": "conventional-changelog -i CHANGELOG.md -s && git add ." 15 | }, 16 | "dependencies": { 17 | "@faker-js/faker": "^8.0.0", 18 | "axios": "^1.2.2", 19 | "bcryptjs": "^2.4.3", 20 | "cors": "^2.8.5", 21 | "dayjs": "^1.10.7", 22 | "dotenv": "^16.0.0", 23 | "express": "^4.17.3", 24 | "jsonwebtoken": "^9.0.0", 25 | "lodash": "^4.17.21", 26 | "mongoose": "^7.0.0", 27 | "morgan": "^1.10.0", 28 | "pino": "^8.0.0", 29 | "pino-http": "^8.0.0", 30 | "pino-pretty": "^10.0.0", 31 | "request-promise": "^4.2.6", 32 | "socket.io": "^4.5.1", 33 | "swagger-jsdoc": "^6.2.1", 34 | "swagger-ui-express": "^4.4.0", 35 | "zod": "^3.14.4" 36 | }, 37 | "devDependencies": { 38 | "@commitlint/cli": "^17.3.0", 39 | "@commitlint/config-conventional": "^17.3.0", 40 | "@types/bcrypt": "^5.0.0", 41 | "@types/bcryptjs": "^2.4.2", 42 | "@types/cors": "^2.8.12", 43 | "@types/dotenv": "^8.2.0", 44 | "@types/express": "^4.17.13", 45 | "@types/jsonwebtoken": "^9.0.0", 46 | "@types/lodash": "^4.14.178", 47 | "@types/mongoose": "^5.11.97", 48 | "@types/morgan": "^1.9.3", 49 | "@types/node": "^18.0.0", 50 | "@types/pino": "^7.0.5", 51 | "@types/pino-http": "^5.8.1", 52 | "@types/socket.io": "^3.0.2", 53 | "@types/supertest": "^2.0.12", 54 | "@types/swagger-jsdoc": "^6.0.1", 55 | "@types/swagger-ui-express": "^4.1.3", 56 | "@types/yup": "^0.29.13", 57 | "@typescript-eslint/eslint-plugin": "^6.0.0", 58 | "@typescript-eslint/parser": "^6.0.0", 59 | "conventional-changelog-cli": "^3.0.0", 60 | "eslint": "^8.13.0", 61 | "eslint-config-airbnb-base": "^15.0.0", 62 | "eslint-plugin-import": "^2.26.0", 63 | "husky": "^8.0.3", 64 | "supertest": "^6.2.2", 65 | "ts-node": "^10.7.0", 66 | "ts-node-dev": "^2.0.0", 67 | "typescript": "^5.0.0" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/makeitrealcamp/nodejs-template.git" 72 | }, 73 | "keywords": [ 74 | "template", 75 | "typescript", 76 | "backend", 77 | "make-it-real" 78 | ], 79 | "author": "makeitreal", 80 | "license": "MIT", 81 | "bugs": { 82 | "url": "https://github.com/makeitrealcamp/nodejs-template/issues" 83 | }, 84 | "homepage": "https://github.com/makeitrealcamp/nodejs-template#readme", 85 | "volta": { 86 | "node": "18.16.1", 87 | "npm": "9.8.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Template - Make It Real 💻 2 | 3 | Codebase for the node.js projects. 4 | 5 | - Built with Node.js and Express 6 | - Typescript 7 | - Mongoose ODM 8 | - REST API & GraphQL 9 | 10 | ## Prerequisites 11 | 12 | - [Git](https://git-scm.com/downloads) 13 | - [Volta.sh](https://dev.to/khriztianmoreno/introduccion-a-volta-la-forma-mas-rapida-de-administrar-entornos-de-node-1oo6) 14 | - [Node.js and npm](https://nodejs.org) Node >= 18.12 LTS, npm >= 8.19.x - Install with Volta.sh 15 | 16 | ## Express Router and Routes 17 | 18 | | Route | HTTP Verb | Route Middleware | Description | 19 | | --------------------| --------- | ------------------ | ------------------------------------ | 20 | | /api/healthcheck | GET | isAuthenticated | Show a simple message | 21 | | /api/users | GET | isAuthenticated | Get list of users | 22 | | /api/users | POST | | Creates a new users | 23 | | /api/users/:id | GET | isAuthenticated | Get a single users | 24 | | /api/users/:id | DELETE | isAuthenticated | Deletes a user | 25 | 26 | 27 | ## Usage 28 | The use of endpoints is very simple, previously you could see a table of endpoints that you can call, if you need to create a note or log in, here we have some examples. 29 | 30 | ### Authentication **user** `/auth/local/login`: 31 | 32 | Request Body: 33 | ```json 34 | { 35 | "email": "cristian.moreno@makeitreal.camp", 36 | "password": "123456" 37 | } 38 | ``` 39 | 40 | Response: 41 | ```json 42 | { 43 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNyaXN0aWFuLm1vcmVub0BtYWtlaXRyZWFsLmNhbXAiLCJpYXQiOjE2NjEyMDgwODJ9.kPdMoVUEnyX36vi606Mc1C66yWLKKAB37GLbF0gzhBo", 44 | "profile": { 45 | "firstName": "cristian", 46 | "lastName": "moreno", 47 | "email": "cristian.moreno@makeitreal.camp", 48 | "avatar": "https://image.com/264.jpg", 49 | "role": "ADMIN" 50 | } 51 | } 52 | ``` 53 | ### Basic example **Create User** `/api/users`: 54 | 55 | Request Body: 56 | ```json 57 | { 58 | "firstName": "cristian", 59 | "lastName": "moreno", 60 | "email": "cristian.moreno@makeitreal.camp", 61 | "password": "123456", 62 | "avatar": "https://image.com/264.jpg", 63 | } 64 | ``` 65 | 66 | Response: 67 | 68 | ```json 69 | { 70 | "name": "cristian moreno", 71 | "email": "cristian.moreno@makeitreal.camp", 72 | "role": "USER", 73 | } 74 | ``` 75 | 76 | ### Developing 77 | 78 | 1. Run `npm install` to install server dependencies. 79 | 80 | 2. Configure the env 81 | ```shell 82 | $ cp .env.example .env 83 | ``` 84 | 85 | 3. Update `.env` with the required info 86 | 87 | 4. Run `npm run dev` to start the development server. 88 | 89 | 90 | #### Convention 91 | 92 | - [Commit Message Convention](https://www.conventionalcommits.org/en/v1.0.0/) 93 | - [Git Flow](https://www.atlassian.com/es/git/tutorials/comparing-workflows/gitflow-workflow) 94 | - [Git Commit Emoji](https://gitmoji.dev/) 95 | 96 | 97 | ## License 98 | 99 | [MIT](LICENSE) 100 | -------------------------------------------------------------------------------- /src/modules/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | import bcrypt from 'bcryptjs'; 3 | 4 | import { userProfileType, paymentType } from './user.types'; 5 | import { Role } from '../auth/auth.types'; 6 | 7 | export interface UserDocument extends Document { 8 | firstName: string; 9 | lastName: string; 10 | email: string; 11 | password: string; // 1234 -> hash - SHA256 -> 64 chars -> 32 bytes -> 12 | avatar?: string; 13 | role: Role; 14 | isActive: boolean; 15 | resetToken?: string; 16 | resetExpires?: Date; 17 | payment?: paymentType; 18 | createdAt: Date; 19 | updatedAt: Date; 20 | 21 | fullName: string; 22 | profile: userProfileType; 23 | // eslint-disable-next-line no-unused-vars 24 | comparePassword: (password: string) => Promise; 25 | } 26 | 27 | const Payment = new Schema({ 28 | customerId: String, 29 | cards: [ 30 | { 31 | paymentMethodId: String, 32 | brand: String, 33 | country: String, 34 | expMonth: Number, 35 | expYear: Number, 36 | funding: String, 37 | last4: String, 38 | }, 39 | ], 40 | }); 41 | 42 | const UserSchema = new Schema({ 43 | firstName: { 44 | type: String, 45 | required: true, 46 | trim: true, 47 | }, 48 | lastName: { 49 | type: String, 50 | required: true, 51 | trim: true, 52 | }, 53 | email: { 54 | type: String, 55 | required: true, 56 | unique: true, 57 | trim: true, 58 | lowercase: true, 59 | }, 60 | password: { 61 | type: String, 62 | required: true, 63 | min: 6, 64 | }, 65 | avatar: { 66 | type: String, 67 | default: '', 68 | }, 69 | role: { 70 | type: String, 71 | enum: ['ADMIN', 'INSTRUCTOR', 'MAKER'], 72 | default: 'MAKER', 73 | }, 74 | isActive: { 75 | type: Boolean, 76 | default: false, 77 | }, 78 | resetToken: String, 79 | resetExpires: Date, 80 | payment: Payment, 81 | }, { 82 | timestamps: true, 83 | versionKey: false, 84 | }); 85 | 86 | // Middlewares 87 | UserSchema.pre('save', async function save(next: Function) { 88 | const user = this; 89 | 90 | try { 91 | if (!this.isModified('password')) { 92 | next(); 93 | } 94 | 95 | if (user.password) { 96 | const salt = await bcrypt.genSalt(10); 97 | const hash = await bcrypt.hash(user.password, salt); 98 | 99 | user.password = hash; 100 | } 101 | } catch (error: any) { 102 | next(error); 103 | } 104 | }); 105 | 106 | // Virtuals 107 | UserSchema.virtual('fullName').get(function fullName() { 108 | const { firstName, lastName } = this; 109 | 110 | return `${firstName} ${lastName}`; 111 | }); 112 | 113 | UserSchema.virtual('profile').get(function profile() { 114 | const { 115 | firstName, lastName, email, avatar, role, 116 | } = this; 117 | 118 | return { 119 | firstName, 120 | lastName, 121 | email, 122 | avatar, 123 | role, 124 | }; 125 | }); 126 | 127 | // Methods 128 | async function comparePassword(this: UserDocument, candidatePassword: string, next: Function) { 129 | const user = this; 130 | 131 | try { 132 | if (user.password) { 133 | const isMatch = await bcrypt.compare(candidatePassword, user.password); 134 | 135 | return isMatch; 136 | } 137 | 138 | return false; 139 | } catch (error) { 140 | next(error); 141 | return false; 142 | } 143 | } 144 | 145 | UserSchema.methods.comparePassword = comparePassword; 146 | 147 | const User = model('User', UserSchema); 148 | 149 | export default User; 150 | --------------------------------------------------------------------------------