├── .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 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | 
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 | 
14 | 
15 | 
16 | 
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 | [](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 | [](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 |