├── .babelrc ├── .dependencygraph └── setting.json ├── .env.test ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ └── node.js.yml ├── .gitignore ├── .mocharc.json ├── .nycrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── config ├── db.js ├── swagger.js └── swagger.json ├── controllers ├── authController.js ├── metricsController.js └── userController.js ├── docker-compose.yml ├── index.js ├── middleware ├── auth.js ├── authorize.js ├── errorHandler.js └── upload.js ├── models ├── metrics.js └── user.js ├── package-lock.json ├── package.json ├── prometheus.yml ├── public └── reset-password.html ├── routes ├── loginRoutes.js ├── metricsRoutes.js └── userRoutes.js ├── test └── userRoutes.test.mjs ├── uploads └── 873409ad6aa037c801ba38a04be33200 └── utils ├── logger.js └── mailer.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 4 | } -------------------------------------------------------------------------------- /.dependencygraph/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFilePath": "index.js", 3 | "alias": false, 4 | "resolveExtensions": [ 5 | ".js", 6 | ".jsx", 7 | ".ts", 8 | ".tsx", 9 | ".vue", 10 | ".scss", 11 | ".less" 12 | ] 13 | } -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DB_USER=your_db_user 3 | DB_HOST=your_db_host 4 | DB_DATABASE=your_db_name 5 | DB_PASSWORD=your_db_password 6 | DB_PORT=your_db_port 7 | JWT_SECRET=your_jwt_secret 8 | JWT_EXPIRATION_TIME=86400 9 | EMAIL_USER=your_email@gmail.com 10 | EMAIL_PASS=your_email_password 11 | 12 | DATABASE_URL=url_db 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Set up environment variables 29 | env: 30 | DATABASE_URL: ${{ secrets.DATABASE_URL }} 31 | run: echo "Environment variable DATABASE_URL has been set." 32 | 33 | - name: Build project 34 | run: npm run build --if-present 35 | 36 | #- name: Run tests 37 | # run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | .clinic/ 132 | features.md 133 | .qodo 134 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["mjs"], 3 | "spec": "test/**/*.test.mjs", 4 | "require": [], 5 | "timeout": 5000 6 | } -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".mjs"], 3 | "reporter": ["text", "html"], 4 | "all": true, 5 | "include": ["src/**/*.mjs"], 6 | "exclude": ["node_modules", "test/**/*.test.mjs"] 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Node Api Postgres is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Node Api Postgres to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open [Source/Culture/Tech] Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at Node Api Postgres events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. kalleljawher4@gmail.com. 61 | 62 | 63 | 64 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 | 66 | ## 8. Addressing Grievances 67 | 68 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 69 | 70 | 71 | 72 | ## 9. Scope 73 | 74 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 75 | 76 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 77 | 78 | ## 10. Contact info 79 | 80 | kalleljawher4@gmail.com 81 | 82 | ## 11. License and attribution 83 | 84 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 85 | 86 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 87 | 88 | _Revision 2.3. Posted 6 March 2017._ 89 | 90 | _Revision 2.2. Posted 4 February 2016._ 91 | 92 | _Revision 2.1. Posted 23 June 2014._ 93 | 94 | _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Node API Postgres 2 | 3 | Thank you for considering contributing to this project! We welcome all kinds of contributions, whether it's bug reports, feature suggestions, or code improvements. Please follow the guidelines below to make the contribution process smooth and effective. 4 | 5 | ## Getting Started 6 | 7 | 1. **Fork the Repository**: Start by forking the repository to your GitHub account. 8 | 2. **Clone the Fork**: Clone the repository to your local machine. 9 | ```bash 10 | git clone https://github.com/your-username/node-api-postgres.git 11 | ``` 12 | 3. **Set Upstream**: Set the original repository as the upstream remote. 13 | ```bash 14 | git remote add upstream https://github.com/JawherKl/node-api-postgres.git 15 | ``` 16 | 17 | ## Contribution Guidelines 18 | 19 | ### Reporting Issues 20 | 21 | If you encounter a bug or have a feature request, please [open an issue](https://github.com/JawherKl/node-api-postgres/issues) with the following details: 22 | - A clear and descriptive title 23 | - Steps to reproduce the issue (if applicable) 24 | - Expected behavior 25 | - Actual behavior 26 | - Any relevant screenshots or error messages 27 | 28 | ### Submitting Pull Requests 29 | 30 | 1. **Branch from `main`**: Always create a new branch from the `main` branch for your work. 31 | ```bash 32 | git checkout -b feature/id-or-bugfix-name 33 | ``` 34 | 2. **Write Clean Code**: Follow the existing code style and add comments where necessary. 35 | 3. **Run Tests**: Ensure all existing tests pass and add new tests if applicable. 36 | ```bash 37 | npm test 38 | ``` 39 | 4. **Commit Messages**: Write meaningful and descriptive commit messages. 40 | ```bash 41 | git commit -m "Add feature X or fix issue Y" 42 | ``` 43 | 5. **Push Changes**: Push your branch to your fork. 44 | ```bash 45 | git push origin feature/id-or-bugfix-name 46 | ``` 47 | 6. **Create a Pull Request**: Submit a pull request to the `main` branch of the original repository. Provide a detailed description of your changes and reference any relevant issues. 48 | 49 | ### Code of Conduct 50 | 51 | Please note that this project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 52 | 53 | ## Development Setup 54 | 55 | 1. **Install Dependencies**: 56 | ```bash 57 | npm install 58 | ``` 59 | 2. **Configure Environment**: 60 | Create a `.env` file in the root directory and configure the required environment variables. Refer to `.env.example` for guidance. 61 | 3. **Run the Application**: 62 | ```bash 63 | npm start 64 | ``` 65 | 4. **Run Tests**: 66 | ```bash 67 | npm test 68 | ``` 69 | 70 | ## Suggestions and Feedback 71 | 72 | We are open to suggestions and feedback! Feel free to start a discussion in the [Discussions](https://github.com/JawherKl/node-api-postgres/discussions) tab. 73 | 74 | Thank you for your contributions! Together, we can make this project better. 75 | 76 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a base image 2 | FROM node:20-alpine 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy package files to the container 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy all application files 14 | COPY . . 15 | 16 | # Expose the port the app runs on 17 | EXPOSE 3000 18 | 19 | # Command to run the application 20 | CMD ["node", "index.js"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [JawherKl] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express - Node.js API with PostgreSQL 2 | 3 | ![Repository Size](https://img.shields.io/github/repo-size/JawherKl/node-api-postgres) 4 | ![Last Commit](https://img.shields.io/github/last-commit/JawherKl/node-api-postgres) 5 | ![Issues](https://img.shields.io/github/issues-raw/JawherKl/node-api-postgres) 6 | ![Forks](https://img.shields.io/github/forks/JawherKl/node-api-postgres) 7 | ![Stars](https://img.shields.io/github/stars/JawherKl/node-api-postgres) 8 | 9 | ![nodepost](https://github.com/user-attachments/assets/6f206c6e-dea0-4045-8baa-a04e74a5fbf8) 10 | 11 | This is a modern RESTful API built with **Node.js** and **Express**, designed to interact with a **PostgreSQL** database. The API provides various endpoints for managing user data, with additional features like authentication, JWT protection, soft deletion, and automated testing. We've also integrated **Swagger** for auto-generated API documentation. 12 | 13 | ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 14 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 15 | ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) 16 | ![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white) 17 | 18 | ## Features 🚀 19 | - **User Management**: 20 | - **Get All Users**: Retrieve a list of all users. 21 | - **Get User by ID**: Retrieve a specific user by their ID. 22 | - **Create User**: Add a new user to the database. 23 | - **Update User**: Update details of an existing user. 24 | - **Delete User**: Remove a user from the database (soft delete functionality). 25 | 26 | - **Authentication & Authorization**: 27 | - **User Authentication**: Secure API access using **JSON Web Tokens (JWT)**. 28 | - **Role-based Access Control (RBAC)**: Control access to resources based on user roles (e.g., admin, user). 29 | - **Password Reset**: Secure password reset functionality with time-limited tokens and email verification using **SendGrid**. 30 | 31 | - **Swagger API Documentation**: 32 | - **Swagger** integrated for real-time API documentation and testing directly in the browser. Access the documentation at: [http://localhost:3000/api-docs](http://localhost:3000/api-docs). 33 | 34 | - **Database**: 35 | - Integration with **PostgreSQL** for storing user data securely. 36 | - **Soft delete functionality**: Mark users as deleted without removing their data. 37 | 38 | - **Unit Testing**: 39 | - Comprehensive unit tests using **Mocha** and **Chai** to ensure the reliability of the application. 40 | - **Test Cases**: Includes tests for user creation, update, deletion, and authentication. 41 | 42 | ## Technologies Used ⚙️ 43 | - **Node.js** (JavaScript runtime) 44 | - **Express** (Web framework) 45 | - **PostgreSQL** (Database) 46 | - **JSON Web Token (JWT)** (Authentication) 47 | - **Body-Parser** (Parsing JSON request bodies) 48 | - **Swagger** (API documentation) 49 | - **Mocha** (Testing framework) 50 | - **Chai** (Assertion library) 51 | 52 | ## Installation 🛠️ 53 | ### Step 1: Clone the Repository 54 | ```bash 55 | git clone https://github.com/JawherKl/node-api-postgres.git 56 | cd node-api-postgres 57 | ``` 58 | 59 | ### Step 2: Install Dependencies 60 | ```bash 61 | npm install 62 | ``` 63 | 64 | ### Step 3: Set up PostgreSQL 65 | Ensure you have **PostgreSQL** installed and running. Create a new database and configure the connection. 66 | 67 | ### Step 4: Configure Database Connection 68 | Update the `db.js` file to set up your PostgreSQL connection credentials. 69 | 70 | ### Step 5: Generate JWT Secret (Optional) 71 | Generate a random JWT secret key (recommended for production environments): 72 | ```bash 73 | node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" 74 | ``` 75 | 76 | ### Inject Table into PostgreSQL 77 | ```sql 78 | CREATE TABLE users ( 79 | id SERIAL PRIMARY KEY, 80 | name VARCHAR(100) NOT NULL, 81 | email VARCHAR(255) UNIQUE NOT NULL, 82 | password VARCHAR(255) NOT NULL, 83 | picture VARCHAR(255) NULL, 84 | role VARCHAR(20) DEFAULT 'user', -- Role-based access control 85 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 86 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 87 | deleted_at TIMESTAMP NULL -- For soft delete functionality 88 | ); 89 | ``` 90 | 91 | ```sql 92 | CREATE TABLE metrics ( 93 | id SERIAL PRIMARY KEY, 94 | user_id INT NOT NULL, 95 | metric_name VARCHAR(255) NOT NULL, 96 | metric_value FLOAT NOT NULL, 97 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 98 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 99 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 100 | ); 101 | ``` 102 | 103 | ### Column Explanation 104 | - `id`: Unique identifier for each user (auto-increment). 105 | - `name`: User's name (max 100 characters). 106 | - `email`: Unique email address (max 255 characters). 107 | - `password`: Hashed password for security. 108 | - `role`: User's role (e.g., admin, user). 109 | - `created_at`: Timestamp for record creation. 110 | - `updated_at`: Timestamp for last update (auto-updates on modification). 111 | - `deleted_at`: Nullable timestamp for soft deletion. 112 | 113 | ## Usage 🏃‍♂️ 114 | 115 | ### Start the Server 116 | ```bash 117 | npm start 118 | ``` 119 | The server will run on [http://localhost:3000]. 120 | 121 | ### Access Swagger API Docs 122 | Once the server is running, you can access the auto-generated API documentation powered by Swagger at: 123 | [http://localhost:3000/api-docs](http://localhost:3000/api-docs). 124 | 125 | ## API Endpoints 📡 126 | - **GET /** - Returns a simple welcome message. 127 | - **GET /users** - Get all users. 128 | - **GET /users/:id** - Get a user by ID. 129 | - **POST /users** - Create a new user (requires JSON body). 130 | - **PUT /users/:id** - Update an existing user by ID (requires JSON body). 131 | - **DELETE /users/:id** - Delete a user by ID. 132 | - **POST /login** - Authenticate a user and return a JWT (requires JSON body with email and password). 133 | - **POST /forgot-password** - Request a password reset link (requires email in JSON body). 134 | - **POST /reset-password/:token** - Reset password using the token received via email. 135 | 136 | [Run In Postman](https://app.getpostman.com/run-collection/31522917-54350f46-dd5e-4a62-9dc2-4346a7879692?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D31522917-54350f46-dd5e-4a62-9dc2-4346a7879692%26entityType%3Dcollection%26workspaceId%3D212c8589-8dd4-4f19-9a53-e77403c6c7d9) 137 | 138 | ## Example Requests 📝 139 | 140 | ### Get All Users 141 | ```bash 142 | curl -X GET http://localhost:3000/users 143 | ``` 144 | 145 | ### Create User 146 | ```bash 147 | curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "John Doe", "email": "john@example.com", "password": "password"}' 148 | ``` 149 | 150 | ### Update User 151 | ```bash 152 | curl -X PUT http://localhost:3000/users/1 -H "Content-Type: application/json" -d '{"name": "Jane Doe"}' 153 | ``` 154 | 155 | ### Delete User 156 | ```bash 157 | curl -X DELETE http://localhost:3000/users/1 158 | ``` 159 | 160 | ### Authenticate User 161 | ```bash 162 | curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email": "john@example.com", "password": "password"}' 163 | ``` 164 | 165 | ### Request Password Reset 166 | ```bash 167 | curl -X POST http://localhost:3000/forgot-password -H "Content-Type: application/json" -d '{"email": "john@example.com"}' 168 | ``` 169 | 170 | ### Reset Password (using token from email) 171 | ```bash 172 | curl -X POST http://localhost:3000/reset-password/your_reset_token -H "Content-Type: application/json" -d '{"password": "new_password"}' 173 | ``` 174 | 175 | ### Access Protected Route 176 | ```bash 177 | curl -X GET http://localhost:3000/users -H "Authorization: Bearer your_jwt_token" 178 | ``` 179 | 180 | ## Unit Testing 🧪 181 | Unit tests are implemented using **Mocha** and **Chai**. To run tests: 182 | 183 | 1. Install **test dependencies** (if not installed): 184 | ```bash 185 | npm install --save-dev mocha chai 186 | ``` 187 | 188 | 2. Run the tests: 189 | ```bash 190 | npm test 191 | ``` 192 | 193 | This will run all tests and output the results to the console. You can find the test cases for different routes and operations in the `test` folder. 194 | 195 | ## Contributing 🤝 196 | Contributions are welcome! If you have suggestions, improvements, or bug fixes, please open an issue or submit a pull request. 197 | 198 | ## License 📝 199 | This project is licensed under the **MIT License**. See the [LICENSE](./LICENSE) file for details. 200 | 201 | ## Acknowledgments 🙏 202 | - Special thanks to all contributors and the open-source community. 203 | - Gratitude to the maintainers of the libraries used in this project. 204 | 205 | ## Stargazers over time 206 | [![Stargazers over time](https://starchart.cc/JawherKl/node-api-postgres.svg?variant=adaptive)](https://starchart.cc/JawherKl/node-api-postgres) 207 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Pool } = require('pg'); 3 | 4 | const pool = new Pool({ 5 | /*user: process.env.DB_USER, 6 | host: process.env.DB_HOST, 7 | database: process.env.DB_DATABASE, 8 | password: process.env.DB_PASSWORD, 9 | port: process.env.DB_PORT,*/ 10 | connectionString: process.env.DATABASE_URL, 11 | ssl: false, // Disable SSL 12 | }); 13 | 14 | pool.on('connect', () => { 15 | console.log('Connected to the database.'); 16 | }); 17 | 18 | pool.on('error', (err) => { 19 | console.error('Database connection error:', err); 20 | }); 21 | 22 | module.exports = pool; 23 | -------------------------------------------------------------------------------- /config/swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerJSDoc = require('swagger-jsdoc'); 2 | const swaggerUi = require('swagger-ui-express'); 3 | 4 | const swaggerOptions = { 5 | definition: { 6 | openapi: '3.0.0', 7 | info: { 8 | title: 'User API', 9 | version: '1.0.0', 10 | description: 'User management API with authentication', 11 | }, 12 | servers: [ 13 | { 14 | url: 'http://localhost:3000', 15 | description: 'Development server', 16 | }, 17 | ], 18 | components: { 19 | securitySchemes: { 20 | bearerAuth: { 21 | type: 'http', 22 | scheme: 'bearer', 23 | bearerFormat: 'JWT', 24 | description: 'Enter your JWT token' 25 | } 26 | }, 27 | }, 28 | security: [ 29 | { 30 | bearerAuth: [] 31 | } 32 | ], 33 | }, 34 | apis: ['./routes/*.js'], 35 | }; 36 | 37 | // Initialize Swagger JSDoc 38 | const swaggerSpec = swaggerJSDoc(swaggerOptions); 39 | 40 | const swaggerUiOptions = { 41 | explorer: true, 42 | swaggerOptions: { 43 | persistAuthorization: true, 44 | }, 45 | }; 46 | 47 | module.exports = { 48 | swaggerUi, 49 | swaggerSpec, 50 | swaggerUiOptions 51 | }; 52 | -------------------------------------------------------------------------------- /config/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "User API" 6 | }, 7 | "host": "localhost:3000", 8 | "basePath": "/api", 9 | "tags": [ 10 | { 11 | "name": "Users", 12 | "description": "API for managing users" 13 | } 14 | ], 15 | "schemes": [ 16 | "http" 17 | ], 18 | "consumes": [ 19 | "application/json" 20 | ], 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "securityDefinitions": { 25 | "Bearer": { 26 | "type": "apiKey", 27 | "name": "Authorization", 28 | "in": "header", 29 | "description": "JWT Bearer Token" 30 | } 31 | }, 32 | "paths": { 33 | "/users": { 34 | "get": { 35 | "tags": [ 36 | "Users" 37 | ], 38 | "summary": "Get all users", 39 | "security": [ 40 | { 41 | "Bearer": [] 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "List of users", 47 | "schema": { 48 | "$ref": "#/definitions/User" 49 | } 50 | }, 51 | "401": { 52 | "description": "Unauthorized" 53 | } 54 | } 55 | }, 56 | "post": { 57 | "tags": [ 58 | "Users" 59 | ], 60 | "summary": "Create new user", 61 | "security": [ 62 | { 63 | "Bearer": [] 64 | } 65 | ], 66 | "parameters": [ 67 | { 68 | "name": "user", 69 | "in": "body", 70 | "description": "User to create", 71 | "schema": { 72 | "$ref": "#/definitions/User" 73 | } 74 | } 75 | ], 76 | "responses": { 77 | "201": { 78 | "description": "User created", 79 | "schema": { 80 | "$ref": "#/definitions/User" 81 | } 82 | }, 83 | "401": { 84 | "description": "Unauthorized" 85 | } 86 | } 87 | } 88 | }, 89 | "/users/{id}": { 90 | "get": { 91 | "tags": [ 92 | "Users" 93 | ], 94 | "summary": "Get user by ID", 95 | "security": [ 96 | { 97 | "Bearer": [] 98 | } 99 | ], 100 | "parameters": [ 101 | { 102 | "name": "id", 103 | "in": "path", 104 | "required": true, 105 | "type": "string", 106 | "description": "User ID" 107 | } 108 | ], 109 | "responses": { 110 | "200": { 111 | "description": "User details", 112 | "schema": { 113 | "$ref": "#/definitions/User" 114 | } 115 | }, 116 | "404": { 117 | "description": "User not found" 118 | }, 119 | "401": { 120 | "description": "Unauthorized" 121 | } 122 | } 123 | }, 124 | "put": { 125 | "tags": [ 126 | "Users" 127 | ], 128 | "summary": "Update user by ID", 129 | "security": [ 130 | { 131 | "Bearer": [] 132 | } 133 | ], 134 | "parameters": [ 135 | { 136 | "name": "id", 137 | "in": "path", 138 | "required": true, 139 | "type": "string", 140 | "description": "User ID" 141 | }, 142 | { 143 | "name": "user", 144 | "in": "body", 145 | "description": "Updated user data", 146 | "schema": { 147 | "$ref": "#/definitions/User" 148 | } 149 | } 150 | ], 151 | "responses": { 152 | "200": { 153 | "description": "User updated" 154 | }, 155 | "404": { 156 | "description": "User not found" 157 | }, 158 | "401": { 159 | "description": "Unauthorized" 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "definitions": { 166 | "User": { 167 | "required": [ 168 | "email", 169 | "name" 170 | ], 171 | "properties": { 172 | "name": { 173 | "type": "string" 174 | }, 175 | "email": { 176 | "type": "string" 177 | }, 178 | "password": { 179 | "type": "string" 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | require('express-async-errors'); 2 | const bcrypt = require('bcryptjs'); 3 | const jwt = require('jsonwebtoken'); 4 | const crypto = require('crypto'); 5 | const pool = require('../config/db'); 6 | const { sendEmail } = require('../utils/mailer'); 7 | 8 | const register = async (req, res, next) => { 9 | const { name, email, password } = req.body; 10 | const hashedPassword = await bcrypt.hash(password, 10); 11 | 12 | const result = await pool.query( 13 | 'INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id', 14 | [name, email, hashedPassword] 15 | ); 16 | res.status(201).json({ message: 'User registered', userId: result.rows[0].id }); 17 | }; 18 | 19 | const login = async (req, res) => { 20 | const { email, password } = req.body; 21 | 22 | // Input validation 23 | if (!email || !password) { 24 | return res.status(400).json({ message: 'Email and password are required' }); 25 | } 26 | 27 | // Check if the user exists 28 | const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]); 29 | const user = result.rows[0]; 30 | 31 | if (!user) { 32 | return res.status(401).json({ message: 'Invalid email or password' }); 33 | } 34 | 35 | // Compare password with hashed password in database 36 | const passwordMatch = await bcrypt.compare(password, user.password); 37 | if (!passwordMatch) { 38 | return res.status(401).json({ message: 'Invalid email or password' }); 39 | } 40 | 41 | // Generate a more secure JWT token 42 | const tokenPayload = { 43 | userId: user.id, 44 | email: user.email, 45 | role: user.role, 46 | name: user.name, 47 | iat: Math.floor(Date.now() / 1000), 48 | jti: require('crypto').randomBytes(16).toString('hex') // Add unique token ID 49 | }; 50 | 51 | const token = jwt.sign( 52 | tokenPayload, 53 | process.env.JWT_SECRET, 54 | { 55 | expiresIn: '24h', // Increased from 1h to 24h 56 | algorithm: 'HS512' // More secure algorithm (upgrade from default HS256) 57 | } 58 | ); 59 | 60 | // Response with token and user details 61 | res.status(200).json({ 62 | message: 'Login successful', 63 | token, 64 | user: { 65 | id: user.id, 66 | name: user.name, 67 | email: user.email, 68 | role: user.role, 69 | }, 70 | }); 71 | }; 72 | 73 | const forgotPassword = async (req, res) => { 74 | const { email } = req.body; 75 | 76 | if (!email) { 77 | return res.status(400).json({ message: 'Email is required' }); 78 | } 79 | 80 | // Check if user exists 81 | const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]); 82 | const user = result.rows[0]; 83 | 84 | if (!user) { 85 | return res.status(404).json({ message: 'User not found' }); 86 | } 87 | 88 | // Generate reset token 89 | const resetToken = crypto.randomBytes(32).toString('hex'); 90 | const resetTokenExpires = new Date(Date.now() + 3600000); // 1 hour from now 91 | 92 | // Save reset token and expiry to database 93 | await pool.query( 94 | 'UPDATE users SET reset_token = $1, reset_token_expires = $2 WHERE id = $3', 95 | [resetToken, resetTokenExpires, user.id] 96 | ); 97 | 98 | // Create reset URL - using the base URL from where the request originated 99 | const resetUrl = `${req.protocol}://${req.get('host')}/reset-password/${resetToken}`; 100 | 101 | // Send email 102 | try { 103 | await sendEmail({ 104 | to: user.email, 105 | subject: 'Password Reset Request', 106 | text: `You requested a password reset. Please go to this link to reset your password: ${resetUrl}\n\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this email.`, 107 | html: ` 108 |

You requested a password reset.

109 |

Please click the link below to reset your password:

110 | Reset Password 111 |

This link will expire in 1 hour.

112 |

If you did not request this, please ignore this email.

113 | ` 114 | }); 115 | 116 | res.status(200).json({ message: 'Password reset email sent' }); 117 | } catch (error) { 118 | // If email fails, remove the reset token from database 119 | await pool.query( 120 | 'UPDATE users SET reset_token = NULL, reset_token_expires = NULL WHERE id = $1', 121 | [user.id] 122 | ); 123 | return res.status(500).json({ message: 'Error sending password reset email' }); 124 | } 125 | }; 126 | 127 | const resetPassword = async (req, res) => { 128 | try { 129 | const { token } = req.params; 130 | const { password } = req.body; 131 | 132 | if (!token || !password) { 133 | return res.status(400).json({ message: 'Token and new password are required' }); 134 | } 135 | 136 | // Find user with valid reset token 137 | const result = await pool.query( 138 | 'SELECT * FROM users WHERE reset_token = $1 AND reset_token_expires > NOW()', 139 | [token] 140 | ); 141 | 142 | const user = result.rows[0]; 143 | 144 | if (!user) { 145 | return res.status(400).json({ message: 'Invalid or expired reset token' }); 146 | } 147 | 148 | // Hash new password and update user 149 | const hashedPassword = await bcrypt.hash(password, 10); 150 | 151 | await pool.query( 152 | 'UPDATE users SET password = $1, reset_token = NULL, reset_token_expires = NULL WHERE id = $2 RETURNING id', 153 | [hashedPassword, user.id] 154 | ); 155 | 156 | console.log(`Password reset successful for user ID: ${user.id}`); 157 | res.status(200).json({ message: 'Password has been reset successfully' }); 158 | } catch (error) { 159 | console.error('Password reset error:', error); 160 | res.status(500).json({ message: 'Error resetting password. Please try again.' }); 161 | } 162 | }; 163 | 164 | module.exports = { register, login, forgotPassword, resetPassword }; 165 | -------------------------------------------------------------------------------- /controllers/metricsController.js: -------------------------------------------------------------------------------- 1 | require('express-async-errors'); 2 | const Metrics = require('../models/metrics'); 3 | const client = require('prom-client'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | // Create a Registry to register metrics 7 | const register = new client.Registry(); 8 | client.collectDefaultMetrics({ register }); 9 | 10 | // Custom metric: HTTP request counter 11 | const httpRequestCounter = new client.Counter({ 12 | name: 'http_requests_total', 13 | help: 'Total number of HTTP requests', 14 | labelNames: ['method', 'path', 'status'], 15 | }); 16 | register.registerMetric(httpRequestCounter); 17 | 18 | // Middleware to count requests 19 | const trackRequests = (req, res, next) => { 20 | res.on('finish', () => { 21 | httpRequestCounter.inc({ 22 | method: req.method, 23 | path: req.route ? req.route.path : req.path, 24 | status: res.statusCode, 25 | }); 26 | }); 27 | next(); 28 | }; 29 | 30 | // Metrics endpoint handler 31 | /* 32 | const getMetrics = async (req, res) => { 33 | res.set('Content-Type', register.contentType); 34 | res.send(await register.metrics()); 35 | }; 36 | */ 37 | 38 | // Metrics endpoint handler 39 | const getMetrics = async (req, res) => { 40 | // Get the Authorization header 41 | const authHeader = req.headers['authorization']; 42 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 43 | return res.status(401).json({ error: 'Unauthorized: No token provided' }); 44 | } 45 | 46 | // Extract the token 47 | const token = authHeader.split(' ')[1]; 48 | 49 | // Verify and decode the token 50 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 51 | const userId = decoded.userId; // Ensure your JWT payload contains a `userId` 52 | 53 | if (!userId) { 54 | return res.status(400).json({ error: 'Invalid token: userId missing' }); 55 | } 56 | 57 | // Set content type and retrieve metrics 58 | res.set('Content-Type', register.contentType); 59 | const metricsText = await register.metrics(); 60 | const metrics = parseMetrics(metricsText); // Helper function to parse metrics. 61 | 62 | // Save metrics to the database 63 | await Metrics.saveMetricsToDatabase(userId, metrics); 64 | 65 | // Send the metrics as a response 66 | res.set('Content-Type', register.contentType); 67 | res.send(metricsText); 68 | }; 69 | 70 | function parseMetrics(metricsText) { 71 | const lines = metricsText.split("\n"); 72 | const metrics = []; 73 | 74 | lines.forEach(line => { 75 | // Look for metric lines that follow the format: 'metric_name value' 76 | if (!line.startsWith("#") && line.trim() !== "") { 77 | const [metricName, metricValue] = line.split(/\s+/); 78 | if (metricName && metricValue) { 79 | metrics.push({ metricName, metricValue }); 80 | } 81 | } 82 | }); 83 | 84 | return metrics; 85 | } 86 | 87 | 88 | const createMetric = async (req, res) => { 89 | const { userId, metricName, metricValue } = req.body; 90 | const metric = await Metrics.create({ userId, metricName, metricValue }); 91 | res.status(201).json(metric); 92 | }; 93 | 94 | const getMetricsByUser = async (req, res) => { 95 | const { userId } = req.params; 96 | const metrics = await Metrics.findByUserId(userId); 97 | res.status(200).json(metrics); 98 | }; 99 | 100 | const updateMetric = async (req, res) => { 101 | const { id } = req.params; 102 | const { metricName, metricValue } = req.body; 103 | const metric = await Metrics.update(id, { metricName, metricValue }); 104 | res.status(200).json(metric); 105 | }; 106 | 107 | const deleteMetric = async (req, res) => { 108 | const { id } = req.params; 109 | await Metrics.delete(id); 110 | res.status(200).json({ message: 'Metric deleted successfully' }); 111 | }; 112 | 113 | module.exports = { 114 | trackRequests, 115 | getMetrics, 116 | createMetric, 117 | getMetricsByUser, 118 | updateMetric, 119 | deleteMetric, 120 | }; 121 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | require('express-async-errors'); 2 | const User = require('../models/user'); 3 | const Joi = require('joi'); 4 | const logger = require('../utils/logger'); 5 | 6 | // Validation schemas 7 | const userSchema = Joi.object({ 8 | name: Joi.string().min(3).required(), 9 | email: Joi.string().email().required(), 10 | password: Joi.string().min(8).required(), 11 | }); 12 | 13 | const getUsers = async (req, res) => { 14 | const users = await User.getAll(req.query); 15 | res.status(200).json(users); 16 | }; 17 | 18 | const getUserById = async (req, res) => { 19 | const userId = parseInt(req.params.id, 10); // Ensure userId is a number 20 | if (isNaN(userId)) { 21 | return res.status(400).json({ error: 'Invalid user ID' }); 22 | } 23 | const user = await User.getById(userId); 24 | if (!user) return res.status(404).json({ message: 'User not found' }); 25 | res.status(200).json(user); 26 | }; 27 | 28 | const createUser = async (req, res) => { 29 | const { error, value } = userSchema.validate(req.body); 30 | if (error) { 31 | return res.status(400).json({ error: error.details[0].message }); 32 | } 33 | if (!req.file) { 34 | return res.status(400).json({ error: "Profile picture is required" }); 35 | } 36 | value.picture = req.file.filename; // Add uploaded filename to user data 37 | const userId = await User.create(value); 38 | res.status(201).json({ message: "User added", userId }); 39 | }; 40 | 41 | const updateUser = async (req, res) => { 42 | const { id } = req.params; 43 | const { name, email, password } = req.body; 44 | 45 | if (!email || !email.match(/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/)) { 46 | return res.status(400).json({ error: 'Invalid email format' }); 47 | } 48 | 49 | const user = await User.getById(id); 50 | if (!user) { 51 | return res.status(404).json({ error: 'User not found' }); 52 | } 53 | 54 | // Assuming `User.update` returns the updated user 55 | await User.update(id, { name, email, password }); 56 | 57 | // If `User.update` doesn't return the full user object, fetch it again 58 | const updatedUserDetails = await User.getById(id); 59 | 60 | return res.status(200).json({ 61 | message: `User modified with ID: ${id}`, 62 | user: updatedUserDetails, // return the updated user object 63 | }); 64 | }; 65 | 66 | const deleteUser = async (req, res) => { 67 | const { id } = req.params; 68 | 69 | // Ensure that the ID is a valid integer 70 | if (!Number.isInteger(Number(id))) { 71 | return res.status(400).json({ error: 'Invalid user ID format' }); 72 | } 73 | 74 | const user = await User.getById(id); // Check if the user exists 75 | if (!user) { 76 | return res.status(404).json({ error: 'User not found' }); 77 | } 78 | 79 | await User.delete(id); // Proceed with the deletion 80 | res.status(200).json({ message: `User soft deleted with ID: ${id}` }); 81 | }; 82 | 83 | module.exports = { 84 | getUsers, 85 | getUserById, 86 | createUser, 87 | updateUser, 88 | deleteUser, 89 | }; 90 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | node-api: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "3000:3000" 10 | environment: 11 | - NODE_ENV=development 12 | 13 | prometheus: 14 | image: prom/prometheus 15 | volumes: 16 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 17 | ports: 18 | - "9090:9090" 19 | 20 | grafana: 21 | image: grafana/grafana 22 | ports: 23 | - "3001:3000" 24 | environment: 25 | - GF_SECURITY_ADMIN_USER=admin 26 | - GF_SECURITY_ADMIN_PASSWORD=admin 27 | volumes: 28 | - grafana-storage:/var/lib/grafana 29 | 30 | volumes: 31 | grafana-storage: 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const multer = require('multer'); 3 | const path = require('path'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const helmet = require('helmet'); 7 | const rateLimit = require('express-rate-limit'); 8 | const userRoutes = require('./routes/userRoutes'); 9 | const loginRoutes = require('./routes/loginRoutes'); 10 | const errorHandler = require('./middleware/errorHandler'); 11 | const swaggerJsDoc = require('swagger-jsdoc'); 12 | const { swaggerUi, swaggerSpec } = require('./config/swagger'); // Import Swagger UI 13 | const metricsRoutes = require('./routes/metricsRoutes'); 14 | const { trackRequests } = require('./controllers/metricsController'); 15 | 16 | dotenv.config(); 17 | 18 | const app = express(); 19 | const port = process.env.PORT || 3000; 20 | 21 | app.use(express.json()); 22 | 23 | // Middleware 24 | app.use(helmet()); 25 | app.use(bodyParser.json()); 26 | app.use(bodyParser.urlencoded({ extended: true })); 27 | app.use(trackRequests); 28 | 29 | // Rate Limiting 30 | const globalLimiter = rateLimit({ 31 | windowMs: 15 * 60 * 1000, // 15 minutes 32 | max: 100, // limit each IP to 100 requests per windowMs 33 | message: 'Too many requests, please try again later.', 34 | headers: true, // Sends the rate limit info in the response headers 35 | }); 36 | 37 | app.use(globalLimiter); 38 | 39 | const cors = require('cors'); 40 | app.use(cors({ 41 | origin: 'http://localhost:3000/api-docs', 42 | methods: ['GET', 'POST', 'PUT', 'DELETE'], 43 | allowedHeaders: ['Content-Type', 'Authorization'], // Make sure Authorization is allowed 44 | })); 45 | 46 | // Serve Swagger documentation at the /api-docs route 47 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); 48 | 49 | app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); 50 | 51 | // Serve static files from the public directory 52 | app.use(express.static(path.join(__dirname, 'public'))); 53 | 54 | // Password reset route 55 | app.get('/reset-password/:token', (req, res) => { 56 | res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); 57 | }); 58 | 59 | // Routes 60 | app.use('/users', userRoutes); 61 | 62 | // Use login routes 63 | app.use('/', loginRoutes); 64 | 65 | // Use metrics routes 66 | app.use('/metrics', metricsRoutes); 67 | 68 | // Error Handler 69 | app.use(errorHandler); 70 | 71 | const loginLimiter = rateLimit({ 72 | windowMs: 15 * 60 * 1000, // 15 minutes 73 | max: 5, // limit each IP to 5 login requests per windowMs 74 | message: 'Too many login attempts, please try again after 15 minutes', 75 | }); 76 | 77 | app.use('/login', loginLimiter); 78 | 79 | const storage = multer.diskStorage({ 80 | destination: (req, file, cb) => { 81 | cb(null, 'uploads/'); 82 | }, 83 | filename: (req, file, cb) => { 84 | cb(null, Date.now() + path.extname(file.originalname)); 85 | }, 86 | }); 87 | 88 | const upload = multer({ storage }); 89 | 90 | app.post('/users/:id/profile-picture', upload.single('profilePicture'), (req, res) => { 91 | const id = parseInt(req.params.id); 92 | const picturePath = req.file.path; 93 | 94 | pool.query('UPDATE users SET profile_picture = $1 WHERE id = $2', [picturePath, id], (error) => { 95 | if (error) { 96 | throw error; 97 | } 98 | res.status(200).send(`Profile picture updated for user ID: ${id}`); 99 | }); 100 | }); 101 | 102 | const swaggerOptions = { 103 | swaggerDefinition: { 104 | openapi: '3.0.0', 105 | info: { 106 | title: 'User API', 107 | version: '1.0.0', 108 | description: 'User management API', 109 | }, 110 | }, 111 | apis: ['./routes/*.js'], // path where API docs are located 112 | }; 113 | 114 | const swaggerDocs = swaggerJsDoc(swaggerOptions); 115 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs)); 116 | 117 | // Conditionally start the server only if this file is the entry point 118 | if (require.main === module) { 119 | app.listen(port, () => { 120 | console.log(`App running on port ${port}.`); 121 | }); 122 | } 123 | 124 | module.exports = app; 125 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | const authenticateToken = (req, res, next) => { 4 | const authHeader = req.headers['authorization']; 5 | const token = authHeader && authHeader.split(' ')[1]; 6 | 7 | if (!token) return res.status(401).json({ error: 'Unauthorized' }); // Change 'message' to 'error' 8 | 9 | jwt.verify(token, process.env.JWT_SECRET, (err, user) => { 10 | if (err) return res.status(403).json({ error: 'Invalid or expired token' }); // Change 'message' to 'error' 11 | 12 | req.user = user; // Attach user info from the token 13 | next(); 14 | }); 15 | }; 16 | 17 | module.exports = authenticateToken; 18 | -------------------------------------------------------------------------------- /middleware/authorize.js: -------------------------------------------------------------------------------- 1 | const authorize = (...allowedRoles) => { 2 | return (req, res, next) => { 3 | if (!req.user || !allowedRoles.includes(req.user.role)) { 4 | return res.status(403).json({ message: 'Forbidden: Access is denied' }); 5 | } 6 | next(); 7 | }; 8 | }; 9 | 10 | module.exports = authorize; -------------------------------------------------------------------------------- /middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | const errorHandler = (err, req, res, next) => { 2 | console.error(err.stack); 3 | res.status(500).json({ message: 'An error occurred', error: err.message }); 4 | }; 5 | 6 | module.exports = errorHandler; 7 | -------------------------------------------------------------------------------- /middleware/upload.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'); 2 | const path = require('path'); 3 | 4 | const storage = multer.diskStorage({ 5 | destination: (req, file, cb) => { 6 | cb(null, 'uploads/'); // Directory for uploaded files 7 | }, 8 | filename: (req, file, cb) => { 9 | const uniqueName = `${Date.now()}-${file.originalname}`; 10 | cb(null, uniqueName); 11 | }, 12 | }); 13 | 14 | const fileFilter = (req, file, cb) => { 15 | const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; 16 | if (allowedTypes.includes(file.mimetype)) { 17 | cb(null, true); 18 | } else { 19 | cb(new Error('Invalid file type. Only JPEG, PNG, and GIF are allowed.')); 20 | } 21 | }; 22 | 23 | const upload = multer({ 24 | storage, 25 | limits: { fileSize: 2 * 1024 * 1024 }, // Limit: 2MB 26 | fileFilter, 27 | }); 28 | 29 | module.exports = upload; 30 | -------------------------------------------------------------------------------- /models/metrics.js: -------------------------------------------------------------------------------- 1 | const pool = require('../config/db'); 2 | 3 | class Metrics { 4 | static async create({ userId, metricName, metricValue }) { 5 | const result = await pool.query( 6 | 'INSERT INTO metrics (user_id, metric_name, metric_value) VALUES ($1, $2, $3) RETURNING *', 7 | [userId, metricName, metricValue] 8 | ); 9 | return result.rows[0]; 10 | } 11 | 12 | static async findByUserId(userId) { 13 | const result = await pool.query('SELECT * FROM metrics WHERE user_id = $1', [userId]); 14 | return result.rows; 15 | } 16 | 17 | static async update(id, { metricName, metricValue }) { 18 | const result = await pool.query( 19 | 'UPDATE metrics SET metric_name = $1, metric_value = $2, updated_at = NOW() WHERE id = $3 RETURNING *', 20 | [metricName, metricValue, id] 21 | ); 22 | return result.rows[0]; 23 | } 24 | 25 | static async delete(id) { 26 | await pool.query('DELETE FROM metrics WHERE id = $1', [id]); 27 | } 28 | 29 | static async saveMetricsToDatabase(userId, metrics) { 30 | for (const metric of metrics) { 31 | const { metricName, metricValue } = metric; 32 | 33 | // Use your database library to insert data, e.g., with PostgreSQL: 34 | await pool.query( 35 | "INSERT INTO metrics (user_id, metric_name, metric_value) VALUES ($1, $2, $3)", 36 | [userId, metricName, metricValue] 37 | ); 38 | } 39 | } 40 | } 41 | 42 | module.exports = Metrics; 43 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const pool = require('../config/db'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | class User { 5 | static async getAll({ page = 1, limit = 10, search, sort }) { 6 | const offset = (page - 1) * limit; 7 | let query = 'SELECT * FROM users'; 8 | const params = []; 9 | 10 | if (search) { 11 | query += ` WHERE name ILIKE $${params.length + 1} OR email ILIKE $${params.length + 2}`; 12 | params.push(`%${search}%`, `%${search}%`); 13 | } 14 | 15 | query += sort ? ` ORDER BY ${sort}` : ' ORDER BY id ASC'; 16 | query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`; 17 | params.push(limit, offset); 18 | 19 | const result = await pool.query(query, params); 20 | return result.rows; 21 | } 22 | 23 | static async getById(id) { 24 | const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]); 25 | if (result.rows.length === 0) { 26 | return null; 27 | } 28 | return result.rows[0]; 29 | } 30 | 31 | static async create({ name, email, password, picture }) { 32 | if (!password) { 33 | throw new Error("Password is required"); 34 | } 35 | const hashedPassword = await bcrypt.hash(password, 10); 36 | const result = await pool.query( 37 | 'INSERT INTO users (name, email, password, picture) VALUES ($1, $2, $3, $4) RETURNING id', 38 | [name, email, hashedPassword, picture] 39 | ); 40 | return result.rows[0].id; 41 | } 42 | 43 | static async update(id, { name, email, password }) { 44 | // Hash the new password before updating 45 | const hashedPassword = await bcrypt.hash(password, 10); 46 | 47 | // Update query includes updated_at column 48 | const updatedAt = new Date(); // Current timestamp 49 | await pool.query( 50 | 'UPDATE users SET name = $1, email = $2, password = $3, updated_at = $4 WHERE id = $5', 51 | [name, email, hashedPassword, updatedAt, id] 52 | ); 53 | } 54 | 55 | static async delete(id) { 56 | await pool.query('UPDATE users SET deleted_at = NOW() WHERE id = $1', [id]); 57 | } 58 | 59 | static async uploadProfilePicture(id, profilePicture) { 60 | await pool.query( 61 | 'UPDATE users SET profile_picture = $1 WHERE id = $2', 62 | [profilePicture, id] 63 | ); 64 | } 65 | } 66 | 67 | module.exports = User; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-api-postgres", 3 | "version": "1.0.0", 4 | "description": "RESTful API with Node.js, Express, and PostgreSQL", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@sendgrid/mail": "^8.1.5", 9 | "bcryptjs": "^2.4.3", 10 | "chai-http": "^5.1.1", 11 | "cors": "^2.8.5", 12 | "dotenv": "^16.4.5", 13 | "express": "^4.19.2", 14 | "express-async-errors": "^3.1.1", 15 | "express-rate-limit": "^7.4.1", 16 | "express-rate-limit-redis": "^0.0.4", 17 | "helmet": "^8.0.0", 18 | "jest": "^29.7.0", 19 | "joi": "^17.13.3", 20 | "jsonwebtoken": "^9.0.2", 21 | "multer": "^1.4.5-lts.1", 22 | "nodemailer": "^6.10.1", 23 | "pg": "^8.12.0", 24 | "prom-client": "^15.1.3", 25 | "redis": "^4.7.0", 26 | "swagger-jsdoc": "^6.2.8", 27 | "swagger-ui-express": "^5.0.1", 28 | "winston": "^3.15.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.26.0", 32 | "@babel/plugin-transform-modules-commonjs": "^7.25.9", 33 | "@babel/preset-env": "^7.26.0", 34 | "@babel/register": "^7.25.9", 35 | "chai": "^4.3.6", 36 | "mocha": "^10.8.2", 37 | "nyc": "^17.1.0", 38 | "supertest": "^7.0.0" 39 | }, 40 | "nyc": { 41 | "require": [ 42 | "@babel/register" 43 | ], 44 | "extension": [ 45 | ".js", 46 | ".mjs" 47 | ], 48 | "include": [ 49 | "src/**/*.js", 50 | "src/**/*.mjs" 51 | ], 52 | "exclude": [ 53 | "test/**/*.test.js", 54 | "node_modules" 55 | ] 56 | }, 57 | "scripts": { 58 | "test": "NODE_ENV=test DEBUG=module:* nyc mocha --require @babel/register", 59 | "start": "node index.js", 60 | "dev": "nodemon index.js" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | 4 | scrape_configs: 5 | - job_name: 'node-api-postgres' 6 | static_configs: 7 | - targets: ['localhost:3000'] # Adjust this if running Docker Desktop 8 | -------------------------------------------------------------------------------- /public/reset-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reset Password 5 | 42 | 43 | 44 |

Reset Your Password

45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 |
Processing...
56 |
57 |

58 |

59 | 60 | 126 | 127 | -------------------------------------------------------------------------------- /routes/loginRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { login, register, forgotPassword, resetPassword } = require('../controllers/authController'); 4 | 5 | /** 6 | * @swagger 7 | * /login: 8 | * post: 9 | * summary: Login an existing user 10 | * description: Authenticates the user and returns a JWT token 11 | * security: [] 12 | * requestBody: 13 | * required: true 14 | * content: 15 | * application/json: 16 | * schema: 17 | * type: object 18 | * properties: 19 | * email: 20 | * type: string 21 | * password: 22 | * type: string 23 | * responses: 24 | * 200: 25 | * description: Login successful 26 | */ 27 | router.post('/login', login); 28 | 29 | /** 30 | * @swagger 31 | * /register: 32 | * post: 33 | * summary: Register a new user 34 | * description: Creates a new user and returns a success message 35 | * requestBody: 36 | * required: true 37 | * content: 38 | * application/json: 39 | * schema: 40 | * type: object 41 | * properties: 42 | * name: 43 | * type: string 44 | * description: The user's name 45 | * email: 46 | * type: string 47 | * description: The user's email 48 | * password: 49 | * type: string 50 | * description: The user's password 51 | * required: 52 | * - name 53 | * - email 54 | * - password 55 | * responses: 56 | * 201: 57 | * description: User successfully created 58 | * content: 59 | * application/json: 60 | * schema: 61 | * type: object 62 | * properties: 63 | * message: 64 | * type: string 65 | * description: Confirmation message of successful user creation 66 | * 400: 67 | * description: Bad request (invalid input) 68 | * 500: 69 | * description: Server error 70 | */ 71 | router.post('/register', register); 72 | 73 | /** 74 | * @swagger 75 | * /forgot-password: 76 | * post: 77 | * summary: Request password reset 78 | */ 79 | router.post('/forgot-password', forgotPassword); 80 | 81 | /** 82 | * @swagger 83 | * /reset-password/{token}: 84 | * post: 85 | * summary: Reset password with token 86 | */ 87 | router.post('/reset-password/:token', resetPassword); 88 | 89 | module.exports = router; 90 | -------------------------------------------------------------------------------- /routes/metricsRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const metricsController = require('../controllers/metricsController'); 4 | const authenticateToken = require('../middleware/auth'); 5 | 6 | // Metrics endpoint 7 | router.get('/', authenticateToken, metricsController.getMetrics); 8 | router.post('/', authenticateToken, metricsController.createMetric); 9 | router.get('/user/:userId', authenticateToken, metricsController.getMetricsByUser); 10 | router.put('/:id', authenticateToken, metricsController.updateMetric); 11 | router.delete('/:id', authenticateToken, metricsController.deleteMetric); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const userController = require('../controllers/userController'); 3 | const auth = require('../middleware/auth'); 4 | const multer = require('multer'); 5 | const upload = multer({ dest: 'uploads/' }); 6 | 7 | const router = express.Router(); 8 | 9 | /** 10 | * @openapi 11 | * components: 12 | * securitySchemes: 13 | * bearerAuth: 14 | * type: http 15 | * scheme: bearer 16 | * bearerFormat: JWT 17 | * schemas: 18 | * User: 19 | * type: object 20 | * properties: 21 | * name: 22 | * type: string 23 | * email: 24 | * type: string 25 | * password: 26 | * type: string 27 | * profile_picture: 28 | * type: string 29 | * format: binary 30 | * 31 | * /users: 32 | * get: 33 | * tags: 34 | * - Users 35 | * summary: Get all users 36 | * security: 37 | * - bearerAuth: [] 38 | * responses: 39 | * 200: 40 | * description: List of users retrieved successfully 41 | * 401: 42 | * description: Unauthorized - invalid token 43 | * 44 | * post: 45 | * tags: 46 | * - Users 47 | * summary: Create a new user 48 | * security: 49 | * - bearerAuth: [] 50 | * requestBody: 51 | * required: true 52 | * content: 53 | * multipart/form-data: 54 | * schema: 55 | * type: object 56 | * properties: 57 | * name: 58 | * type: string 59 | * email: 60 | * type: string 61 | * password: 62 | * type: string 63 | * picture: 64 | * type: string 65 | * format: binary 66 | * responses: 67 | * 201: 68 | * description: User created successfully 69 | * 401: 70 | * description: Unauthorized - invalid token 71 | * 72 | * /users/{id}: 73 | * get: 74 | * tags: 75 | * - Users 76 | * summary: Get user by ID 77 | * security: 78 | * - bearerAuth: [] 79 | * parameters: 80 | * - in: path 81 | * name: id 82 | * required: true 83 | * schema: 84 | * type: integer 85 | * responses: 86 | * 200: 87 | * description: User found successfully 88 | * 401: 89 | * description: Unauthorized - invalid token 90 | * 404: 91 | * description: User not found 92 | * 93 | * put: 94 | * tags: 95 | * - Users 96 | * summary: Update user 97 | * security: 98 | * - bearerAuth: [] 99 | * parameters: 100 | * - in: path 101 | * name: id 102 | * required: true 103 | * schema: 104 | * type: integer 105 | * requestBody: 106 | * required: true 107 | * content: 108 | * application/json: 109 | * schema: 110 | * type: object 111 | * properties: 112 | * name: 113 | * type: string 114 | * email: 115 | * type: string 116 | * password: 117 | * type: string 118 | * responses: 119 | * 200: 120 | * description: User updated successfully 121 | * 401: 122 | * description: Unauthorized - invalid token 123 | * 404: 124 | * description: User not found 125 | * 126 | * delete: 127 | * tags: 128 | * - Users 129 | * summary: Delete user 130 | * security: 131 | * - bearerAuth: [] 132 | * parameters: 133 | * - in: path 134 | * name: id 135 | * required: true 136 | * schema: 137 | * type: integer 138 | * responses: 139 | * 200: 140 | * description: User deleted successfully 141 | * 401: 142 | * description: Unauthorized - invalid token 143 | * 404: 144 | * description: User not found 145 | */ 146 | 147 | router.get('/', auth, userController.getUsers); 148 | router.post('/', auth, upload.single('picture'), userController.createUser); 149 | router.get('/:id', auth, userController.getUserById); 150 | router.put('/:id', auth, userController.updateUser); 151 | router.delete('/:id', auth, userController.deleteUser); 152 | 153 | module.exports = router; 154 | -------------------------------------------------------------------------------- /test/userRoutes.test.mjs: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiHttp from 'chai-http'; 3 | import request from 'supertest'; 4 | import app from '../index.js'; 5 | import bcrypt from 'bcryptjs'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | chai.use(chaiHttp); 11 | const { expect } = chai; 12 | 13 | describe('User API', () => { 14 | let token; // Store the authentication token 15 | let server; 16 | 17 | // Start server before tests and login to get token 18 | before(async () => { 19 | // Start the app server 20 | server = app.listen(3000, () => { 21 | console.log('Test server started'); 22 | }); 23 | 24 | const res = await request(app) 25 | .post('/login') 26 | .send({ email: 'alaa@gmail.com', password: 'alaapass' }); 27 | 28 | token = res.body.token; // Adjust depending on your login response 29 | }); 30 | 31 | describe('GET /users', () => { 32 | it('should return a list of users with a valid token', async () => { 33 | const res = await request(app) 34 | .get('/users') 35 | .set('Authorization', `Bearer ${token}`); 36 | 37 | expect(res.status).to.equal(200); 38 | expect(res.body).to.be.an('array'); 39 | }); 40 | 41 | it('should return 401 without a token', async () => { 42 | const res = await request(app).get('/users'); 43 | expect(res.status).to.equal(401); 44 | expect(res.body).to.have.property('error', 'Unauthorized'); 45 | }); 46 | }); 47 | 48 | describe('GET /users/:id', () => { 49 | it('should return a user by ID with a valid token', async () => { 50 | const userId = 9; // Replace with an actual valid numeric user ID in your database 51 | const res = await request(app) 52 | .get(`/users/${userId}`) 53 | .set('Authorization', `Bearer ${token}`); 54 | 55 | expect(res.status).to.equal(200); 56 | expect(res.body).to.have.property('id', userId); 57 | expect(res.body).to.have.property('name'); 58 | expect(res.body).to.have.property('email'); 59 | }); 60 | 61 | it('should return 404 if user not found', async () => { 62 | const invalidUserId = 999999; // Replace with a numeric ID that doesn't exist in your database 63 | const res = await request(app) 64 | .get(`/users/${invalidUserId}`) 65 | .set('Authorization', `Bearer ${token}`); 66 | 67 | expect(res.status).to.equal(404); 68 | expect(res.body).to.have.property('message', 'User not found'); 69 | }); 70 | 71 | it('should return 401 without a token', async () => { 72 | const userId = 1; // Replace with an actual valid numeric user ID in your database 73 | const res = await request(app) 74 | .get(`/users/${userId}`); 75 | 76 | expect(res.status).to.equal(401); 77 | expect(res.body).to.have.property('error', 'Unauthorized'); 78 | }); 79 | }); 80 | 81 | describe('POST /users', () => { 82 | it('should create a new user with valid data and token', async () => { 83 | const filePath = path.join(__dirname, 'fixtures/sample-profile-pic.jpg'); 84 | 85 | const res = await request(app) 86 | .post('/users') 87 | .set('Authorization', `Bearer ${token}`) 88 | .field('name', 'John Doe') 89 | .field('email', 'john.doe@example.com') 90 | .field('password', 'password123') 91 | .attach('picture', filePath); // Attach a file 92 | 93 | expect(res.status).to.equal(201); 94 | expect(res.body).to.have.property('message', 'User added'); 95 | }); 96 | 97 | it('should return 400 for invalid user data', async () => { 98 | const res = await request(app) 99 | .post('/users') 100 | .set('Authorization', `Bearer ${token}`) 101 | .send({ name: 'JD', email: 'not-an-email', password: '123' }); 102 | 103 | expect(res.status).to.equal(400); 104 | expect(res.body).to.have.property('error').that.includes('"name" length must be at least 3 characters long'); 105 | }); 106 | 107 | it('should return 401 without a token', async () => { 108 | const res = await request(app) 109 | .post('/users') 110 | .send({ name: 'Jane Doe', email: 'jane.doe@example.com', password: 'password123' }); 111 | 112 | expect(res.status).to.equal(401); 113 | expect(res.body).to.have.property('error', 'Unauthorized'); 114 | }); 115 | }); 116 | 117 | describe('PUT /users/:id', () => { 118 | it('should update a user with valid data', async () => { 119 | const userId = 44; // Use a valid integer ID 120 | const updatedData = { name: 'John Updated', email: 'john.updated@example.com', password: 'password123' }; 121 | 122 | const res = await request(app) 123 | .put(`/users/${userId}`) 124 | .set('Authorization', `Bearer ${token}`) 125 | .send(updatedData); 126 | 127 | expect(res.status).to.equal(200); 128 | expect(res.body).to.have.property('message', `User modified with ID: ${userId}`); 129 | expect(res.body).to.have.property('user'); 130 | expect(res.body.user).to.have.property('name', 'John Updated'); 131 | expect(res.body.user).to.have.property('email', 'john.updated@example.com'); 132 | 133 | const isPasswordCorrect = await bcrypt.compare(updatedData.password, res.body.user.password); 134 | expect(isPasswordCorrect).to.be.true; 135 | }); 136 | 137 | it('should return 400 for invalid user data', async () => { 138 | const userId = 44; // Use a valid user ID 139 | const updatedData = { name: 'JD', email: 'not-an-email', password: '123' }; 140 | 141 | const res = await request(app) 142 | .put(`/users/${userId}`) 143 | .set('Authorization', `Bearer ${token}`) 144 | .send(updatedData); 145 | 146 | expect(res.status).to.equal(400); 147 | expect(res.body).to.have.property('error').that.includes('Invalid email format'); 148 | }); 149 | 150 | it('should return 404 if user not found', async () => { 151 | const invalidUserId = 9999; // Use a non-existing user ID 152 | const updatedData = { name: 'Nonexistent User', email: 'nonexistent@example.com', password: 'password123' }; 153 | 154 | const res = await request(app) 155 | .put(`/users/${invalidUserId}`) 156 | .set('Authorization', `Bearer ${token}`) 157 | .send(updatedData); 158 | 159 | expect(res.status).to.equal(404); 160 | expect(res.body).to.have.property('error', 'User not found'); 161 | }); 162 | 163 | it('should return 401 without a token', async () => { 164 | const userId = 44; // Use a valid user ID 165 | const updatedData = { name: 'John Doe', email: 'john.doe@example.com', password: 'password123' }; 166 | 167 | const res = await request(app) 168 | .put(`/users/${userId}`) 169 | .send(updatedData); 170 | 171 | expect(res.status).to.equal(401); 172 | expect(res.body).to.have.property('error', 'Unauthorized'); 173 | }); 174 | }); 175 | 176 | 177 | describe('DELETE /users/:id', () => { 178 | it('should delete a user with valid ID', async () => { 179 | const userId = 45; // Replace with an actual user ID (integer) 180 | 181 | const res = await request(app) 182 | .delete(`/users/${userId}`) 183 | .set('Authorization', `Bearer ${token}`); 184 | 185 | expect(res.status).to.equal(200); 186 | expect(res.body).to.have.property('message', 'User soft deleted with ID: ' + userId); 187 | }); 188 | 189 | it('should return 404 if user not found', async () => { 190 | const invalidUserId = 999999; // Use a non-existent integer ID 191 | 192 | const res = await request(app) 193 | .delete(`/users/${invalidUserId}`) 194 | .set('Authorization', `Bearer ${token}`); 195 | 196 | expect(res.status).to.equal(404); 197 | expect(res.body).to.have.property('error', 'User not found'); 198 | }); 199 | 200 | it('should return 401 without a token', async () => { 201 | const userId = 45; // Replace with a valid integer ID 202 | 203 | const res = await request(app) 204 | .delete(`/users/${userId}`); 205 | 206 | expect(res.status).to.equal(401); 207 | expect(res.body).to.have.property('error', 'Unauthorized'); 208 | }); 209 | }); 210 | 211 | // Close the server after tests 212 | after(async () => { 213 | if (server) { 214 | server.close(); 215 | } 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /uploads/873409ad6aa037c801ba38a04be33200: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JawherKl/node-api-postgres/9715458598c91d01a1fa54b7b3d1c093abdf8bb1/uploads/873409ad6aa037c801ba38a04be33200 -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, transports, format } = require('winston'); 2 | 3 | const logger = createLogger({ 4 | level: 'info', 5 | format: format.combine( 6 | format.timestamp(), 7 | format.json() 8 | ), 9 | transports: [ 10 | new transports.File({ filename: 'app.log' }), 11 | ], 12 | }); 13 | 14 | module.exports = logger; -------------------------------------------------------------------------------- /utils/mailer.js: -------------------------------------------------------------------------------- 1 | const sgMail = require('@sendgrid/mail'); 2 | 3 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 4 | 5 | const sendEmail = async ({ to, subject, text, html }) => { 6 | const msg = { 7 | to, 8 | from: process.env.SENDGRID_FROM_EMAIL, // verified sender email in SendGrid 9 | subject, 10 | text, 11 | html: html || text // If no HTML is provided, use the text content 12 | }; 13 | 14 | try { 15 | await sgMail.send(msg); 16 | return { success: true }; 17 | } catch (error) { 18 | console.error('Email sending failed:', error); 19 | throw error; 20 | } 21 | }; 22 | 23 | module.exports = { sendEmail }; --------------------------------------------------------------------------------