init
commit
2677abe35f
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
|
@ -0,0 +1,39 @@
|
||||||
|
MARIA_HOST =172.16.1.1
|
||||||
|
MARIA_USER =beanstalk
|
||||||
|
MARIA_PASS =Zu*B]Lx_heDFVtt]
|
||||||
|
MARIA_DBNM =beanstalk_iimtt
|
||||||
|
CORS_ALLOWED_ORIGINS=http://192.168.0.166:2022,http://192.168.0.166:2051
|
||||||
|
OPENAI_KEY =sk-proj-AjYBwdJwg6nzuQVsfdgsdfgsdfsdfdfsdfsdfI0AdLNNOYkJZ
|
||||||
|
MONGODB_URL=mongodb://root:pass@10.0.0.90:27017
|
||||||
|
MONGO_DB_NAME=iimttnewdb
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=AKIDFDFSDFDFDFQ4NURNQB
|
||||||
|
AWS_SECRET_ACCESS_KEY=V6T9uZxU/zx3xa9cZ6M/fhfhgfxghxfghgg+DptMbwjXS
|
||||||
|
AWS_REGION=ap-south-1
|
||||||
|
S3_BUCKET_NAME=gamescreenshot
|
||||||
|
|
||||||
|
# Port number
|
||||||
|
PORT=5174
|
||||||
|
HOST = 0.0.0.0
|
||||||
|
# URL of the Mongo DB
|
||||||
|
#MONGODB_URL=mongodb://127.0.0.1:27017/node-boilerplate
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
# JWT secret key
|
||||||
|
JWT_SECRET=thisisasamplesecret
|
||||||
|
# Number of minutes after which an access token expires
|
||||||
|
JWT_ACCESS_EXPIRATION_MINUTES=30
|
||||||
|
# Number of days after which a refresh token expires
|
||||||
|
JWT_REFRESH_EXPIRATION_DAYS=30
|
||||||
|
# Number of minutes after which a reset password token expires
|
||||||
|
JWT_RESET_PASSWORD_EXPIRATION_MINUTES=10
|
||||||
|
# Number of minutes after which a verify email token expires
|
||||||
|
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=10
|
||||||
|
|
||||||
|
# SMTP configuration options for the email service
|
||||||
|
# For testing, you can use a fake SMTP service like Ethereal: https://ethereal.email/create
|
||||||
|
SMTP_HOST=email-server
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=email-server-username
|
||||||
|
SMTP_PASSWORD=email-server-password
|
||||||
|
EMAIL_FROM=support@yourapp.com
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
bin
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"extends": ["airbnb-base", "plugin:jest/recommended", "plugin:security/recommended", "plugin:prettier/recommended"],
|
||||||
|
"plugins": ["jest", "security", "prettier"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": "error",
|
||||||
|
"func-names": "off",
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
"consistent-return": "off",
|
||||||
|
"jest/expect-expect": "off",
|
||||||
|
"security/detect-object-injection": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Convert text file line endings to lf
|
||||||
|
* text eol=lf
|
||||||
|
*.js text
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# yarn error logs
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Environment varibales
|
||||||
|
.env*
|
||||||
|
!.env*.example
|
||||||
|
|
||||||
|
# Code coverage
|
||||||
|
coverage
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn install
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
git status
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint-staged
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"*.js": "eslint"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
coverage
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 125
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- '12'
|
||||||
|
services:
|
||||||
|
- mongodb
|
||||||
|
cache: yarn
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- PORT=3000
|
||||||
|
- MONGODB_URL=mongodb://localhost:27017/node-boilerplate
|
||||||
|
- JWT_SECRET=thisisasamplesecret
|
||||||
|
- JWT_ACCESS_EXPIRATION_MINUTES=30
|
||||||
|
- JWT_REFRESH_EXPIRATION_DAYS=30
|
||||||
|
script:
|
||||||
|
- yarn lint
|
||||||
|
- yarn test
|
||||||
|
after_success: yarn coverage:coveralls
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM node:alpine
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/src/node-app && chown -R node:node /usr/src/node-app
|
||||||
|
|
||||||
|
WORKDIR /usr/src/node-app
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
|
@ -0,0 +1,433 @@
|
||||||
|
# RESTful API Node Server Boilerplate
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
To create a project, simply run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-nodejs-express-app <project-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm init nodejs-express-app <project-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
If you would still prefer to do the installation manually, follow these steps:
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --depth 1 https://github.com/hagopj13/node-express-boilerplate.git
|
||||||
|
cd node-express-boilerplate
|
||||||
|
npx rimraf ./.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# open .env and modify the environment variables (if needed)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [API Documentation](#api-documentation)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Validation](#validation)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Authorization](#authorization)
|
||||||
|
- [Logging](#logging)
|
||||||
|
- [Custom Mongoose Plugins](#custom-mongoose-plugins)
|
||||||
|
- [Linting](#linting)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **NoSQL database**: [MongoDB](https://www.mongodb.com) object data modeling using [Mongoose](https://mongoosejs.com)
|
||||||
|
- **Authentication and authorization**: using [passport](http://www.passportjs.org)
|
||||||
|
- **Validation**: request data validation using [Joi](https://github.com/hapijs/joi)
|
||||||
|
- **Logging**: using [winston](https://github.com/winstonjs/winston) and [morgan](https://github.com/expressjs/morgan)
|
||||||
|
- **Testing**: unit and integration tests using [Jest](https://jestjs.io)
|
||||||
|
- **Error handling**: centralized error handling mechanism
|
||||||
|
- **API documentation**: with [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc) and [swagger-ui-express](https://github.com/scottie1984/swagger-ui-express)
|
||||||
|
- **Process management**: advanced production process management using [PM2](https://pm2.keymetrics.io)
|
||||||
|
- **Dependency management**: with [Yarn](https://yarnpkg.com)
|
||||||
|
- **Environment variables**: using [dotenv](https://github.com/motdotla/dotenv) and [cross-env](https://github.com/kentcdodds/cross-env#readme)
|
||||||
|
- **Security**: set security HTTP headers using [helmet](https://helmetjs.github.io)
|
||||||
|
- **Santizing**: sanitize request data against xss and query injection
|
||||||
|
- **CORS**: Cross-Origin Resource-Sharing enabled using [cors](https://github.com/expressjs/cors)
|
||||||
|
- **Compression**: gzip compression with [compression](https://github.com/expressjs/compression)
|
||||||
|
- **CI**: continuous integration with [Travis CI](https://travis-ci.org)
|
||||||
|
- **Docker support**
|
||||||
|
- **Code coverage**: using [coveralls](https://coveralls.io)
|
||||||
|
- **Code quality**: with [Codacy](https://www.codacy.com)
|
||||||
|
- **Git hooks**: with [husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)
|
||||||
|
- **Linting**: with [ESLint](https://eslint.org) and [Prettier](https://prettier.io)
|
||||||
|
- **Editor config**: consistent editor configuration using [EditorConfig](https://editorconfig.org)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Running locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Running in production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# run all tests
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
# run all tests in watch mode
|
||||||
|
yarn test:watch
|
||||||
|
|
||||||
|
# run test coverage
|
||||||
|
yarn coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# run docker container in development mode
|
||||||
|
yarn docker:dev
|
||||||
|
|
||||||
|
# run docker container in production mode
|
||||||
|
yarn docker:prod
|
||||||
|
|
||||||
|
# run all tests in a docker container
|
||||||
|
yarn docker:test
|
||||||
|
```
|
||||||
|
|
||||||
|
Linting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# run ESLint
|
||||||
|
yarn lint
|
||||||
|
|
||||||
|
# fix ESLint errors
|
||||||
|
yarn lint:fix
|
||||||
|
|
||||||
|
# run prettier
|
||||||
|
yarn prettier
|
||||||
|
|
||||||
|
# fix prettier errors
|
||||||
|
yarn prettier:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The environment variables can be found and modified in the `.env` file. They come with these default values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Port number
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# URL of the Mongo DB
|
||||||
|
MONGODB_URL=mongodb://127.0.0.1:27017/node-boilerplate
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
# JWT secret key
|
||||||
|
JWT_SECRET=thisisasamplesecret
|
||||||
|
# Number of minutes after which an access token expires
|
||||||
|
JWT_ACCESS_EXPIRATION_MINUTES=30
|
||||||
|
# Number of days after which a refresh token expires
|
||||||
|
JWT_REFRESH_EXPIRATION_DAYS=30
|
||||||
|
|
||||||
|
# SMTP configuration options for the email service
|
||||||
|
# For testing, you can use a fake SMTP service like Ethereal: https://ethereal.email/create
|
||||||
|
SMTP_HOST=email-server
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=email-server-username
|
||||||
|
SMTP_PASSWORD=email-server-password
|
||||||
|
EMAIL_FROM=support@yourapp.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src\
|
||||||
|
|--config\ # Environment variables and configuration related things
|
||||||
|
|--controllers\ # Route controllers (controller layer)
|
||||||
|
|--docs\ # Swagger files
|
||||||
|
|--middlewares\ # Custom express middlewares
|
||||||
|
|--models\ # Mongoose models (data layer)
|
||||||
|
|--routes\ # Routes
|
||||||
|
|--services\ # Business logic (service layer)
|
||||||
|
|--utils\ # Utility classes and functions
|
||||||
|
|--validations\ # Request data validation schemas
|
||||||
|
|--app.js # Express app
|
||||||
|
|--index.js # App entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
To view the list of available APIs and their specifications, run the server and go to `http://localhost:3000/v1/docs` in your browser. This documentation page is automatically generated using the [swagger](https://swagger.io/) definitions written as comments in the route files.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
List of available routes:
|
||||||
|
|
||||||
|
**Auth routes**:\
|
||||||
|
`POST /v1/auth/register` - register\
|
||||||
|
`POST /v1/auth/login` - login\
|
||||||
|
`POST /v1/auth/refresh-tokens` - refresh auth tokens\
|
||||||
|
`POST /v1/auth/forgot-password` - send reset password email\
|
||||||
|
`POST /v1/auth/reset-password` - reset password\
|
||||||
|
`POST /v1/auth/send-verification-email` - send verification email\
|
||||||
|
`POST /v1/auth/verify-email` - verify email
|
||||||
|
|
||||||
|
**User routes**:\
|
||||||
|
`POST /v1/users` - create a user\
|
||||||
|
`GET /v1/users` - get all users\
|
||||||
|
`GET /v1/users/:userId` - get user\
|
||||||
|
`PATCH /v1/users/:userId` - update user\
|
||||||
|
`DELETE /v1/users/:userId` - delete user
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The app has a centralized error handling mechanism.
|
||||||
|
|
||||||
|
Controllers should try to catch the errors and forward them to the error handling middleware (by calling `next(error)`). For convenience, you can also wrap the controller inside the catchAsync utility wrapper, which forwards the error.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const catchAsync = require('../utils/catchAsync');
|
||||||
|
|
||||||
|
const controller = catchAsync(async (req, res) => {
|
||||||
|
// this error will be forwarded to the error handling middleware
|
||||||
|
throw new Error('Something wrong happened');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The error handling middleware sends an error response, which has the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "Not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When running in development mode, the error response also contains the error stack.
|
||||||
|
|
||||||
|
The app has a utility ApiError class to which you can attach a response code and a message, and then throw it from anywhere (catchAsync will catch it).
|
||||||
|
|
||||||
|
For example, if you are trying to get a user from the DB who is not found, and you want to send a 404 error, the code should look something like:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
const getUser = async (userId) => {
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Request data is validated using [Joi](https://joi.dev/). Check the [documentation](https://joi.dev/api/) for more details on how to write Joi validation schemas.
|
||||||
|
|
||||||
|
The validation schemas are defined in the `src/validations` directory and are used in the routes by providing them as parameters to the `validate` middleware.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const validate = require('../../middlewares/validate');
|
||||||
|
const userValidation = require('../../validations/user.validation');
|
||||||
|
const userController = require('../../controllers/user.controller');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/users', validate(userValidation.createUser), userController.createUser);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
To require authentication for certain routes, you can use the `auth` middleware.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const auth = require('../../middlewares/auth');
|
||||||
|
const userController = require('../../controllers/user.controller');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/users', auth(), userController.createUser);
|
||||||
|
```
|
||||||
|
|
||||||
|
These routes require a valid JWT access token in the Authorization request header using the Bearer schema. If the request does not contain a valid access token, an Unauthorized (401) error is thrown.
|
||||||
|
|
||||||
|
**Generating Access Tokens**:
|
||||||
|
|
||||||
|
An access token can be generated by making a successful call to the register (`POST /v1/auth/register`) or login (`POST /v1/auth/login`) endpoints. The response of these endpoints also contains refresh tokens (explained below).
|
||||||
|
|
||||||
|
An access token is valid for 30 minutes. You can modify this expiration time by changing the `JWT_ACCESS_EXPIRATION_MINUTES` environment variable in the .env file.
|
||||||
|
|
||||||
|
**Refreshing Access Tokens**:
|
||||||
|
|
||||||
|
After the access token expires, a new access token can be generated, by making a call to the refresh token endpoint (`POST /v1/auth/refresh-tokens`) and sending along a valid refresh token in the request body. This call returns a new access token and a new refresh token.
|
||||||
|
|
||||||
|
A refresh token is valid for 30 days. You can modify this expiration time by changing the `JWT_REFRESH_EXPIRATION_DAYS` environment variable in the .env file.
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
The `auth` middleware can also be used to require certain rights/permissions to access a route.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const auth = require('../../middlewares/auth');
|
||||||
|
const userController = require('../../controllers/user.controller');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/users', auth('manageUsers'), userController.createUser);
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, an authenticated user can access this route only if that user has the `manageUsers` permission.
|
||||||
|
|
||||||
|
The permissions are role-based. You can view the permissions/rights of each role in the `src/config/roles.js` file.
|
||||||
|
|
||||||
|
If the user making the request does not have the required permissions to access this route, a Forbidden (403) error is thrown.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Import the logger from `src/config/logger.js`. It is using the [Winston](https://github.com/winstonjs/winston) logging library.
|
||||||
|
|
||||||
|
Logging should be done according to the following severity levels (ascending order from most important to least important):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('<path to src>/config/logger');
|
||||||
|
|
||||||
|
logger.error('message'); // level 0
|
||||||
|
logger.warn('message'); // level 1
|
||||||
|
logger.info('message'); // level 2
|
||||||
|
logger.http('message'); // level 3
|
||||||
|
logger.verbose('message'); // level 4
|
||||||
|
logger.debug('message'); // level 5
|
||||||
|
```
|
||||||
|
|
||||||
|
In development mode, log messages of all severity levels will be printed to the console.
|
||||||
|
|
||||||
|
In production mode, only `info`, `warn`, and `error` logs will be printed to the console.\
|
||||||
|
It is up to the server (or process manager) to actually read them from the console and store them in log files.\
|
||||||
|
This app uses pm2 in production mode, which is already configured to store the logs in log files.
|
||||||
|
|
||||||
|
Note: API request information (request url, response code, timestamp, etc.) are also automatically logged (using [morgan](https://github.com/expressjs/morgan)).
|
||||||
|
|
||||||
|
## Custom Mongoose Plugins
|
||||||
|
|
||||||
|
The app also contains 2 custom mongoose plugins that you can attach to any mongoose model schema. You can find the plugins in `src/models/plugins`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { toJSON, paginate } = require('./plugins');
|
||||||
|
|
||||||
|
const userSchema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
/* schema definition here */
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
userSchema.plugin(toJSON);
|
||||||
|
userSchema.plugin(paginate);
|
||||||
|
|
||||||
|
const User = mongoose.model('User', userSchema);
|
||||||
|
```
|
||||||
|
|
||||||
|
### toJSON
|
||||||
|
|
||||||
|
The toJSON plugin applies the following changes in the toJSON transform call:
|
||||||
|
|
||||||
|
- removes \_\_v, createdAt, updatedAt, and any schema path that has private: true
|
||||||
|
- replaces \_id with id
|
||||||
|
|
||||||
|
### paginate
|
||||||
|
|
||||||
|
The paginate plugin adds the `paginate` static method to the mongoose schema.
|
||||||
|
|
||||||
|
Adding this plugin to the `User` model schema will allow you to do the following:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const queryUsers = async (filter, options) => {
|
||||||
|
const users = await User.paginate(filter, options);
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `filter` param is a regular mongo filter.
|
||||||
|
|
||||||
|
The `options` param can have the following (optional) fields:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const options = {
|
||||||
|
sortBy: 'name:desc', // sort order
|
||||||
|
limit: 5, // maximum results per page
|
||||||
|
page: 2, // page number
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin also supports sorting by multiple criteria (separated by a comma): `sortBy: name:desc,role:asc`
|
||||||
|
|
||||||
|
The `paginate` method returns a Promise, which fulfills with an object having the following properties:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [],
|
||||||
|
"page": 2,
|
||||||
|
"limit": 5,
|
||||||
|
"totalPages": 10,
|
||||||
|
"totalResults": 48
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
Linting is done using [ESLint](https://eslint.org/) and [Prettier](https://prettier.io).
|
||||||
|
|
||||||
|
In this app, ESLint is configured to follow the [Airbnb JavaScript style guide](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base) with some modifications. It also extends [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) to turn off all rules that are unnecessary or might conflict with Prettier.
|
||||||
|
|
||||||
|
To modify the ESLint configuration, update the `.eslintrc.json` file. To modify the Prettier configuration, update the `.prettierrc.json` file.
|
||||||
|
|
||||||
|
To prevent a certain file or directory from being linted, add it to `.eslintignore` and `.prettierignore`.
|
||||||
|
|
||||||
|
To maintain a consistent coding style across different IDEs, the project contains `.editorconfig`
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## Inspirations
|
||||||
|
|
||||||
|
- [danielfsousa/express-rest-es2017-boilerplate](https://github.com/danielfsousa/express-rest-es2017-boilerplate)
|
||||||
|
- [madhums/node-express-mongoose](https://github.com/madhums/node-express-mongoose)
|
||||||
|
- [kunalkapadia/express-mongoose-es6-rest-api](https://github.com/kunalkapadia/express-mongoose-es6-rest-api)
|
||||||
|
|
||||||
|
## License
|
||||||
|
taken from https://github.com/hagopj13/node-express-boilerplate
|
||||||
|
[MIT](LICENSE)
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const util = require('util');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
async function runCmd(command) {
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await exec(command);
|
||||||
|
console.log(stdout);
|
||||||
|
console.log(stderr);
|
||||||
|
} catch {
|
||||||
|
(error) => {
|
||||||
|
console.log(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasYarn() {
|
||||||
|
try {
|
||||||
|
await execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.log('Please specify the target project directory.');
|
||||||
|
console.log('For example:');
|
||||||
|
console.log(' npx create-nodejs-app my-app');
|
||||||
|
console.log(' OR');
|
||||||
|
console.log(' npm init nodejs-app my-app');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define constants
|
||||||
|
const ownPath = process.cwd();
|
||||||
|
const folderName = process.argv[2];
|
||||||
|
const appPath = path.join(ownPath, folderName);
|
||||||
|
const repo = 'https://github.com/hagopj13/node-express-boilerplate.git';
|
||||||
|
|
||||||
|
// Check if directory already exists
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(appPath);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EEXIST') {
|
||||||
|
console.log('Directory already exists. Please choose another name for the project.');
|
||||||
|
} else {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
try {
|
||||||
|
// Clone repo
|
||||||
|
console.log(`Downloading files from repo ${repo}`);
|
||||||
|
await runCmd(`git clone --depth 1 ${repo} ${folderName}`);
|
||||||
|
console.log('Cloned successfully.');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Change directory
|
||||||
|
process.chdir(appPath);
|
||||||
|
|
||||||
|
// Install dependencies
|
||||||
|
const useYarn = await hasYarn();
|
||||||
|
console.log('Installing dependencies...');
|
||||||
|
if (useYarn) {
|
||||||
|
await runCmd('yarn install');
|
||||||
|
} else {
|
||||||
|
await runCmd('npm install');
|
||||||
|
}
|
||||||
|
console.log('Dependencies installed successfully.');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Copy envornment variables
|
||||||
|
fs.copyFileSync(path.join(appPath, '.env.example'), path.join(appPath, '.env'));
|
||||||
|
console.log('Environment files copied.');
|
||||||
|
|
||||||
|
// Delete .git folder
|
||||||
|
await runCmd('npx rimraf ./.git');
|
||||||
|
|
||||||
|
// Remove extra files
|
||||||
|
fs.unlinkSync(path.join(appPath, 'CHANGELOG.md'));
|
||||||
|
fs.unlinkSync(path.join(appPath, 'CODE_OF_CONDUCT.md'));
|
||||||
|
fs.unlinkSync(path.join(appPath, 'CONTRIBUTING.md'));
|
||||||
|
fs.unlinkSync(path.join(appPath, 'bin', 'createNodejsApp.js'));
|
||||||
|
fs.rmdirSync(path.join(appPath, 'bin'));
|
||||||
|
if (!useYarn) {
|
||||||
|
fs.unlinkSync(path.join(appPath, 'yarn.lock'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Installation is now complete!');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
console.log('We suggest that you start by typing:');
|
||||||
|
console.log(` cd ${folderName}`);
|
||||||
|
console.log(useYarn ? ' yarn dev' : ' npm run dev');
|
||||||
|
console.log();
|
||||||
|
console.log('Enjoy your production-ready Node.js app, which already supports a large number of ready-made features!');
|
||||||
|
console.log('Check README.md for more info.');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup();
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
node-app:
|
||||||
|
container_name: node-app-dev
|
||||||
|
command: yarn dev -L
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
node-app:
|
||||||
|
container_name: node-app-prod
|
||||||
|
command: yarn start
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
node-app:
|
||||||
|
container_name: node-app-test
|
||||||
|
command: yarn test
|
|
@ -0,0 +1,32 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
node-app:
|
||||||
|
build: .
|
||||||
|
image: node-app
|
||||||
|
environment:
|
||||||
|
- MONGODB_URL=mongodb://mongodb:27017/node-boilerplate
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/node-app
|
||||||
|
networks:
|
||||||
|
- node-network
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo:4.2.1-bionic
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
volumes:
|
||||||
|
- dbdata:/data/db
|
||||||
|
networks:
|
||||||
|
- node-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
node-network:
|
||||||
|
driver: bridge
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"script": "src/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"autorestart": true,
|
||||||
|
"watch": false,
|
||||||
|
"time": true,
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testEnvironmentOptions: {
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
},
|
||||||
|
restoreMocks: true,
|
||||||
|
coveragePathIgnorePatterns: ['node_modules', 'src/config', 'src/app.js', 'tests'],
|
||||||
|
coverageReporters: ['text', 'lcov', 'clover', 'html'],
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"name": "iimttapi",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Create a Node.js app for building production-ready RESTful APIs using Express, by running one command",
|
||||||
|
"bin": "bin/createNodejsApp.js",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "pm2 start ecosystem.config.json --no-daemon",
|
||||||
|
"dev": "cross-env NODE_ENV=development nodemon src/index.js",
|
||||||
|
"test": "jest -i --colors --verbose --detectOpenHandles",
|
||||||
|
"test:watch": "jest -i --watchAll",
|
||||||
|
"coverage": "jest -i --coverage",
|
||||||
|
"coverage:coveralls": "jest -i --coverage --coverageReporters=text-lcov | coveralls",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"prettier": "prettier --check **/*.js",
|
||||||
|
"prettier:fix": "prettier --write **/*.js",
|
||||||
|
"docker:prod": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up",
|
||||||
|
"docker:dev": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||||
|
"docker:test": "docker-compose -f docker-compose.yml -f docker-compose.test.yml up",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"node",
|
||||||
|
"node.js",
|
||||||
|
"boilerplate",
|
||||||
|
"generator",
|
||||||
|
"express",
|
||||||
|
"rest",
|
||||||
|
"api",
|
||||||
|
"mongodb",
|
||||||
|
"mongoose",
|
||||||
|
"es6",
|
||||||
|
"es7",
|
||||||
|
"es8",
|
||||||
|
"es9",
|
||||||
|
"jest",
|
||||||
|
"travis",
|
||||||
|
"docker",
|
||||||
|
"passport",
|
||||||
|
"joi",
|
||||||
|
"eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.1669.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.0",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-mongo-sanitize": "^2.0.0",
|
||||||
|
"express-rate-limit": "^5.0.0",
|
||||||
|
"helmet": "^4.1.0",
|
||||||
|
"http-status": "^1.4.0",
|
||||||
|
"joi": "^17.3.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"mongoose": "^5.7.7",
|
||||||
|
"morgan": "^1.9.1",
|
||||||
|
"mysql2": "^3.11.0",
|
||||||
|
"nodemailer": "^6.3.1",
|
||||||
|
"passport": "^0.4.0",
|
||||||
|
"passport-jwt": "^4.0.0",
|
||||||
|
"pm2": "^5.1.0",
|
||||||
|
"swagger-jsdoc": "^6.0.8",
|
||||||
|
"swagger-ui-express": "^4.1.6",
|
||||||
|
"validator": "^13.0.0",
|
||||||
|
"winston": "^3.2.1",
|
||||||
|
"xss-clean": "^0.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"coveralls": "^3.0.7",
|
||||||
|
"eslint": "^7.0.0",
|
||||||
|
"eslint-config-airbnb-base": "^14.0.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-plugin-import": "^2.18.2",
|
||||||
|
"eslint-plugin-jest": "^24.0.1",
|
||||||
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
|
"eslint-plugin-security": "^1.4.0",
|
||||||
|
"faker": "^5.1.0",
|
||||||
|
"husky": "7.0.4",
|
||||||
|
"jest": "^26.0.1",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"node-mocks-http": "^1.8.0",
|
||||||
|
"nodemon": "^2.0.0",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"supertest": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
const express = require('express');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const xss = require('xss-clean');
|
||||||
|
const mongoSanitize = require('express-mongo-sanitize');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const passport = require('passport');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const config = require('./config/config');
|
||||||
|
const morgan = require('./config/morgan');
|
||||||
|
const { jwtStrategy } = require('./config/passport');
|
||||||
|
const { authLimiter } = require('./middlewares/rateLimiter');
|
||||||
|
const routes = require('./routes/v1');
|
||||||
|
const { errorConverter, errorHandler } = require('./middlewares/error');
|
||||||
|
const ApiError = require('./utils/ApiError');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
if (config.env !== 'test') {
|
||||||
|
app.use(morgan.successHandler);
|
||||||
|
app.use(morgan.errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware to parse JSON bodies
|
||||||
|
// app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
// set security HTTP headers
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
// parse json request body
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// parse urlencoded request body
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// sanitize request data
|
||||||
|
app.use(xss());
|
||||||
|
app.use(mongoSanitize());
|
||||||
|
|
||||||
|
// gzip compression
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
// enable cors
|
||||||
|
app.use(cors());
|
||||||
|
app.options('*', cors());
|
||||||
|
|
||||||
|
//# Need to implement
|
||||||
|
// const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS.split(',');
|
||||||
|
// const corsOptions = {
|
||||||
|
// origin: function (origin, callback) {
|
||||||
|
// // Allow requests with no origin (like mobile apps, curl requests)
|
||||||
|
// if (!origin) return callback(null, true);
|
||||||
|
// if (allowedOrigins.indexOf(origin) !== -1) {
|
||||||
|
// callback(null, true);
|
||||||
|
// } else {
|
||||||
|
// callback(new Error('Not allowed by CORS'));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// app.use(cors(corsOptions));
|
||||||
|
// app.use(cors());
|
||||||
|
|
||||||
|
// jwt authentication
|
||||||
|
app.use(passport.initialize());
|
||||||
|
passport.use('jwt', jwtStrategy);
|
||||||
|
|
||||||
|
// limit repeated failed requests to auth endpoints
|
||||||
|
if (config.env === 'production') {
|
||||||
|
app.use('/v1/auth', authLimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1 api routes
|
||||||
|
app.use('/', routes);
|
||||||
|
|
||||||
|
// send back a 404 error for any unknown api request
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
next(new ApiError(httpStatus.NOT_FOUND, 'Not found'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// convert error to ApiError, if needed
|
||||||
|
app.use(errorConverter);
|
||||||
|
|
||||||
|
// handle error
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
module.exports = app;
|
|
@ -0,0 +1,64 @@
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
const path = require('path');
|
||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
||||||
|
|
||||||
|
const envVarsSchema = Joi.object()
|
||||||
|
.keys({
|
||||||
|
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
|
||||||
|
PORT: Joi.number().default(3000),
|
||||||
|
MONGODB_URL: Joi.string().required().description('Mongo DB url'),
|
||||||
|
JWT_SECRET: Joi.string().required().description('JWT secret key'),
|
||||||
|
JWT_ACCESS_EXPIRATION_MINUTES: Joi.number().default(30).description('minutes after which access tokens expire'),
|
||||||
|
JWT_REFRESH_EXPIRATION_DAYS: Joi.number().default(30).description('days after which refresh tokens expire'),
|
||||||
|
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: Joi.number()
|
||||||
|
.default(10)
|
||||||
|
.description('minutes after which reset password token expires'),
|
||||||
|
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()
|
||||||
|
.default(10)
|
||||||
|
.description('minutes after which verify email token expires'),
|
||||||
|
SMTP_HOST: Joi.string().description('server that will send the emails'),
|
||||||
|
SMTP_PORT: Joi.number().description('port to connect to the email server'),
|
||||||
|
SMTP_USERNAME: Joi.string().description('username for email server'),
|
||||||
|
SMTP_PASSWORD: Joi.string().description('password for email server'),
|
||||||
|
EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),
|
||||||
|
})
|
||||||
|
.unknown();
|
||||||
|
|
||||||
|
const { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(process.env);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Config validation error: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: envVars.NODE_ENV,
|
||||||
|
port: envVars.PORT,
|
||||||
|
mongoose: {
|
||||||
|
url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
|
||||||
|
options: {
|
||||||
|
useCreateIndex: true,
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
secret: envVars.JWT_SECRET,
|
||||||
|
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
|
||||||
|
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
|
||||||
|
resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
|
||||||
|
verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
smtp: {
|
||||||
|
host: envVars.SMTP_HOST,
|
||||||
|
port: envVars.SMTP_PORT,
|
||||||
|
auth: {
|
||||||
|
user: envVars.SMTP_USERNAME,
|
||||||
|
pass: envVars.SMTP_PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
from: envVars.EMAIL_FROM,
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
const winston = require('winston');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
const enumerateErrorFormat = winston.format((info) => {
|
||||||
|
if (info instanceof Error) {
|
||||||
|
Object.assign(info, { message: info.stack });
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: config.env === 'development' ? 'debug' : 'info',
|
||||||
|
format: winston.format.combine(
|
||||||
|
enumerateErrorFormat(),
|
||||||
|
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
|
||||||
|
winston.format.splat(),
|
||||||
|
winston.format.printf(({ level, message }) => `${level}: ${message}`)
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
stderrLevels: ['error'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = logger;
|
|
@ -0,0 +1,24 @@
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const config = require('./config');
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
morgan.token('message', (req, res) => res.locals.errorMessage || '');
|
||||||
|
|
||||||
|
const getIpFormat = () => (config.env === 'production' ? ':remote-addr - ' : '');
|
||||||
|
const successResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms`;
|
||||||
|
const errorResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms - message: :message`;
|
||||||
|
|
||||||
|
const successHandler = morgan(successResponseFormat, {
|
||||||
|
skip: (req, res) => res.statusCode >= 400,
|
||||||
|
stream: { write: (message) => logger.info(message.trim()) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorHandler = morgan(errorResponseFormat, {
|
||||||
|
skip: (req, res) => res.statusCode < 400,
|
||||||
|
stream: { write: (message) => logger.error(message.trim()) },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
successHandler,
|
||||||
|
errorHandler,
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
||||||
|
const config = require('./config');
|
||||||
|
const { tokenTypes } = require('./tokens');
|
||||||
|
const { User } = require('../models');
|
||||||
|
|
||||||
|
const jwtOptions = {
|
||||||
|
secretOrKey: config.jwt.secret,
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwtVerify = async (payload, done) => {
|
||||||
|
try {
|
||||||
|
if (payload.type !== tokenTypes.ACCESS) {
|
||||||
|
throw new Error('Invalid token type');
|
||||||
|
}
|
||||||
|
const user = await User.findById(payload.sub);
|
||||||
|
if (!user) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
done(null, user);
|
||||||
|
} catch (error) {
|
||||||
|
done(error, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
jwtStrategy,
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
const allRoles = {
|
||||||
|
user: [],
|
||||||
|
admin: ['getUsers', 'manageUsers'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const roles = Object.keys(allRoles);
|
||||||
|
const roleRights = new Map(Object.entries(allRoles));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
roles,
|
||||||
|
roleRights,
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
const tokenTypes = {
|
||||||
|
ACCESS: 'access',
|
||||||
|
REFRESH: 'refresh',
|
||||||
|
RESET_PASSWORD: 'resetPassword',
|
||||||
|
VERIFY_EMAIL: 'verifyEmail',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tokenTypes,
|
||||||
|
};
|
|
@ -0,0 +1,59 @@
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const catchAsync = require('../utils/catchAsync');
|
||||||
|
const { authService, userService, tokenService, emailService } = require('../services');
|
||||||
|
|
||||||
|
const register = catchAsync(async (req, res) => {
|
||||||
|
const user = await userService.createUser(req.body);
|
||||||
|
const tokens = await tokenService.generateAuthTokens(user);
|
||||||
|
res.status(httpStatus.CREATED).send({ user, tokens });
|
||||||
|
});
|
||||||
|
|
||||||
|
const login = catchAsync(async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
const user = await authService.loginUserWithEmailAndPassword(email, password);
|
||||||
|
const tokens = await tokenService.generateAuthTokens(user);
|
||||||
|
res.send({ user, tokens });
|
||||||
|
});
|
||||||
|
|
||||||
|
const logout = catchAsync(async (req, res) => {
|
||||||
|
await authService.logout(req.body.refreshToken);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshTokens = catchAsync(async (req, res) => {
|
||||||
|
const tokens = await authService.refreshAuth(req.body.refreshToken);
|
||||||
|
res.send({ ...tokens });
|
||||||
|
});
|
||||||
|
|
||||||
|
const forgotPassword = catchAsync(async (req, res) => {
|
||||||
|
const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email);
|
||||||
|
await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetPassword = catchAsync(async (req, res) => {
|
||||||
|
await authService.resetPassword(req.query.token, req.body.password);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendVerificationEmail = catchAsync(async (req, res) => {
|
||||||
|
const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user);
|
||||||
|
await emailService.sendVerificationEmail(req.user.email, verifyEmailToken);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyEmail = catchAsync(async (req, res) => {
|
||||||
|
await authService.verifyEmail(req.query.token);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshTokens,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
sendVerificationEmail,
|
||||||
|
verifyEmail,
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports.authController = require('./auth.controller');
|
||||||
|
module.exports.userController = require('./user.controller');
|
|
@ -0,0 +1,43 @@
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const pick = require('../utils/pick');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
const catchAsync = require('../utils/catchAsync');
|
||||||
|
const { userService } = require('../services');
|
||||||
|
|
||||||
|
const createUser = catchAsync(async (req, res) => {
|
||||||
|
const user = await userService.createUser(req.body);
|
||||||
|
res.status(httpStatus.CREATED).send(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUsers = catchAsync(async (req, res) => {
|
||||||
|
const filter = pick(req.query, ['name', 'role']);
|
||||||
|
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
||||||
|
const result = await userService.queryUsers(filter, options);
|
||||||
|
res.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUser = catchAsync(async (req, res) => {
|
||||||
|
const user = await userService.getUserById(req.params.userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
||||||
|
}
|
||||||
|
res.send(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateUser = catchAsync(async (req, res) => {
|
||||||
|
const user = await userService.updateUserById(req.params.userId, req.body);
|
||||||
|
res.send(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteUser = catchAsync(async (req, res) => {
|
||||||
|
await userService.deleteUserById(req.params.userId);
|
||||||
|
res.status(httpStatus.NO_CONTENT).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createUser,
|
||||||
|
getUsers,
|
||||||
|
getUser,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
};
|
|
@ -0,0 +1,92 @@
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
enum: [user, admin]
|
||||||
|
example:
|
||||||
|
id: 5ebac534954b54139806c112
|
||||||
|
email: fake@example.com
|
||||||
|
name: fake name
|
||||||
|
role: user
|
||||||
|
|
||||||
|
Token:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
expires:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
example:
|
||||||
|
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg
|
||||||
|
expires: 2020-05-12T16:18:04.793Z
|
||||||
|
|
||||||
|
AuthTokens:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
access:
|
||||||
|
$ref: '#/components/schemas/Token'
|
||||||
|
refresh:
|
||||||
|
$ref: '#/components/schemas/Token'
|
||||||
|
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: number
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
responses:
|
||||||
|
DuplicateEmail:
|
||||||
|
description: Email already taken
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
example:
|
||||||
|
code: 400
|
||||||
|
message: Email already taken
|
||||||
|
Unauthorized:
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
example:
|
||||||
|
code: 401
|
||||||
|
message: Please authenticate
|
||||||
|
Forbidden:
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
example:
|
||||||
|
code: 403
|
||||||
|
message: Forbidden
|
||||||
|
NotFound:
|
||||||
|
description: Not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
example:
|
||||||
|
code: 404
|
||||||
|
message: Not found
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
|
@ -0,0 +1,21 @@
|
||||||
|
const { version } = require('../../package.json');
|
||||||
|
const config = require('../config/config');
|
||||||
|
|
||||||
|
const swaggerDef = {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'node-express-boilerplate API documentation',
|
||||||
|
version,
|
||||||
|
license: {
|
||||||
|
name: 'MIT',
|
||||||
|
url: 'https://github.com/hagopj13/node-express-boilerplate/blob/master/LICENSE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `http://localhost:${config.port}/v1`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = swaggerDef;
|
|
@ -0,0 +1,5 @@
|
||||||
|
const homeHandler = (req, res) => {
|
||||||
|
res.send("Pong");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default homeHandler;
|
|
@ -0,0 +1,38 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const app = require('./app');
|
||||||
|
const config = require('./config/config');
|
||||||
|
const logger = require('./config/logger');
|
||||||
|
|
||||||
|
let server;
|
||||||
|
mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {
|
||||||
|
logger.info('Connected to MongoDB');
|
||||||
|
server = app.listen(config.port, () => {
|
||||||
|
logger.info(`Listening to port ${config.port}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitHandler = () => {
|
||||||
|
if (server) {
|
||||||
|
server.close(() => {
|
||||||
|
logger.info('Server closed');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unexpectedErrorHandler = (error) => {
|
||||||
|
logger.error(error);
|
||||||
|
exitHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('uncaughtException', unexpectedErrorHandler);
|
||||||
|
process.on('unhandledRejection', unexpectedErrorHandler);
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
logger.info('SIGTERM received');
|
||||||
|
if (server) {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
const passport = require('passport');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
const { roleRights } = require('../config/roles');
|
||||||
|
|
||||||
|
const verifyCallback = (req, resolve, reject, requiredRights) => async (err, user, info) => {
|
||||||
|
if (err || info || !user) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||||
|
}
|
||||||
|
req.user = user;
|
||||||
|
|
||||||
|
if (requiredRights.length) {
|
||||||
|
const userRights = roleRights.get(user.role);
|
||||||
|
const hasRequiredRights = requiredRights.every((requiredRight) => userRights.includes(requiredRight));
|
||||||
|
if (!hasRequiredRights && req.params.userId !== user.id) {
|
||||||
|
return reject(new ApiError(httpStatus.FORBIDDEN, 'Forbidden'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const auth = (...requiredRights) => async (req, res, next) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
passport.authenticate('jwt', { session: false }, verifyCallback(req, resolve, reject, requiredRights))(req, res, next);
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = auth;
|
|
@ -0,0 +1,44 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const config = require('../config/config');
|
||||||
|
const logger = require('../config/logger');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
|
||||||
|
const errorConverter = (err, req, res, next) => {
|
||||||
|
let error = err;
|
||||||
|
if (!(error instanceof ApiError)) {
|
||||||
|
const statusCode =
|
||||||
|
error.statusCode || error instanceof mongoose.Error ? httpStatus.BAD_REQUEST : httpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
const message = error.message || httpStatus[statusCode];
|
||||||
|
error = new ApiError(statusCode, message, false, err.stack);
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const errorHandler = (err, req, res, next) => {
|
||||||
|
let { statusCode, message } = err;
|
||||||
|
if (config.env === 'production' && !err.isOperational) {
|
||||||
|
statusCode = httpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
message = httpStatus[httpStatus.INTERNAL_SERVER_ERROR];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.errorMessage = err.message;
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
code: statusCode,
|
||||||
|
message,
|
||||||
|
...(config.env === 'development' && { stack: err.stack }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.env === 'development') {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
errorConverter,
|
||||||
|
errorHandler,
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 20,
|
||||||
|
skipSuccessfulRequests: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
authLimiter,
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
const Joi = require('joi');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const pick = require('../utils/pick');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
|
||||||
|
const validate = (schema) => (req, res, next) => {
|
||||||
|
const validSchema = pick(schema, ['params', 'query', 'body']);
|
||||||
|
const object = pick(req, Object.keys(validSchema));
|
||||||
|
const { value, error } = Joi.compile(validSchema)
|
||||||
|
.prefs({ errors: { label: 'key' }, abortEarly: false })
|
||||||
|
.validate(object);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errorMessage = error.details.map((details) => details.message).join(', ');
|
||||||
|
return next(new ApiError(httpStatus.BAD_REQUEST, errorMessage));
|
||||||
|
}
|
||||||
|
Object.assign(req, value);
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = validate;
|
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports.Token = require('./token.model');
|
||||||
|
module.exports.User = require('./user.model');
|
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports.toJSON = require('./toJSON.plugin');
|
||||||
|
module.exports.paginate = require('./paginate.plugin');
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
|
||||||
|
const paginate = (schema) => {
|
||||||
|
/**
|
||||||
|
* @typedef {Object} QueryResult
|
||||||
|
* @property {Document[]} results - Results found
|
||||||
|
* @property {number} page - Current page
|
||||||
|
* @property {number} limit - Maximum number of results per page
|
||||||
|
* @property {number} totalPages - Total number of pages
|
||||||
|
* @property {number} totalResults - Total number of documents
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Query for documents with pagination
|
||||||
|
* @param {Object} [filter] - Mongo filter
|
||||||
|
* @param {Object} [options] - Query options
|
||||||
|
* @param {string} [options.sortBy] - Sorting criteria using the format: sortField:(desc|asc). Multiple sorting criteria should be separated by commas (,)
|
||||||
|
* @param {string} [options.populate] - Populate data fields. Hierarchy of fields should be separated by (.). Multiple populating criteria should be separated by commas (,)
|
||||||
|
* @param {number} [options.limit] - Maximum number of results per page (default = 10)
|
||||||
|
* @param {number} [options.page] - Current page (default = 1)
|
||||||
|
* @returns {Promise<QueryResult>}
|
||||||
|
*/
|
||||||
|
schema.statics.paginate = async function (filter, options) {
|
||||||
|
let sort = '';
|
||||||
|
if (options.sortBy) {
|
||||||
|
const sortingCriteria = [];
|
||||||
|
options.sortBy.split(',').forEach((sortOption) => {
|
||||||
|
const [key, order] = sortOption.split(':');
|
||||||
|
sortingCriteria.push((order === 'desc' ? '-' : '') + key);
|
||||||
|
});
|
||||||
|
sort = sortingCriteria.join(' ');
|
||||||
|
} else {
|
||||||
|
sort = 'createdAt';
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10;
|
||||||
|
const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const countPromise = this.countDocuments(filter).exec();
|
||||||
|
let docsPromise = this.find(filter).sort(sort).skip(skip).limit(limit);
|
||||||
|
|
||||||
|
if (options.populate) {
|
||||||
|
options.populate.split(',').forEach((populateOption) => {
|
||||||
|
docsPromise = docsPromise.populate(
|
||||||
|
populateOption
|
||||||
|
.split('.')
|
||||||
|
.reverse()
|
||||||
|
.reduce((a, b) => ({ path: b, populate: a }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
docsPromise = docsPromise.exec();
|
||||||
|
|
||||||
|
return Promise.all([countPromise, docsPromise]).then((values) => {
|
||||||
|
const [totalResults, results] = values;
|
||||||
|
const totalPages = Math.ceil(totalResults / limit);
|
||||||
|
const result = {
|
||||||
|
results,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
totalResults,
|
||||||
|
};
|
||||||
|
return Promise.resolve(result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = paginate;
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mongoose schema plugin which applies the following in the toJSON transform call:
|
||||||
|
* - removes __v, createdAt, updatedAt, and any path that has private: true
|
||||||
|
* - replaces _id with id
|
||||||
|
*/
|
||||||
|
|
||||||
|
const deleteAtPath = (obj, path, index) => {
|
||||||
|
if (index === path.length - 1) {
|
||||||
|
delete obj[path[index]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteAtPath(obj[path[index]], path, index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toJSON = (schema) => {
|
||||||
|
let transform;
|
||||||
|
if (schema.options.toJSON && schema.options.toJSON.transform) {
|
||||||
|
transform = schema.options.toJSON.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
|
||||||
|
transform(doc, ret, options) {
|
||||||
|
Object.keys(schema.paths).forEach((path) => {
|
||||||
|
if (schema.paths[path].options && schema.paths[path].options.private) {
|
||||||
|
deleteAtPath(ret, path.split('.'), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ret.id = ret._id.toString();
|
||||||
|
delete ret._id;
|
||||||
|
delete ret.__v;
|
||||||
|
delete ret.createdAt;
|
||||||
|
delete ret.updatedAt;
|
||||||
|
if (transform) {
|
||||||
|
return transform(doc, ret, options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = toJSON;
|
|
@ -0,0 +1,44 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { toJSON } = require('./plugins');
|
||||||
|
const { tokenTypes } = require('../config/tokens');
|
||||||
|
|
||||||
|
const tokenSchema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
token: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: mongoose.SchemaTypes.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFY_EMAIL],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
expires: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
blacklisted: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// add plugin that converts mongoose to json
|
||||||
|
tokenSchema.plugin(toJSON);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Token
|
||||||
|
*/
|
||||||
|
const Token = mongoose.model('Token', tokenSchema);
|
||||||
|
|
||||||
|
module.exports = Token;
|
|
@ -0,0 +1,91 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const validator = require('validator');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { toJSON, paginate } = require('./plugins');
|
||||||
|
const { roles } = require('../config/roles');
|
||||||
|
|
||||||
|
const userSchema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
trim: true,
|
||||||
|
lowercase: true,
|
||||||
|
validate(value) {
|
||||||
|
if (!validator.isEmail(value)) {
|
||||||
|
throw new Error('Invalid email');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
minlength: 8,
|
||||||
|
validate(value) {
|
||||||
|
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
|
||||||
|
throw new Error('Password must contain at least one letter and one number');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
private: true, // used by the toJSON plugin
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: roles,
|
||||||
|
default: 'user',
|
||||||
|
},
|
||||||
|
isEmailVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// add plugin that converts mongoose to json
|
||||||
|
userSchema.plugin(toJSON);
|
||||||
|
userSchema.plugin(paginate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email is taken
|
||||||
|
* @param {string} email - The user's email
|
||||||
|
* @param {ObjectId} [excludeUserId] - The id of the user to be excluded
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
userSchema.statics.isEmailTaken = async function (email, excludeUserId) {
|
||||||
|
const user = await this.findOne({ email, _id: { $ne: excludeUserId } });
|
||||||
|
return !!user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if password matches the user's password
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
userSchema.methods.isPasswordMatch = async function (password) {
|
||||||
|
const user = this;
|
||||||
|
return bcrypt.compare(password, user.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
userSchema.pre('save', async function (next) {
|
||||||
|
const user = this;
|
||||||
|
if (user.isModified('password')) {
|
||||||
|
user.password = await bcrypt.hash(user.password, 8);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef User
|
||||||
|
*/
|
||||||
|
const User = mongoose.model('User', userSchema);
|
||||||
|
|
||||||
|
module.exports = User;
|
|
@ -0,0 +1,17 @@
|
||||||
|
const apiTest = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const responseObject = {
|
||||||
|
message: 'Hello, this is your JSON response!',
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: 'John Doe',
|
||||||
|
age: 30,
|
||||||
|
job: 'Developer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res.json(responseObject);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = apiTest
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
const classMates = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
let classmatesData = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Daniel Nguyen",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Sarah Anderson",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "John Smith",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar3.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "Emily Davis",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar4.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
name: "Michael Johnson",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar5.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
name: "Jessica Wilson",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar6.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
name: "David Brown",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
name: "Laura Lee",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
name: "Chris Miller",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar3.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
name: "Sophia Taylor",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar4.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
name: "James Anderson",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar5.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
name: "Olivia Thomas",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar6.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
name: "Ethan Martinez",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "14",
|
||||||
|
name: "Ava Garcia",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "15",
|
||||||
|
name: "Noah Rodriguez",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar3.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16",
|
||||||
|
name: "Mia Martinez",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar4.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "17",
|
||||||
|
name: "Lucas Wilson",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar5.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "18",
|
||||||
|
name: "Isabella Clark",
|
||||||
|
program: "Undergraduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar6.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "19",
|
||||||
|
name: "Liam Walker",
|
||||||
|
program: "Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "20",
|
||||||
|
name: "Charlotte Lewis",
|
||||||
|
program: "Post-Graduate Program",
|
||||||
|
type: "Student",
|
||||||
|
avatar: "/assets/avatar2.png"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
res.json(classmatesData);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = classMates;
|
|
@ -0,0 +1,70 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const continueLearning = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM continue_learning`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = continueLearning;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let courseData = [
|
||||||
|
// {
|
||||||
|
// id : "1",
|
||||||
|
// title : "Life History of Dr. Maria Montessori",
|
||||||
|
// chapter : "1",
|
||||||
|
// Program : "Graduate Program",
|
||||||
|
// img : "/assets/course1.jpg"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id : "2",
|
||||||
|
// title : "Introduction to Montessori Methods",
|
||||||
|
// chapter : "2",
|
||||||
|
// Program : "Graduate Program",
|
||||||
|
// img : "/assets/course2.jpg"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id : "3",
|
||||||
|
// title : "Exercises on Practical Life",
|
||||||
|
// chapter : "3",
|
||||||
|
// Program : "Graduate Program",
|
||||||
|
// img : "/assets/course3.jpg"
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// res.json(courseData);
|
|
@ -0,0 +1,35 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const generateQuestions = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const { questions } = req.body;
|
||||||
|
res.send( questions );
|
||||||
|
questions.forEach(question => {
|
||||||
|
const { questionText, options, correctAnswer } = question;
|
||||||
|
const sql = 'INSERT INTO quiz_questions (questionText, option1, option2, option3, option4, correctAnswer) VALUES (?, ?, ?, ?, ?, ?)';
|
||||||
|
db.query(sql, [questionText, options[0], options[1], options[2], options[3], correctAnswer], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting question:', err);
|
||||||
|
res.status(500).send('Error inserting question');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send('Questions added successfully');
|
||||||
|
}
|
||||||
|
module.exports = generateQuestions;
|
|
@ -0,0 +1,32 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const getGameScore = (req, res) => {
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: 'beanstalk_game',
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0
|
||||||
|
});
|
||||||
|
const promisePool = pool.promise();
|
||||||
|
const { userId, gameName, gameID } = req.body;
|
||||||
|
|
||||||
|
const query = 'SELECT score, gameTime FROM gameData WHERE userId = ? AND gameName = ? AND gameID = ?';
|
||||||
|
|
||||||
|
pool.query(query, [userId, gameName, gameID], (error, results) => {
|
||||||
|
if (error) {
|
||||||
|
return res.status(500).json({ message: 'Database query failed', error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
const game = results[0];
|
||||||
|
res.json({ score: game.score, gameTime: game.gameTime });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: 'Game data not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getGameScore;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const knowledgeQuests = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM knowledge_quests WHERE status = 1`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = knowledgeQuests;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let knowledgeData = [
|
||||||
|
// {
|
||||||
|
// id: "1",
|
||||||
|
// status: "1",
|
||||||
|
// title: "Assessment on Special Education",
|
||||||
|
// challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
// question: "Subjective Question",
|
||||||
|
// img: "/assets/knowledge1.jpg"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "2",
|
||||||
|
// status: "1",
|
||||||
|
// title: "Quiz on Children Psychology",
|
||||||
|
// challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
// question: "MCQ",
|
||||||
|
// img: "/assets/knowledge2.jpg"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "3",
|
||||||
|
// status: "1",
|
||||||
|
// title: "Quiz on Montessori Methods",
|
||||||
|
// challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
// question: "MCQ",
|
||||||
|
// img: "/assets/knowledge3.jpg"
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// res.json(knowledgeData);
|
|
@ -0,0 +1,180 @@
|
||||||
|
|
||||||
|
const knowledgeQuestsCompleted = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
let knowledgeCompleted = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
status: "1",
|
||||||
|
title: "Assessment on Special Education",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "Subjective Question",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Children Psychology",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Montessori Methods",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
status: "1",
|
||||||
|
title: "Assessment on Special Education",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "Subjective Question",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Children Psychology",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Montessori Methods",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on Child Development",
|
||||||
|
challenge: "Expand your knowledge & earn badges.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Educational Psychology",
|
||||||
|
challenge: "Join & enhance your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Inclusive Education",
|
||||||
|
challenge: "Participate & gain insights.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
status: "1",
|
||||||
|
title: "Course on Early Childhood Education",
|
||||||
|
challenge: "Complete the course & get certified.",
|
||||||
|
question: "Multiple Modules",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
status: "1",
|
||||||
|
title: "Training on Classroom Management",
|
||||||
|
challenge: "Improve your teaching strategies.",
|
||||||
|
question: "Practical Tasks",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
status: "1",
|
||||||
|
title: "Lecture on Cognitive Development",
|
||||||
|
challenge: "Expand your understanding & get certified.",
|
||||||
|
question: "Q&A Session",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on Behavioral Issues",
|
||||||
|
challenge: "Join & learn from experts.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "14",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Learning Disabilities",
|
||||||
|
challenge: "Participate & enhance your knowledge.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "15",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Child Psychology",
|
||||||
|
challenge: "Join & expand your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16",
|
||||||
|
status: "1",
|
||||||
|
title: "Course on Special Education Needs",
|
||||||
|
challenge: "Complete the course & get certified.",
|
||||||
|
question: "Multiple Modules",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "17",
|
||||||
|
status: "1",
|
||||||
|
title: "Training on Autism Spectrum Disorder",
|
||||||
|
challenge: "Improve your teaching strategies.",
|
||||||
|
question: "Practical Tasks",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "18",
|
||||||
|
status: "1",
|
||||||
|
title: "Lecture on Emotional Development",
|
||||||
|
challenge: "Expand your understanding & get certified.",
|
||||||
|
question: "Q&A Session",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "19",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on ADHD",
|
||||||
|
challenge: "Join & learn from experts.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "20",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Speech and Language Disorders",
|
||||||
|
challenge: "Participate & enhance your knowledge.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "21",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Child Nutrition",
|
||||||
|
challenge: "Join & expand your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
res.json(knowledgeCompleted);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = knowledgeQuestsCompleted;
|
||||||
|
|
||||||
|
// knowledgeQuestsAllContent
|
|
@ -0,0 +1,180 @@
|
||||||
|
|
||||||
|
const knowledgeQuestsAllContent = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
let knowledgeData = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
status: "1",
|
||||||
|
title: "Assessment on Special Education",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "Subjective Question",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Children Psychology",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Montessori Methods",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
status: "1",
|
||||||
|
title: "Assessment on Special Education",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "Subjective Question",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Children Psychology",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
status: "1",
|
||||||
|
title: "Quiz on Montessori Methods",
|
||||||
|
challenge: "Challenge yourself & climb the leaderboard.",
|
||||||
|
question: "MCQ",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on Child Development",
|
||||||
|
challenge: "Expand your knowledge & earn badges.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Educational Psychology",
|
||||||
|
challenge: "Join & enhance your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Inclusive Education",
|
||||||
|
challenge: "Participate & gain insights.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
status: "1",
|
||||||
|
title: "Course on Early Childhood Education",
|
||||||
|
challenge: "Complete the course & get certified.",
|
||||||
|
question: "Multiple Modules",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
status: "1",
|
||||||
|
title: "Training on Classroom Management",
|
||||||
|
challenge: "Improve your teaching strategies.",
|
||||||
|
question: "Practical Tasks",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
status: "1",
|
||||||
|
title: "Lecture on Cognitive Development",
|
||||||
|
challenge: "Expand your understanding & get certified.",
|
||||||
|
question: "Q&A Session",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on Behavioral Issues",
|
||||||
|
challenge: "Join & learn from experts.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "14",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Learning Disabilities",
|
||||||
|
challenge: "Participate & enhance your knowledge.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "15",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Child Psychology",
|
||||||
|
challenge: "Join & expand your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16",
|
||||||
|
status: "1",
|
||||||
|
title: "Course on Special Education Needs",
|
||||||
|
challenge: "Complete the course & get certified.",
|
||||||
|
question: "Multiple Modules",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "17",
|
||||||
|
status: "1",
|
||||||
|
title: "Training on Autism Spectrum Disorder",
|
||||||
|
challenge: "Improve your teaching strategies.",
|
||||||
|
question: "Practical Tasks",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "18",
|
||||||
|
status: "1",
|
||||||
|
title: "Lecture on Emotional Development",
|
||||||
|
challenge: "Expand your understanding & get certified.",
|
||||||
|
question: "Q&A Session",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "19",
|
||||||
|
status: "1",
|
||||||
|
title: "Workshop on ADHD",
|
||||||
|
challenge: "Join & learn from experts.",
|
||||||
|
question: "Interactive Session",
|
||||||
|
img: "/assets/knowledge1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "20",
|
||||||
|
status: "1",
|
||||||
|
title: "Seminar on Speech and Language Disorders",
|
||||||
|
challenge: "Participate & enhance your knowledge.",
|
||||||
|
question: "Lecture",
|
||||||
|
img: "/assets/knowledge2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "21",
|
||||||
|
status: "1",
|
||||||
|
title: "Webinar on Child Nutrition",
|
||||||
|
challenge: "Join & expand your skills.",
|
||||||
|
question: "Discussion",
|
||||||
|
img: "/assets/knowledge3.jpg"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
res.json(knowledgeData);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = knowledgeQuestsAllContent;
|
||||||
|
|
||||||
|
// knowledgeQuestsAllContent
|
|
@ -0,0 +1,34 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const newQuestion = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `INSERT INTO quiz_questions (questionText, option1, option2, option3, option4, correctAnswer, moduleId) VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
const values = [data.question, data.option1, data.option2, data.option3, data.option4, data.correctAnswer, data.moduleId];
|
||||||
|
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = newQuestion
|
||||||
|
;
|
|
@ -0,0 +1,34 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const newQuiz = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `INSERT INTO quiz (moduleName, type) VALUES (?, ?)`;
|
||||||
|
const values = [data.moduleName, data.moduleType];
|
||||||
|
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = newQuiz
|
||||||
|
;
|
|
@ -0,0 +1,182 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const questionList = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
function generateUniqueId() {
|
||||||
|
let timestamp = new Date().getTime().toString();
|
||||||
|
let randomNumber = Math.floor(Math.random() * 100000).toString();
|
||||||
|
let uniqueId = timestamp + randomNumber;
|
||||||
|
|
||||||
|
if (uniqueId.length > 10) {
|
||||||
|
uniqueId = uniqueId.substring(0, 10);
|
||||||
|
} else if (uniqueId.length < 10) {
|
||||||
|
while (uniqueId.length < 10) {
|
||||||
|
uniqueId += Math.floor(Math.random() * 10).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM quiz_questions LIMIT 6`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uniqueId = generateUniqueId();
|
||||||
|
results.forEach(obj => {
|
||||||
|
obj.newQuizId = uniqueId
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = questionList;
|
||||||
|
// {
|
||||||
|
// id: 1,
|
||||||
|
// question: "What is the capital of France?",
|
||||||
|
// options: ["Berlin", "Madrid", "Paris", "Rome"],
|
||||||
|
// answer: "Paris",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 2,
|
||||||
|
// question: "Which planet is known as the Red Planet?",
|
||||||
|
// options: ["Earth", "Mars", "Jupiter", "Saturn"],
|
||||||
|
// answer: "Mars",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 3,
|
||||||
|
// question: "What is the chemical symbol for gold?",
|
||||||
|
// options: ["Au", "Ag", "Pb", "Fe"],
|
||||||
|
// answer: "Au",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 4,
|
||||||
|
// question: "Who wrote 'To Kill a Mockingbird'?",
|
||||||
|
// options: ["Harper Lee", "Mark Twain", "Ernest Hemingway", "J.K. Rowling"],
|
||||||
|
// answer: "Harper Lee",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 5,
|
||||||
|
// question: "What is the largest ocean on Earth?",
|
||||||
|
// options: ["Atlantic Ocean", "Indian Ocean", "Arctic Ocean", "Pacific Ocean"],
|
||||||
|
// answer: "Pacific Ocean",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 6,
|
||||||
|
// question: "Which element has the atomic number 1?",
|
||||||
|
// options: ["Helium", "Hydrogen", "Oxygen", "Carbon"],
|
||||||
|
// answer: "Hydrogen",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 7,
|
||||||
|
// question: "In which year did the Titanic sink?",
|
||||||
|
// options: ["1912", "1905", "1898", "1923"],
|
||||||
|
// answer: "1912",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 8,
|
||||||
|
// question: "Who is the author of '1984'?",
|
||||||
|
// options: ["George Orwell", "Aldous Huxley", "Ray Bradbury", "J.D. Salinger"],
|
||||||
|
// answer: "George Orwell",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 9,
|
||||||
|
// question: "What is the hardest natural substance on Earth?",
|
||||||
|
// options: ["Gold", "Platinum", "Diamond", "Iron"],
|
||||||
|
// answer: "Diamond",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 10,
|
||||||
|
// question: "What is the largest planet in our solar system?",
|
||||||
|
// options: ["Earth", "Saturn", "Neptune", "Jupiter"],
|
||||||
|
// answer: "Jupiter",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 11,
|
||||||
|
// question: "What is the main ingredient in guacamole?",
|
||||||
|
// options: ["Tomato", "Avocado", "Pepper", "Onion"],
|
||||||
|
// answer: "Avocado",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 12,
|
||||||
|
// question: "Which country is known as the Land of the Rising Sun?",
|
||||||
|
// options: ["China", "Japan", "Thailand", "South Korea"],
|
||||||
|
// answer: "Japan",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 13,
|
||||||
|
// question: "What is the smallest prime number?",
|
||||||
|
// options: ["1", "2", "3", "5"],
|
||||||
|
// answer: "2",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 14,
|
||||||
|
// question: "Who painted the Mona Lisa?",
|
||||||
|
// options: ["Vincent van Gogh", "Leonardo da Vinci", "Pablo Picasso", "Claude Monet"],
|
||||||
|
// answer: "Leonardo da Vinci",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 15,
|
||||||
|
// question: "What is the capital city of Australia?",
|
||||||
|
// options: ["Sydney", "Melbourne", "Canberra", "Brisbane"],
|
||||||
|
// answer: "Canberra",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 16,
|
||||||
|
// question: "Which gas do plants primarily use for photosynthesis?",
|
||||||
|
// options: ["Oxygen", "Nitrogen", "Carbon Dioxide", "Hydrogen"],
|
||||||
|
// answer: "Carbon Dioxide",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 17,
|
||||||
|
// question: "What is the boiling point of water in Celsius?",
|
||||||
|
// options: ["90°C", "100°C", "110°C", "120°C"],
|
||||||
|
// answer: "100°C",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 18,
|
||||||
|
// question: "Which language is primarily spoken in Brazil?",
|
||||||
|
// options: ["Spanish", "Portuguese", "French", "English"],
|
||||||
|
// answer: "Portuguese",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 19,
|
||||||
|
// question: "What is the smallest unit of life?",
|
||||||
|
// options: ["Tissue", "Organ", "Cell", "Organism"],
|
||||||
|
// answer: "Cell",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 20,
|
||||||
|
// question: "Who developed the theory of relativity?",
|
||||||
|
// options: ["Isaac Newton", "Galileo Galilei", "Albert Einstein", "Niels Bohr"],
|
||||||
|
// answer: "Albert Einstein",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 21,
|
||||||
|
// question: "In what year did World War II end?",
|
||||||
|
// options: ["1945", "1944", "1946", "1943"],
|
||||||
|
// answer: "1945",
|
||||||
|
// },
|
||||||
|
// ];
|
|
@ -0,0 +1,33 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const quizList = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM quiz`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = quizList;
|
|
@ -0,0 +1,419 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const quizModuleData = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
|
||||||
|
|
||||||
|
// const connection = mysql.createConnection({
|
||||||
|
// host: process.env.MARIA_HOST,
|
||||||
|
// user: process.env.MARIA_USER,
|
||||||
|
// password: process.env.MARIA_PASS,
|
||||||
|
// database: process.env.MARIA_DBNM
|
||||||
|
// });
|
||||||
|
|
||||||
|
// connection.connect((err) => {
|
||||||
|
// if(err) {
|
||||||
|
// console.error('Error connecting to the database:', err);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// console.log('Connected to the MariaDB database.');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const data = req.body;
|
||||||
|
|
||||||
|
// const query = `SELECT * FROM quiz_questions`;
|
||||||
|
// connection.query(query, (err, results) => {
|
||||||
|
// if (err) {
|
||||||
|
// console.error('Error inserting data:', err);
|
||||||
|
// res.status(500).send('Internal Server Error');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// res.status(200).json(results);
|
||||||
|
// });
|
||||||
|
|
||||||
|
let quizModuleData = {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
moduleId: 1,
|
||||||
|
type: "Theory Quiz Scores",
|
||||||
|
moduleName: "Module 1 - Life History of Dr. Maria Montessori",
|
||||||
|
quizzes: [
|
||||||
|
{
|
||||||
|
quizId: 1,
|
||||||
|
quizName: "Lorem Ipsum Dolor Sit",
|
||||||
|
attendQuestion: 48,
|
||||||
|
totalQuestion: 50,
|
||||||
|
internalMarks: 28,
|
||||||
|
attendance: 20,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionId: 1,
|
||||||
|
questionText: "What is the capital of France?",
|
||||||
|
options: [
|
||||||
|
"Paris",
|
||||||
|
"London",
|
||||||
|
"Berlin",
|
||||||
|
"Madrid"
|
||||||
|
],
|
||||||
|
correctAnswer: "Paris"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 2,
|
||||||
|
questionText: "What is 2 + 2?",
|
||||||
|
options: [
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6"
|
||||||
|
],
|
||||||
|
correctAnswer: "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 3,
|
||||||
|
questionText: "What is the boiling point of water?",
|
||||||
|
options: [
|
||||||
|
"90°C",
|
||||||
|
"100°C",
|
||||||
|
"110°C",
|
||||||
|
"120°C"
|
||||||
|
],
|
||||||
|
correctAnswer: "100°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 4,
|
||||||
|
questionText: "Who wrote 'To Kill a Mockingbird'?",
|
||||||
|
options: [
|
||||||
|
"Harper Lee",
|
||||||
|
"Mark Twain",
|
||||||
|
"J.K. Rowling",
|
||||||
|
"Ernest Hemingway"
|
||||||
|
],
|
||||||
|
correctAnswer: "Harper Lee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 5,
|
||||||
|
questionText: "What is the largest planet in our solar system?",
|
||||||
|
options: [
|
||||||
|
"Earth",
|
||||||
|
"Mars",
|
||||||
|
"Jupiter",
|
||||||
|
"Saturn"
|
||||||
|
],
|
||||||
|
correctAnswer: "Jupiter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 6,
|
||||||
|
questionText: "What is the speed of light?",
|
||||||
|
options: [
|
||||||
|
"300,000 km/s",
|
||||||
|
"150,000 km/s",
|
||||||
|
"100,000 km/s",
|
||||||
|
"50,000 km/s"
|
||||||
|
],
|
||||||
|
correctAnswer: "300,000 km/s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 7,
|
||||||
|
questionText: "Who painted the Mona Lisa?",
|
||||||
|
options: [
|
||||||
|
"Vincent van Gogh",
|
||||||
|
"Pablo Picasso",
|
||||||
|
"Leonardo da Vinci",
|
||||||
|
"Claude Monet"
|
||||||
|
],
|
||||||
|
correctAnswer: "Leonardo da Vinci"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 8,
|
||||||
|
questionText: "What is the chemical symbol for gold?",
|
||||||
|
options: [
|
||||||
|
"Au",
|
||||||
|
"Ag",
|
||||||
|
"Pt",
|
||||||
|
"Pb"
|
||||||
|
],
|
||||||
|
correctAnswer: "Au"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 9,
|
||||||
|
questionText: "What is the tallest mountain in the world?",
|
||||||
|
options: [
|
||||||
|
"K2",
|
||||||
|
"Kangchenjunga",
|
||||||
|
"Mount Everest",
|
||||||
|
"Lhotse"
|
||||||
|
],
|
||||||
|
correctAnswer: "Mount Everest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 10,
|
||||||
|
questionText: "What is the smallest unit of life?",
|
||||||
|
options: [
|
||||||
|
"Cell",
|
||||||
|
"Atom",
|
||||||
|
"Molecule",
|
||||||
|
"Organ"
|
||||||
|
],
|
||||||
|
correctAnswer: "Cell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quizId: 2,
|
||||||
|
quizName: "Lorem Ipsum Dolor Sit",
|
||||||
|
attendQuestion: 45,
|
||||||
|
totalQuestion: 50,
|
||||||
|
internalMarks: 29,
|
||||||
|
attendance: 20,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionId: 1,
|
||||||
|
questionText: "What is the capital of Italy?",
|
||||||
|
options: [
|
||||||
|
"Rome",
|
||||||
|
"Venice",
|
||||||
|
"Florence",
|
||||||
|
"Milan"
|
||||||
|
],
|
||||||
|
correctAnswer: "Rome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 2,
|
||||||
|
questionText: "What is 3 + 5?",
|
||||||
|
options: [
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
correctAnswer: "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 3,
|
||||||
|
questionText: "What is the freezing point of water?",
|
||||||
|
options: [
|
||||||
|
"0°C",
|
||||||
|
"32°C",
|
||||||
|
"100°C",
|
||||||
|
"273K"
|
||||||
|
],
|
||||||
|
correctAnswer: "0°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 4,
|
||||||
|
questionText: "Who wrote 'Pride and Prejudice'?",
|
||||||
|
options: [
|
||||||
|
"Jane Austen",
|
||||||
|
"Charles Dickens",
|
||||||
|
"Emily Brontë",
|
||||||
|
"George Eliot"
|
||||||
|
],
|
||||||
|
correctAnswer: "Jane Austen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 5,
|
||||||
|
questionText: "What is the smallest planet in our solar system?",
|
||||||
|
options: [
|
||||||
|
"Mercury",
|
||||||
|
"Venus",
|
||||||
|
"Earth",
|
||||||
|
"Mars"
|
||||||
|
],
|
||||||
|
correctAnswer: "Mercury"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 6,
|
||||||
|
questionText: "What is the speed of sound?",
|
||||||
|
options: [
|
||||||
|
"343 m/s",
|
||||||
|
"300 m/s",
|
||||||
|
"1500 m/s",
|
||||||
|
"1000 m/s"
|
||||||
|
],
|
||||||
|
correctAnswer: "343 m/s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 7,
|
||||||
|
questionText: "Who painted the Starry Night?",
|
||||||
|
options: [
|
||||||
|
"Vincent van Gogh",
|
||||||
|
"Pablo Picasso",
|
||||||
|
"Leonardo da Vinci",
|
||||||
|
"Claude Monet"
|
||||||
|
],
|
||||||
|
correctAnswer: "Vincent van Gogh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 8,
|
||||||
|
questionText: "What is the chemical symbol for silver?",
|
||||||
|
options: [
|
||||||
|
"Au",
|
||||||
|
"Ag",
|
||||||
|
"Pt",
|
||||||
|
"Pb"
|
||||||
|
],
|
||||||
|
correctAnswer: "Ag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 9,
|
||||||
|
questionText: "What is the second tallest mountain in the world?",
|
||||||
|
options: [
|
||||||
|
"K2",
|
||||||
|
"Kangchenjunga",
|
||||||
|
"Mount Everest",
|
||||||
|
"Lhotse"
|
||||||
|
],
|
||||||
|
correctAnswer: "K2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 10,
|
||||||
|
questionText: "What is the largest organ in the human body?",
|
||||||
|
options: [
|
||||||
|
"Liver",
|
||||||
|
"Heart",
|
||||||
|
"Skin",
|
||||||
|
"Brain"
|
||||||
|
],
|
||||||
|
correctAnswer: "Skin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
moduleId: 2,
|
||||||
|
type: "Theory Quiz Scores",
|
||||||
|
moduleName: "Module 2",
|
||||||
|
attendQuestion: 42,
|
||||||
|
totalQuestion: 50,
|
||||||
|
internalMarks: 22,
|
||||||
|
attendance: 20,
|
||||||
|
quizzes: [
|
||||||
|
{
|
||||||
|
quizId: 1,
|
||||||
|
quizName: "Quiz 1",
|
||||||
|
attendQuestion: 49,
|
||||||
|
totalQuestion: 50,
|
||||||
|
internalMarks: 2,
|
||||||
|
attendance: 20,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionId: 1,
|
||||||
|
questionText: "What is the capital of Germany?",
|
||||||
|
options: [
|
||||||
|
"Berlin",
|
||||||
|
"Munich",
|
||||||
|
"Hamburg",
|
||||||
|
"Frankfurt"
|
||||||
|
],
|
||||||
|
correctAnswer: "Berlin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 2,
|
||||||
|
questionText: "What is 5 + 3?",
|
||||||
|
options: [
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
correctAnswer: "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 3,
|
||||||
|
questionText: "What is the melting point of ice?",
|
||||||
|
options: [
|
||||||
|
"0°C",
|
||||||
|
"32°C",
|
||||||
|
"100°C",
|
||||||
|
"273K"
|
||||||
|
],
|
||||||
|
correctAnswer: "0°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 4,
|
||||||
|
questionText: "Who wrote '1984'?",
|
||||||
|
options: [
|
||||||
|
"George Orwell",
|
||||||
|
"Aldous Huxley",
|
||||||
|
"Ray Bradbury",
|
||||||
|
"J.D. Salinger"
|
||||||
|
],
|
||||||
|
correctAnswer: "George Orwell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 5,
|
||||||
|
questionText: "What is the second smallest planet in our solar system?",
|
||||||
|
options: [
|
||||||
|
"Mercury",
|
||||||
|
"Venus",
|
||||||
|
"Earth",
|
||||||
|
"Mars"
|
||||||
|
],
|
||||||
|
correctAnswer: "Mars"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 6,
|
||||||
|
questionText: "What is the speed of light in a vacuum?",
|
||||||
|
options: [
|
||||||
|
"300,000 km/s",
|
||||||
|
"150,000 km/s",
|
||||||
|
"299,792 km/s",
|
||||||
|
"299,792 m/s"
|
||||||
|
],
|
||||||
|
correctAnswer: "299,792 km/s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 7,
|
||||||
|
questionText: "Who painted the Last Supper?",
|
||||||
|
options: [
|
||||||
|
"Vincent van Gogh",
|
||||||
|
"Pablo Picasso",
|
||||||
|
"Leonardo da Vinci",
|
||||||
|
"Claude Monet"
|
||||||
|
],
|
||||||
|
correctAnswer: "Leonardo da Vinci"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 8,
|
||||||
|
questionText: "What is the chemical symbol for iron?",
|
||||||
|
options: [
|
||||||
|
"Fe",
|
||||||
|
"Ir",
|
||||||
|
"In",
|
||||||
|
"I"
|
||||||
|
],
|
||||||
|
correctAnswer: "Fe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 9,
|
||||||
|
questionText: "What is the third tallest mountain in the world?",
|
||||||
|
options: [
|
||||||
|
"K2",
|
||||||
|
"Kangchenjunga",
|
||||||
|
"Mount Everest",
|
||||||
|
"Lhotse"
|
||||||
|
],
|
||||||
|
correctAnswer: "Kangchenjunga"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionId: 10,
|
||||||
|
questionText: "What is the smallest bone in the human body?",
|
||||||
|
options: [
|
||||||
|
"Stapes",
|
||||||
|
"Femur",
|
||||||
|
"Tibia",
|
||||||
|
"Fibula"
|
||||||
|
],
|
||||||
|
correctAnswer: "Stapes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res.json(quizModuleData);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = quizModuleData;
|
|
@ -0,0 +1,34 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const quizModuleList = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM quiz_modules WHERE moduleId = ?`;
|
||||||
|
const values = req.query.module_id;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = quizModuleList;
|
|
@ -0,0 +1,34 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const quizModuleList = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
let values = req.query.module_id ? req.query.module_id : '';
|
||||||
|
const query = `SELECT * FROM quiz_modules WHERE moduleId = ?`;
|
||||||
|
// const values = req.query.module_id;
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = quizModuleList;
|
|
@ -0,0 +1,33 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const quizNewModule = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `INSERT INTO quiz_modules (moduleName, type) VALUES (?, ?)`;
|
||||||
|
const values = [data.moduleName, data.moduleType];
|
||||||
|
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = quizNewModule;
|
|
@ -0,0 +1,176 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const quizzesScore = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM quiz_score`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = quizzesScore;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let quizData = [
|
||||||
|
// {
|
||||||
|
// quizId: 1,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 1",
|
||||||
|
// percentage: "60"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 2,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 2",
|
||||||
|
// percentage: "75"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 3,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 3",
|
||||||
|
// percentage: "80"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 4,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 4",
|
||||||
|
// percentage: "65"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 5,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 5",
|
||||||
|
// percentage: "70"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 6,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 6",
|
||||||
|
// percentage: "85"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 7,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 7",
|
||||||
|
// percentage: "90"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 8,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 8",
|
||||||
|
// percentage: "95"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 9,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 9",
|
||||||
|
// percentage: "88"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 10,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 10",
|
||||||
|
// percentage: "92"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 11,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 11",
|
||||||
|
// percentage: "77"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 12,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 12",
|
||||||
|
// percentage: "82"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 13,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 13",
|
||||||
|
// percentage: "68"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 14,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 14",
|
||||||
|
// percentage: "73"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 15,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 15",
|
||||||
|
// percentage: "79"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 16,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 16",
|
||||||
|
// percentage: "87"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 17,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 17",
|
||||||
|
// percentage: "93"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 18,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 18",
|
||||||
|
// percentage: "67"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 19,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 19",
|
||||||
|
// percentage: "89"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// quizId: 20,
|
||||||
|
// quizType: "AI Quiz",
|
||||||
|
// quizName: "Assessment on Special Education - 20",
|
||||||
|
// percentage: "91"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// res.json(quizData);
|
|
@ -0,0 +1,93 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const resultAfterQuizSubmit = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryData = req.query;
|
||||||
|
const responseValues = [queryData.id]; // Ensure this is an array
|
||||||
|
|
||||||
|
const responseQuery = `SELECT * FROM quiz_response WHERE quizId = ?`;
|
||||||
|
connection.query(responseQuery, responseValues, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error retrieving data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
connection.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let questionsProcessed = 0;
|
||||||
|
let allResults = [];
|
||||||
|
|
||||||
|
results.forEach((resultData, index) => {
|
||||||
|
const questionId = [resultData.questionId]; // Ensure this is an array
|
||||||
|
const answerQuery = `SELECT * FROM quiz_questions WHERE questionId = ?`;
|
||||||
|
|
||||||
|
connection.query(answerQuery, questionId, (err, questionResults) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error retrieving question data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
connection.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allResults.push({
|
||||||
|
response: resultData,
|
||||||
|
question: questionResults[0] // Assuming it returns one result per questionId
|
||||||
|
});
|
||||||
|
|
||||||
|
questionsProcessed++;
|
||||||
|
|
||||||
|
if (questionsProcessed === results.length) {
|
||||||
|
let correctAnswers = 0;
|
||||||
|
allResults.forEach(item => {
|
||||||
|
if (item.response.selectedOption === item.question.correctAnswer) {
|
||||||
|
correctAnswers++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let quizMessage;
|
||||||
|
|
||||||
|
if(correctAnswers > 3){
|
||||||
|
quizMessage = 'Congratulations on your achievement.'
|
||||||
|
}else if(correctAnswers < 3){
|
||||||
|
quizMessage = 'You not paassed the quiz Try Again'
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
totalQuestions: results.length,
|
||||||
|
correctAnswers: correctAnswers,
|
||||||
|
message: quizMessage,
|
||||||
|
details: allResults
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
res.status(200).json({
|
||||||
|
totalQuestions: 0,
|
||||||
|
correctAnswers: 0,
|
||||||
|
details: []
|
||||||
|
});
|
||||||
|
connection.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = resultAfterQuizSubmit;
|
|
@ -0,0 +1,66 @@
|
||||||
|
var MongoClient = require('mongodb').MongoClient;
|
||||||
|
const AWS = require('aws-sdk');
|
||||||
|
|
||||||
|
const saveGameScore = (req, res) => {
|
||||||
|
const url = process.env.MONGODB_URL;
|
||||||
|
const dbName = process.env.MONGO_DB_NAME;
|
||||||
|
const client = new MongoClient(url, { useUnifiedTopology: true });
|
||||||
|
client.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to connect to the server', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log('Connected successfully to server');
|
||||||
|
const db = client.db(dbName);
|
||||||
|
const collection = db.collection('gameData');
|
||||||
|
// const data = req.body;
|
||||||
|
const { userId, gameName, gameID, gameTime, score, screenShot } = req.body;
|
||||||
|
const data = {
|
||||||
|
userId: userId,
|
||||||
|
gameName: gameName,
|
||||||
|
gameID: gameID
|
||||||
|
};
|
||||||
|
|
||||||
|
collection.insertOne(data, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to insert document', err);
|
||||||
|
} else {
|
||||||
|
// console.log('Document inserted with _id: ', result.insertedId);
|
||||||
|
}
|
||||||
|
client.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to close connection', err);
|
||||||
|
} else {
|
||||||
|
// console.log('Connection closed');
|
||||||
|
const s3 = new AWS.S3({
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
region: process.env.AWS_REGION
|
||||||
|
});
|
||||||
|
if (screenShot != undefined) {
|
||||||
|
// Upload image to S3
|
||||||
|
let base64Image = screenShot.split(";base64,").pop();
|
||||||
|
const buffer = Buffer.from(base64Image, 'base64');
|
||||||
|
const s3Params = {
|
||||||
|
Bucket: process.env.S3_BUCKET_NAME,
|
||||||
|
Key: `images/${result.insertedId}.png`, // Change the file extension to .png
|
||||||
|
Body: buffer,
|
||||||
|
ContentEncoding: 'base64',
|
||||||
|
ContentType: 'image/jpeg' // Change the content type to image/png
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const data = s3.upload(s3Params).promise();
|
||||||
|
console.log(`File uploaded successfully at ${data.Location}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res.send(result.insertedId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = saveGameScore;
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const savePostData = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `INSERT INTO tst (name, email) VALUES (?, ?)`;
|
||||||
|
const values = [data.name, data.email];
|
||||||
|
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).send('Data inserted successfully');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = savePostData;
|
|
@ -0,0 +1,59 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
const saveQuizResponse = (req, res) => {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
// console.log(data);
|
||||||
|
|
||||||
|
const query = `INSERT INTO quiz_response (questionId, quizId, selectedOption) VALUES (?, ?, ?)`;
|
||||||
|
|
||||||
|
let errorOccurred = false;
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
data.forEach((response) => {
|
||||||
|
const values = [response.questionId, response.quizId, response.selectedOption];
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
connection.query(query, values, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
errorOccurred = true;
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
promises.push(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
if (!errorOccurred) {
|
||||||
|
res.status(200).json({success: true, quizId: data[0].quizId, message: 'All responses saved successfully.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error saving responses:', error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
connection.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = saveQuizResponse;
|
|
@ -0,0 +1,231 @@
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
const topPerformers = (req, res) => {
|
||||||
|
// res.send(req.query.doa); //get
|
||||||
|
// res.send(req.body.doa); //post
|
||||||
|
|
||||||
|
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
host: process.env.MARIA_HOST,
|
||||||
|
user: process.env.MARIA_USER,
|
||||||
|
password: process.env.MARIA_PASS,
|
||||||
|
database: process.env.MARIA_DBNM
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.connect((err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to the MariaDB database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
const query = `SELECT * FROM top_performers`;
|
||||||
|
connection.query(query, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting data:', err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = topPerformers;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let performersData = [
|
||||||
|
// {
|
||||||
|
// id: "1",
|
||||||
|
// name: "Eiden",
|
||||||
|
// score: "48/50",
|
||||||
|
// points: "999",
|
||||||
|
// rank: "1",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar1.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "2",
|
||||||
|
// name: "Jackson",
|
||||||
|
// score: "45/50",
|
||||||
|
// points: "997",
|
||||||
|
// rank: "2",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar2.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "3",
|
||||||
|
// name: "Emma Aria",
|
||||||
|
// score: "43/50",
|
||||||
|
// points: "994",
|
||||||
|
// rank: "3",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar3.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "4",
|
||||||
|
// name: "John Doe",
|
||||||
|
// score: "40/50",
|
||||||
|
// points: "990",
|
||||||
|
// rank: "4",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar4.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "5",
|
||||||
|
// name: "Jane Cooper",
|
||||||
|
// score: "37/50",
|
||||||
|
// points: "987",
|
||||||
|
// rank: "5",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar5.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "6",
|
||||||
|
// name: "John Doe",
|
||||||
|
// score: "35/50",
|
||||||
|
// points: "982",
|
||||||
|
// rank: "6",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar6.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "7",
|
||||||
|
// name: "Alice",
|
||||||
|
// score: "33/50",
|
||||||
|
// points: "980",
|
||||||
|
// rank: "7",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar1.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "8",
|
||||||
|
// name: "Bob",
|
||||||
|
// score: "32/50",
|
||||||
|
// points: "978",
|
||||||
|
// rank: "8",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar2.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "9",
|
||||||
|
// name: "Charlie",
|
||||||
|
// score: "30/50",
|
||||||
|
// points: "975",
|
||||||
|
// rank: "9",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar3.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "10",
|
||||||
|
// name: "Diana",
|
||||||
|
// score: "28/50",
|
||||||
|
// points: "972",
|
||||||
|
// rank: "10",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar4.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "11",
|
||||||
|
// name: "Edward",
|
||||||
|
// score: "27/50",
|
||||||
|
// points: "970",
|
||||||
|
// rank: "11",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar5.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "12",
|
||||||
|
// name: "Fiona",
|
||||||
|
// score: "26/50",
|
||||||
|
// points: "968",
|
||||||
|
// rank: "12",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar6.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "13",
|
||||||
|
// name: "George",
|
||||||
|
// score: "25/50",
|
||||||
|
// points: "965",
|
||||||
|
// rank: "13",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar1.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "14",
|
||||||
|
// name: "Hannah",
|
||||||
|
// score: "23/50",
|
||||||
|
// points: "962",
|
||||||
|
// rank: "14",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar2.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "15",
|
||||||
|
// name: "Ian",
|
||||||
|
// score: "22/50",
|
||||||
|
// points: "960",
|
||||||
|
// rank: "15",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar3.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "16",
|
||||||
|
// name: "Julia",
|
||||||
|
// score: "20/50",
|
||||||
|
// points: "957",
|
||||||
|
// rank: "16",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar4.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "17",
|
||||||
|
// name: "Kyle",
|
||||||
|
// score: "19/50",
|
||||||
|
// points: "955",
|
||||||
|
// rank: "17",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar5.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "18",
|
||||||
|
// name: "Laura",
|
||||||
|
// score: "18/50",
|
||||||
|
// points: "953",
|
||||||
|
// rank: "18",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar6.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "19",
|
||||||
|
// name: "Michael",
|
||||||
|
// score: "17/50",
|
||||||
|
// points: "950",
|
||||||
|
// rank: "19",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar1.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "20",
|
||||||
|
// name: "Nancy",
|
||||||
|
// score: "16/50",
|
||||||
|
// points: "947",
|
||||||
|
// rank: "20",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar2.png"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "21",
|
||||||
|
// name: "Oliver",
|
||||||
|
// score: "15/50",
|
||||||
|
// points: "945",
|
||||||
|
// rank: "21",
|
||||||
|
// program: "Graduate Program",
|
||||||
|
// avatar: "/assets/avatar3.png"
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// res.json(performersData);
|
|
@ -0,0 +1,378 @@
|
||||||
|
// const express = require('express');
|
||||||
|
const express = require("express");
|
||||||
|
const Ping = require("../api/apiTest");
|
||||||
|
const apiTest = require("../api/apiTest");
|
||||||
|
const topPerformers = require("../api/topPerformers");
|
||||||
|
const classMates = require("../api/classMates");
|
||||||
|
const continueLearning = require("../api/continueLearning");
|
||||||
|
const knowledgeQuests = require("../api/knowledgeQuests");
|
||||||
|
const quizzesScore = require("../api/quizzesScore");
|
||||||
|
const quizModuleData = require("../api/quizModuleData");
|
||||||
|
const knowledgeQuestsAllContent = require("../api/knowledgeQuestsAllContent");
|
||||||
|
const knowledgeQuestsCompleted = require("../api/knowledgeQuestsCompleted");
|
||||||
|
const quizModuleList = require("../api/quizModuleList");
|
||||||
|
const quizNewModule = require("../api/quizNewModule");
|
||||||
|
const newQuiz = require("../api/newQuiz");
|
||||||
|
const quizList = require("../api/quizList");
|
||||||
|
const savePostData = require("../api/savePostData");
|
||||||
|
// const signIn = require("../api/signIn");
|
||||||
|
const questionList = require("../api/questionList");
|
||||||
|
const newQuestion = require("../api/newQuestion");
|
||||||
|
const saveQuizResponse = require("../api/saveQuizResponse");
|
||||||
|
const getGameScore = require("../api/getGameScore");
|
||||||
|
const resultAfterQuizSubmit = require("../api/resultAfterQuizSubmit");
|
||||||
|
const generateQuestions = require("../api/generateQuestions");
|
||||||
|
const saveGameScore = require("../api/saveGameScore");
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
/* GET home page. */
|
||||||
|
router.get("/ping", (req, res) => {
|
||||||
|
Ping(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get("/apiTest", (req, res) => {
|
||||||
|
apiTest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Classmates Directory page top performers section data
|
||||||
|
router.get("/top-performers", (req, res) => {
|
||||||
|
topPerformers(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Classmates Directory page top class mates section data
|
||||||
|
router.get("/class-mates", (req, res) => {
|
||||||
|
classMates(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Student Dashboard page Continue Learning section data
|
||||||
|
router.get("/continue-learning", (req, res) => {
|
||||||
|
continueLearning(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Student Dashboard page knowledge-Quests section data
|
||||||
|
router.get("/knowledge-quests", (req, res) => {
|
||||||
|
knowledgeQuests(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Progress Review page quiz details section data
|
||||||
|
router.get("/quiz-score", (req, res) => {
|
||||||
|
quizzesScore(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Progress Review page quiz module section data
|
||||||
|
router.get("/quiz-module", (req, res) => {
|
||||||
|
quizModuleData(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// knowledge Quests page All Content section data
|
||||||
|
router.get("/all-assesment", (req, res) => {
|
||||||
|
knowledgeQuestsAllContent(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// knowledge Quests page Completed section data
|
||||||
|
router.get("/complete-assesment", (req, res) => {
|
||||||
|
knowledgeQuestsCompleted(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.post("/savePostData", (req, res) => {
|
||||||
|
savePostData(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Sign in
|
||||||
|
// router.post("/signin", (req, res) => {
|
||||||
|
// signIn(req, res);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// For Quiz Module list Data
|
||||||
|
router.get("/quiz-module-list", (req, res) => {
|
||||||
|
quizModuleList(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Create new module
|
||||||
|
router.post("/create-module", (req, res) => {
|
||||||
|
quizNewModule(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Create new Quiz
|
||||||
|
router.post("/create-quiz", (req, res) => {
|
||||||
|
newQuiz(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz List data
|
||||||
|
router.get("/quiz-list", (req, res) => {
|
||||||
|
quizList(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Question List data
|
||||||
|
router.get("/question-list", (req, res) => {
|
||||||
|
questionList(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Question List data
|
||||||
|
router.post("/create-question", (req, res) => {
|
||||||
|
newQuestion(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Question List data
|
||||||
|
router.post("/save-quiz-response", (req, res) => {
|
||||||
|
saveQuizResponse(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Question List data
|
||||||
|
router.post("/getGameScore", (req, res) => {
|
||||||
|
getGameScore(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// For Quiz Result After Submit Quiz
|
||||||
|
router.get("/quizresult-aftersubmit", (req, res) => {
|
||||||
|
resultAfterQuizSubmit(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Result After Submit Quiz
|
||||||
|
router.post("/generateQuestions", (req, res) => {
|
||||||
|
generateQuestions(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Quiz Result After Submit Quiz
|
||||||
|
router.post("/saveGameScore", (req, res) => {
|
||||||
|
saveGameScore(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Users
|
||||||
|
* description: User management and retrieval
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /users:
|
||||||
|
* post:
|
||||||
|
* summary: Create a user
|
||||||
|
* description: Only admins can create other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* - role
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: must be unique
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* role:
|
||||||
|
* type: string
|
||||||
|
* enum: [user, admin]
|
||||||
|
* example:
|
||||||
|
* name: fake name
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* role: user
|
||||||
|
* responses:
|
||||||
|
* "201":
|
||||||
|
* description: Created
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "400":
|
||||||
|
* $ref: '#/components/responses/DuplicateEmail'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
*
|
||||||
|
* get:
|
||||||
|
* summary: Get all users
|
||||||
|
* description: Only admins can retrieve all users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: name
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User name
|
||||||
|
* - in: query
|
||||||
|
* name: role
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User role
|
||||||
|
* - in: query
|
||||||
|
* name: sortBy
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: sort by query in the form of field:desc/asc (ex. name:asc)
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* default: 10
|
||||||
|
* description: Maximum number of users
|
||||||
|
* - in: query
|
||||||
|
* name: page
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* default: 1
|
||||||
|
* description: Page number
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* results:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* page:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* limit:
|
||||||
|
* type: integer
|
||||||
|
* example: 10
|
||||||
|
* totalPages:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* totalResults:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /users/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Get a user
|
||||||
|
* description: Logged in users can fetch only their own user information. Only admins can fetch other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*
|
||||||
|
* patch:
|
||||||
|
* summary: Update a user
|
||||||
|
* description: Logged in users can only update their own information. Only admins can update other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: must be unique
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* example:
|
||||||
|
* name: fake name
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "400":
|
||||||
|
* $ref: '#/components/responses/DuplicateEmail'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*
|
||||||
|
* delete:
|
||||||
|
* summary: Delete a user
|
||||||
|
* description: Logged in users can delete only themselves. Only admins can delete other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: No content
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*/
|
|
@ -0,0 +1,291 @@
|
||||||
|
const express = require('express');
|
||||||
|
const validate = require('../../middlewares/validate');
|
||||||
|
const authValidation = require('../../validations/auth.validation');
|
||||||
|
const authController = require('../../controllers/auth.controller');
|
||||||
|
const auth = require('../../middlewares/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/register', validate(authValidation.register), authController.register);
|
||||||
|
router.post('/login', validate(authValidation.login), authController.login);
|
||||||
|
router.post('/logout', validate(authValidation.logout), authController.logout);
|
||||||
|
router.post('/refresh-tokens', validate(authValidation.refreshTokens), authController.refreshTokens);
|
||||||
|
router.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword);
|
||||||
|
router.post('/reset-password', validate(authValidation.resetPassword), authController.resetPassword);
|
||||||
|
router.post('/send-verification-email', auth(), authController.sendVerificationEmail);
|
||||||
|
router.post('/verify-email', validate(authValidation.verifyEmail), authController.verifyEmail);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Auth
|
||||||
|
* description: Authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/register:
|
||||||
|
* post:
|
||||||
|
* summary: Register as user
|
||||||
|
* tags: [Auth]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: must be unique
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* example:
|
||||||
|
* name: fake name
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* responses:
|
||||||
|
* "201":
|
||||||
|
* description: Created
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* user:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* tokens:
|
||||||
|
* $ref: '#/components/schemas/AuthTokens'
|
||||||
|
* "400":
|
||||||
|
* $ref: '#/components/responses/DuplicateEmail'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/login:
|
||||||
|
* post:
|
||||||
|
* summary: Login
|
||||||
|
* tags: [Auth]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* example:
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* user:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* tokens:
|
||||||
|
* $ref: '#/components/schemas/AuthTokens'
|
||||||
|
* "401":
|
||||||
|
* description: Invalid email or password
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* example:
|
||||||
|
* code: 401
|
||||||
|
* message: Invalid email or password
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/logout:
|
||||||
|
* post:
|
||||||
|
* summary: Logout
|
||||||
|
* tags: [Auth]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - refreshToken
|
||||||
|
* properties:
|
||||||
|
* refreshToken:
|
||||||
|
* type: string
|
||||||
|
* example:
|
||||||
|
* refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg
|
||||||
|
* responses:
|
||||||
|
* "204":
|
||||||
|
* description: No content
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/refresh-tokens:
|
||||||
|
* post:
|
||||||
|
* summary: Refresh auth tokens
|
||||||
|
* tags: [Auth]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - refreshToken
|
||||||
|
* properties:
|
||||||
|
* refreshToken:
|
||||||
|
* type: string
|
||||||
|
* example:
|
||||||
|
* refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/AuthTokens'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/forgot-password:
|
||||||
|
* post:
|
||||||
|
* summary: Forgot password
|
||||||
|
* description: An email will be sent to reset password.
|
||||||
|
* tags: [Auth]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - email
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* example:
|
||||||
|
* email: fake@example.com
|
||||||
|
* responses:
|
||||||
|
* "204":
|
||||||
|
* description: No content
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/reset-password:
|
||||||
|
* post:
|
||||||
|
* summary: Reset password
|
||||||
|
* tags: [Auth]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: token
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: The reset password token
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* example:
|
||||||
|
* password: password1
|
||||||
|
* responses:
|
||||||
|
* "204":
|
||||||
|
* description: No content
|
||||||
|
* "401":
|
||||||
|
* description: Password reset failed
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* example:
|
||||||
|
* code: 401
|
||||||
|
* message: Password reset failed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/send-verification-email:
|
||||||
|
* post:
|
||||||
|
* summary: Send verification email
|
||||||
|
* description: An email will be sent to verify email.
|
||||||
|
* tags: [Auth]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* "204":
|
||||||
|
* description: No content
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /auth/verify-email:
|
||||||
|
* post:
|
||||||
|
* summary: verify email
|
||||||
|
* tags: [Auth]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: token
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: The verify email token
|
||||||
|
* responses:
|
||||||
|
* "204":
|
||||||
|
* description: No content
|
||||||
|
* "401":
|
||||||
|
* description: verify email failed
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* example:
|
||||||
|
* code: 401
|
||||||
|
* message: verify email failed
|
||||||
|
*/
|
|
@ -0,0 +1,21 @@
|
||||||
|
const express = require('express');
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const swaggerDefinition = require('../../docs/swaggerDef');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const specs = swaggerJsdoc({
|
||||||
|
swaggerDefinition,
|
||||||
|
apis: ['src/docs/*.yml', 'src/routes/v1/*.js'],
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/', swaggerUi.serve);
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
swaggerUi.setup(specs, {
|
||||||
|
explorer: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -0,0 +1,44 @@
|
||||||
|
const express = require('express');
|
||||||
|
const authRoute = require('./auth.route');
|
||||||
|
const userRoute = require('./user.route');
|
||||||
|
const apiRoute = require('./api.route');
|
||||||
|
const docsRoute = require('./docs.route');
|
||||||
|
const config = require('../../config/config');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const defaultRoutes = [
|
||||||
|
{
|
||||||
|
path: '/auth',
|
||||||
|
route: authRoute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
route: userRoute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/api',
|
||||||
|
route: apiRoute,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const devRoutes = [
|
||||||
|
// routes available only in development mode
|
||||||
|
{
|
||||||
|
path: '/docs',
|
||||||
|
route: docsRoute,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultRoutes.forEach((route) => {
|
||||||
|
router.use(route.path, route.route);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (config.env === 'development') {
|
||||||
|
devRoutes.forEach((route) => {
|
||||||
|
router.use(route.path, route.route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -0,0 +1,252 @@
|
||||||
|
const express = require('express');
|
||||||
|
const auth = require('../../middlewares/auth');
|
||||||
|
const validate = require('../../middlewares/validate');
|
||||||
|
const userValidation = require('../../validations/user.validation');
|
||||||
|
const userController = require('../../controllers/user.controller');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/')
|
||||||
|
.post(auth('manageUsers'), validate(userValidation.createUser), userController.createUser)
|
||||||
|
.get(auth('getUsers'), validate(userValidation.getUsers), userController.getUsers);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/:userId')
|
||||||
|
.get(auth('getUsers'), validate(userValidation.getUser), userController.getUser)
|
||||||
|
.patch(auth('manageUsers'), validate(userValidation.updateUser), userController.updateUser)
|
||||||
|
.delete(auth('manageUsers'), validate(userValidation.deleteUser), userController.deleteUser);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Users
|
||||||
|
* description: User management and retrieval
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /users:
|
||||||
|
* post:
|
||||||
|
* summary: Create a user
|
||||||
|
* description: Only admins can create other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* - role
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: must be unique
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* role:
|
||||||
|
* type: string
|
||||||
|
* enum: [user, admin]
|
||||||
|
* example:
|
||||||
|
* name: fake name
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* role: user
|
||||||
|
* responses:
|
||||||
|
* "201":
|
||||||
|
* description: Created
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "400":
|
||||||
|
* $ref: '#/components/responses/DuplicateEmail'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
*
|
||||||
|
* get:
|
||||||
|
* summary: Get all users
|
||||||
|
* description: Only admins can retrieve all users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: name
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User name
|
||||||
|
* - in: query
|
||||||
|
* name: role
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User role
|
||||||
|
* - in: query
|
||||||
|
* name: sortBy
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: sort by query in the form of field:desc/asc (ex. name:asc)
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* default: 10
|
||||||
|
* description: Maximum number of users
|
||||||
|
* - in: query
|
||||||
|
* name: page
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* default: 1
|
||||||
|
* description: Page number
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* results:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* page:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* limit:
|
||||||
|
* type: integer
|
||||||
|
* example: 10
|
||||||
|
* totalPages:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* totalResults:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /users/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Get a user
|
||||||
|
* description: Logged in users can fetch only their own user information. Only admins can fetch other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*
|
||||||
|
* patch:
|
||||||
|
* summary: Update a user
|
||||||
|
* description: Logged in users can only update their own information. Only admins can update other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: must be unique
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* minLength: 8
|
||||||
|
* description: At least one number and one letter
|
||||||
|
* example:
|
||||||
|
* name: fake name
|
||||||
|
* email: fake@example.com
|
||||||
|
* password: password1
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* "400":
|
||||||
|
* $ref: '#/components/responses/DuplicateEmail'
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*
|
||||||
|
* delete:
|
||||||
|
* summary: Delete a user
|
||||||
|
* description: Logged in users can delete only themselves. Only admins can delete other users.
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: User id
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: No content
|
||||||
|
* "401":
|
||||||
|
* $ref: '#/components/responses/Unauthorized'
|
||||||
|
* "403":
|
||||||
|
* $ref: '#/components/responses/Forbidden'
|
||||||
|
* "404":
|
||||||
|
* $ref: '#/components/responses/NotFound'
|
||||||
|
*/
|
|
@ -0,0 +1,99 @@
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const tokenService = require('./token.service');
|
||||||
|
const userService = require('./user.service');
|
||||||
|
const Token = require('../models/token.model');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
const { tokenTypes } = require('../config/tokens');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with username and password
|
||||||
|
* @param {string} email
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const loginUserWithEmailAndPassword = async (email, password) => {
|
||||||
|
const user = await userService.getUserByEmail(email);
|
||||||
|
if (!user || !(await user.isPasswordMatch(password))) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout
|
||||||
|
* @param {string} refreshToken
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const logout = async (refreshToken) => {
|
||||||
|
const refreshTokenDoc = await Token.findOne({ token: refreshToken, type: tokenTypes.REFRESH, blacklisted: false });
|
||||||
|
if (!refreshTokenDoc) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'Not found');
|
||||||
|
}
|
||||||
|
await refreshTokenDoc.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh auth tokens
|
||||||
|
* @param {string} refreshToken
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
const refreshAuth = async (refreshToken) => {
|
||||||
|
try {
|
||||||
|
const refreshTokenDoc = await tokenService.verifyToken(refreshToken, tokenTypes.REFRESH);
|
||||||
|
const user = await userService.getUserById(refreshTokenDoc.user);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
await refreshTokenDoc.remove();
|
||||||
|
return tokenService.generateAuthTokens(user);
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password
|
||||||
|
* @param {string} resetPasswordToken
|
||||||
|
* @param {string} newPassword
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const resetPassword = async (resetPasswordToken, newPassword) => {
|
||||||
|
try {
|
||||||
|
const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
|
||||||
|
const user = await userService.getUserById(resetPasswordTokenDoc.user);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
await userService.updateUserById(user.id, { password: newPassword });
|
||||||
|
await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify email
|
||||||
|
* @param {string} verifyEmailToken
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const verifyEmail = async (verifyEmailToken) => {
|
||||||
|
try {
|
||||||
|
const verifyEmailTokenDoc = await tokenService.verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL);
|
||||||
|
const user = await userService.getUserById(verifyEmailTokenDoc.user);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL });
|
||||||
|
await userService.updateUserById(user.id, { isEmailVerified: true });
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loginUserWithEmailAndPassword,
|
||||||
|
logout,
|
||||||
|
refreshAuth,
|
||||||
|
resetPassword,
|
||||||
|
verifyEmail,
|
||||||
|
};
|
|
@ -0,0 +1,63 @@
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const config = require('../config/config');
|
||||||
|
const logger = require('../config/logger');
|
||||||
|
|
||||||
|
const transport = nodemailer.createTransport(config.email.smtp);
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (config.env !== 'test') {
|
||||||
|
transport
|
||||||
|
.verify()
|
||||||
|
.then(() => logger.info('Connected to email server'))
|
||||||
|
.catch(() => logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an email
|
||||||
|
* @param {string} to
|
||||||
|
* @param {string} subject
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const sendEmail = async (to, subject, text) => {
|
||||||
|
const msg = { from: config.email.from, to, subject, text };
|
||||||
|
await transport.sendMail(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send reset password email
|
||||||
|
* @param {string} to
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const sendResetPasswordEmail = async (to, token) => {
|
||||||
|
const subject = 'Reset password';
|
||||||
|
// replace this url with the link to the reset password page of your front-end app
|
||||||
|
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
|
||||||
|
const text = `Dear user,
|
||||||
|
To reset your password, click on this link: ${resetPasswordUrl}
|
||||||
|
If you did not request any password resets, then ignore this email.`;
|
||||||
|
await sendEmail(to, subject, text);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send verification email
|
||||||
|
* @param {string} to
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const sendVerificationEmail = async (to, token) => {
|
||||||
|
const subject = 'Email Verification';
|
||||||
|
// replace this url with the link to the email verification page of your front-end app
|
||||||
|
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
|
||||||
|
const text = `Dear user,
|
||||||
|
To verify your email, click on this link: ${verificationEmailUrl}
|
||||||
|
If you did not create an account, then ignore this email.`;
|
||||||
|
await sendEmail(to, subject, text);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
transport,
|
||||||
|
sendEmail,
|
||||||
|
sendResetPasswordEmail,
|
||||||
|
sendVerificationEmail,
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports.authService = require('./auth.service');
|
||||||
|
module.exports.emailService = require('./email.service');
|
||||||
|
module.exports.tokenService = require('./token.service');
|
||||||
|
module.exports.userService = require('./user.service');
|
|
@ -0,0 +1,123 @@
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const moment = require('moment');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const config = require('../config/config');
|
||||||
|
const userService = require('./user.service');
|
||||||
|
const { Token } = require('../models');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
const { tokenTypes } = require('../config/tokens');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate token
|
||||||
|
* @param {ObjectId} userId
|
||||||
|
* @param {Moment} expires
|
||||||
|
* @param {string} type
|
||||||
|
* @param {string} [secret]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const generateToken = (userId, expires, type, secret = config.jwt.secret) => {
|
||||||
|
const payload = {
|
||||||
|
sub: userId,
|
||||||
|
iat: moment().unix(),
|
||||||
|
exp: expires.unix(),
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
return jwt.sign(payload, secret);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a token
|
||||||
|
* @param {string} token
|
||||||
|
* @param {ObjectId} userId
|
||||||
|
* @param {Moment} expires
|
||||||
|
* @param {string} type
|
||||||
|
* @param {boolean} [blacklisted]
|
||||||
|
* @returns {Promise<Token>}
|
||||||
|
*/
|
||||||
|
const saveToken = async (token, userId, expires, type, blacklisted = false) => {
|
||||||
|
const tokenDoc = await Token.create({
|
||||||
|
token,
|
||||||
|
user: userId,
|
||||||
|
expires: expires.toDate(),
|
||||||
|
type,
|
||||||
|
blacklisted,
|
||||||
|
});
|
||||||
|
return tokenDoc;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify token and return token doc (or throw an error if it is not valid)
|
||||||
|
* @param {string} token
|
||||||
|
* @param {string} type
|
||||||
|
* @returns {Promise<Token>}
|
||||||
|
*/
|
||||||
|
const verifyToken = async (token, type) => {
|
||||||
|
const payload = jwt.verify(token, config.jwt.secret);
|
||||||
|
const tokenDoc = await Token.findOne({ token, type, user: payload.sub, blacklisted: false });
|
||||||
|
if (!tokenDoc) {
|
||||||
|
throw new Error('Token not found');
|
||||||
|
}
|
||||||
|
return tokenDoc;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate auth tokens
|
||||||
|
* @param {User} user
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
const generateAuthTokens = async (user) => {
|
||||||
|
const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
||||||
|
const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS);
|
||||||
|
|
||||||
|
const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = generateToken(user.id, refreshTokenExpires, tokenTypes.REFRESH);
|
||||||
|
await saveToken(refreshToken, user.id, refreshTokenExpires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
return {
|
||||||
|
access: {
|
||||||
|
token: accessToken,
|
||||||
|
expires: accessTokenExpires.toDate(),
|
||||||
|
},
|
||||||
|
refresh: {
|
||||||
|
token: refreshToken,
|
||||||
|
expires: refreshTokenExpires.toDate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate reset password token
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
const generateResetPasswordToken = async (email) => {
|
||||||
|
const user = await userService.getUserByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');
|
||||||
|
}
|
||||||
|
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
||||||
|
const resetPasswordToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
return resetPasswordToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate verify email token
|
||||||
|
* @param {User} user
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
const generateVerifyEmailToken = async (user) => {
|
||||||
|
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
|
||||||
|
const verifyEmailToken = generateToken(user.id, expires, tokenTypes.VERIFY_EMAIL);
|
||||||
|
await saveToken(verifyEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL);
|
||||||
|
return verifyEmailToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateToken,
|
||||||
|
saveToken,
|
||||||
|
verifyToken,
|
||||||
|
generateAuthTokens,
|
||||||
|
generateResetPasswordToken,
|
||||||
|
generateVerifyEmailToken,
|
||||||
|
};
|
|
@ -0,0 +1,89 @@
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const { User } = require('../models');
|
||||||
|
const ApiError = require('../utils/ApiError');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a user
|
||||||
|
* @param {Object} userBody
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const createUser = async (userBody) => {
|
||||||
|
if (await User.isEmailTaken(userBody.email)) {
|
||||||
|
throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');
|
||||||
|
}
|
||||||
|
return User.create(userBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for users
|
||||||
|
* @param {Object} filter - Mongo filter
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
|
||||||
|
* @param {number} [options.limit] - Maximum number of results per page (default = 10)
|
||||||
|
* @param {number} [options.page] - Current page (default = 1)
|
||||||
|
* @returns {Promise<QueryResult>}
|
||||||
|
*/
|
||||||
|
const queryUsers = async (filter, options) => {
|
||||||
|
const users = await User.paginate(filter, options);
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user by id
|
||||||
|
* @param {ObjectId} id
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const getUserById = async (id) => {
|
||||||
|
return User.findById(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user by email
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const getUserByEmail = async (email) => {
|
||||||
|
return User.findOne({ email });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user by id
|
||||||
|
* @param {ObjectId} userId
|
||||||
|
* @param {Object} updateBody
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const updateUserById = async (userId, updateBody) => {
|
||||||
|
const user = await getUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
||||||
|
}
|
||||||
|
if (updateBody.email && (await User.isEmailTaken(updateBody.email, userId))) {
|
||||||
|
throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');
|
||||||
|
}
|
||||||
|
Object.assign(user, updateBody);
|
||||||
|
await user.save();
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user by id
|
||||||
|
* @param {ObjectId} userId
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
const deleteUserById = async (userId) => {
|
||||||
|
const user = await getUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
||||||
|
}
|
||||||
|
await user.remove();
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createUser,
|
||||||
|
queryUsers,
|
||||||
|
getUserById,
|
||||||
|
getUserByEmail,
|
||||||
|
updateUserById,
|
||||||
|
deleteUserById,
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(statusCode, message, isOperational = true, stack = '') {
|
||||||
|
super(message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.isOperational = isOperational;
|
||||||
|
if (stack) {
|
||||||
|
this.stack = stack;
|
||||||
|
} else {
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiError;
|
|
@ -0,0 +1,5 @@
|
||||||
|
const catchAsync = (fn) => (req, res, next) => {
|
||||||
|
Promise.resolve(fn(req, res, next)).catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = catchAsync;
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Create an object composed of the picked object properties
|
||||||
|
* @param {Object} object
|
||||||
|
* @param {string[]} keys
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
const pick = (object, keys) => {
|
||||||
|
return keys.reduce((obj, key) => {
|
||||||
|
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
obj[key] = object[key];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = pick;
|
|
@ -0,0 +1,60 @@
|
||||||
|
const Joi = require('joi');
|
||||||
|
const { password } = require('./custom.validation');
|
||||||
|
|
||||||
|
const register = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
email: Joi.string().required().email(),
|
||||||
|
password: Joi.string().required().custom(password),
|
||||||
|
name: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
email: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
refreshToken: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshTokens = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
refreshToken: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const forgotPassword = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
email: Joi.string().email().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPassword = {
|
||||||
|
query: Joi.object().keys({
|
||||||
|
token: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
body: Joi.object().keys({
|
||||||
|
password: Joi.string().required().custom(password),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyEmail = {
|
||||||
|
query: Joi.object().keys({
|
||||||
|
token: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshTokens,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
verifyEmail,
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
const objectId = (value, helpers) => {
|
||||||
|
if (!value.match(/^[0-9a-fA-F]{24}$/)) {
|
||||||
|
return helpers.message('"{{#label}}" must be a valid mongo id');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const password = (value, helpers) => {
|
||||||
|
if (value.length < 8) {
|
||||||
|
return helpers.message('password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
|
||||||
|
return helpers.message('password must contain at least 1 letter and 1 number');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
objectId,
|
||||||
|
password,
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports.authValidation = require('./auth.validation');
|
||||||
|
module.exports.userValidation = require('./user.validation');
|
|
@ -0,0 +1,54 @@
|
||||||
|
const Joi = require('joi');
|
||||||
|
const { password, objectId } = require('./custom.validation');
|
||||||
|
|
||||||
|
const createUser = {
|
||||||
|
body: Joi.object().keys({
|
||||||
|
email: Joi.string().required().email(),
|
||||||
|
password: Joi.string().required().custom(password),
|
||||||
|
name: Joi.string().required(),
|
||||||
|
role: Joi.string().required().valid('user', 'admin'),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsers = {
|
||||||
|
query: Joi.object().keys({
|
||||||
|
name: Joi.string(),
|
||||||
|
role: Joi.string(),
|
||||||
|
sortBy: Joi.string(),
|
||||||
|
limit: Joi.number().integer(),
|
||||||
|
page: Joi.number().integer(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUser = {
|
||||||
|
params: Joi.object().keys({
|
||||||
|
userId: Joi.string().custom(objectId),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = {
|
||||||
|
params: Joi.object().keys({
|
||||||
|
userId: Joi.required().custom(objectId),
|
||||||
|
}),
|
||||||
|
body: Joi.object()
|
||||||
|
.keys({
|
||||||
|
email: Joi.string().email(),
|
||||||
|
password: Joi.string().custom(password),
|
||||||
|
name: Joi.string(),
|
||||||
|
})
|
||||||
|
.min(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = {
|
||||||
|
params: Joi.object().keys({
|
||||||
|
userId: Joi.string().custom(objectId),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createUser,
|
||||||
|
getUsers,
|
||||||
|
getUser,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
const moment = require('moment');
|
||||||
|
const config = require('../../src/config/config');
|
||||||
|
const { tokenTypes } = require('../../src/config/tokens');
|
||||||
|
const tokenService = require('../../src/services/token.service');
|
||||||
|
const { userOne, admin } = require('./user.fixture');
|
||||||
|
|
||||||
|
const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
||||||
|
const userOneAccessToken = tokenService.generateToken(userOne._id, accessTokenExpires, tokenTypes.ACCESS);
|
||||||
|
const adminAccessToken = tokenService.generateToken(admin._id, accessTokenExpires, tokenTypes.ACCESS);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
userOneAccessToken,
|
||||||
|
adminAccessToken,
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const faker = require('faker');
|
||||||
|
const User = require('../../src/models/user.model');
|
||||||
|
|
||||||
|
const password = 'password1';
|
||||||
|
const salt = bcrypt.genSaltSync(8);
|
||||||
|
const hashedPassword = bcrypt.hashSync(password, salt);
|
||||||
|
|
||||||
|
const userOne = {
|
||||||
|
_id: mongoose.Types.ObjectId(),
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password,
|
||||||
|
role: 'user',
|
||||||
|
isEmailVerified: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userTwo = {
|
||||||
|
_id: mongoose.Types.ObjectId(),
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password,
|
||||||
|
role: 'user',
|
||||||
|
isEmailVerified: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const admin = {
|
||||||
|
_id: mongoose.Types.ObjectId(),
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password,
|
||||||
|
role: 'admin',
|
||||||
|
isEmailVerified: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertUsers = async (users) => {
|
||||||
|
await User.insertMany(users.map((user) => ({ ...user, password: hashedPassword })));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
userOne,
|
||||||
|
userTwo,
|
||||||
|
admin,
|
||||||
|
insertUsers,
|
||||||
|
};
|
|
@ -0,0 +1,587 @@
|
||||||
|
const request = require('supertest');
|
||||||
|
const faker = require('faker');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const httpMocks = require('node-mocks-http');
|
||||||
|
const moment = require('moment');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const app = require('../../src/app');
|
||||||
|
const config = require('../../src/config/config');
|
||||||
|
const auth = require('../../src/middlewares/auth');
|
||||||
|
const { tokenService, emailService } = require('../../src/services');
|
||||||
|
const ApiError = require('../../src/utils/ApiError');
|
||||||
|
const setupTestDB = require('../utils/setupTestDB');
|
||||||
|
const { User, Token } = require('../../src/models');
|
||||||
|
const { roleRights } = require('../../src/config/roles');
|
||||||
|
const { tokenTypes } = require('../../src/config/tokens');
|
||||||
|
const { userOne, admin, insertUsers } = require('../fixtures/user.fixture');
|
||||||
|
const { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture');
|
||||||
|
|
||||||
|
setupTestDB();
|
||||||
|
|
||||||
|
describe('Auth routes', () => {
|
||||||
|
describe('POST /v1/auth/register', () => {
|
||||||
|
let newUser;
|
||||||
|
beforeEach(() => {
|
||||||
|
newUser = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: 'password1',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 201 and successfully register user if request data is ok', async () => {
|
||||||
|
const res = await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.CREATED);
|
||||||
|
|
||||||
|
expect(res.body.user).not.toHaveProperty('password');
|
||||||
|
expect(res.body.user).toEqual({
|
||||||
|
id: expect.anything(),
|
||||||
|
name: newUser.name,
|
||||||
|
email: newUser.email,
|
||||||
|
role: 'user',
|
||||||
|
isEmailVerified: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbUser = await User.findById(res.body.user.id);
|
||||||
|
expect(dbUser).toBeDefined();
|
||||||
|
expect(dbUser.password).not.toBe(newUser.password);
|
||||||
|
expect(dbUser).toMatchObject({ name: newUser.name, email: newUser.email, role: 'user', isEmailVerified: false });
|
||||||
|
|
||||||
|
expect(res.body.tokens).toEqual({
|
||||||
|
access: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
refresh: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if email is invalid', async () => {
|
||||||
|
newUser.email = 'invalidEmail';
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if email is already used', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
newUser.email = userOne.email;
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if password length is less than 8 characters', async () => {
|
||||||
|
newUser.password = 'passwo1';
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if password does not contain both letters and numbers', async () => {
|
||||||
|
newUser.password = 'password';
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
newUser.password = '11111111';
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/login', () => {
|
||||||
|
test('should return 200 and login user if email and password match', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const loginCredentials = {
|
||||||
|
email: userOne.email,
|
||||||
|
password: userOne.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body.user).toEqual({
|
||||||
|
id: expect.anything(),
|
||||||
|
name: userOne.name,
|
||||||
|
email: userOne.email,
|
||||||
|
role: userOne.role,
|
||||||
|
isEmailVerified: userOne.isEmailVerified,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.tokens).toEqual({
|
||||||
|
access: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
refresh: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if there are no users with that email', async () => {
|
||||||
|
const loginCredentials = {
|
||||||
|
email: userOne.email,
|
||||||
|
password: userOne.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if password is wrong', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const loginCredentials = {
|
||||||
|
email: userOne.email,
|
||||||
|
password: 'wrongPassword1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/logout', () => {
|
||||||
|
test('should return 204 if refresh token is valid', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
const dbRefreshTokenDoc = await Token.findOne({ token: refreshToken });
|
||||||
|
expect(dbRefreshTokenDoc).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if refresh token is missing from request body', async () => {
|
||||||
|
await request(app).post('/v1/auth/logout').send().expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 error if refresh token is not found in the database', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 error if refresh token is blacklisted', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/refresh-tokens', () => {
|
||||||
|
test('should return 200 and new auth tokens if refresh token is valid', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
const res = await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
access: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
refresh: { token: expect.anything(), expires: expect.anything() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbRefreshTokenDoc = await Token.findOne({ token: res.body.refresh.token });
|
||||||
|
expect(dbRefreshTokenDoc).toMatchObject({ type: tokenTypes.REFRESH, user: userOne._id, blacklisted: false });
|
||||||
|
|
||||||
|
const dbRefreshTokenCount = await Token.countDocuments();
|
||||||
|
expect(dbRefreshTokenCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if refresh token is missing from request body', async () => {
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send().expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if refresh token is signed using an invalid secret', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH, 'invalidSecret');
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if refresh token is not found in the database', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if refresh token is blacklisted', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if refresh token is expired', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().subtract(1, 'minutes');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if user is not found', async () => {
|
||||||
|
const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/forgot-password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 204 and send reset password email to the user', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const sendResetPasswordEmailSpy = jest.spyOn(emailService, 'sendResetPasswordEmail');
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
expect(sendResetPasswordEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
|
||||||
|
const resetPasswordToken = sendResetPasswordEmailSpy.mock.calls[0][1];
|
||||||
|
const dbResetPasswordTokenDoc = await Token.findOne({ token: resetPasswordToken, user: userOne._id });
|
||||||
|
expect(dbResetPasswordTokenDoc).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if email is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/forgot-password').send().expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 if email does not belong to any user', async () => {
|
||||||
|
await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/reset-password', () => {
|
||||||
|
test('should return 204 and reset the password', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
||||||
|
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'password2' })
|
||||||
|
.expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
const dbUser = await User.findById(userOne._id);
|
||||||
|
const isPasswordMatch = await bcrypt.compare('password2', dbUser.password);
|
||||||
|
expect(isPasswordMatch).toBe(true);
|
||||||
|
|
||||||
|
const dbResetPasswordTokenCount = await Token.countDocuments({ user: userOne._id, type: tokenTypes.RESET_PASSWORD });
|
||||||
|
expect(dbResetPasswordTokenCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if reset password token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/reset-password').send({ password: 'password2' }).expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if reset password token is blacklisted', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
||||||
|
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'password2' })
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if reset password token is expired', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().subtract(1, 'minutes');
|
||||||
|
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'password2' })
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if user is not found', async () => {
|
||||||
|
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
||||||
|
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'password2' })
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if password is missing or invalid', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
||||||
|
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/reset-password').query({ token: resetPasswordToken }).expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'short1' })
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: 'password' })
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/reset-password')
|
||||||
|
.query({ token: resetPasswordToken })
|
||||||
|
.send({ password: '11111111' })
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/send-verification-email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 204 and send verification email to the user', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const sendVerificationEmailSpy = jest.spyOn(emailService, 'sendVerificationEmail');
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/send-verification-email')
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
expect(sendVerificationEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
|
||||||
|
const verifyEmailToken = sendVerificationEmailSpy.mock.calls[0][1];
|
||||||
|
const dbVerifyEmailToken = await Token.findOne({ token: verifyEmailToken, user: userOne._id });
|
||||||
|
|
||||||
|
expect(dbVerifyEmailToken).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if access token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/send-verification-email').send().expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /v1/auth/verify-email', () => {
|
||||||
|
test('should return 204 and verify the email', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
|
||||||
|
const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
|
||||||
|
await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/verify-email')
|
||||||
|
.query({ token: verifyEmailToken })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
const dbUser = await User.findById(userOne._id);
|
||||||
|
|
||||||
|
expect(dbUser.isEmailVerified).toBe(true);
|
||||||
|
|
||||||
|
const dbVerifyEmailToken = await Token.countDocuments({
|
||||||
|
user: userOne._id,
|
||||||
|
type: tokenTypes.VERIFY_EMAIL,
|
||||||
|
});
|
||||||
|
expect(dbVerifyEmailToken).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if verify email token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).post('/v1/auth/verify-email').send().expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if verify email token is blacklisted', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
|
||||||
|
const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
|
||||||
|
await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL, true);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/verify-email')
|
||||||
|
.query({ token: verifyEmailToken })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if verify email token is expired', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().subtract(1, 'minutes');
|
||||||
|
const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
|
||||||
|
await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/verify-email')
|
||||||
|
.query({ token: verifyEmailToken })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if user is not found', async () => {
|
||||||
|
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
|
||||||
|
const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
|
||||||
|
await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/auth/verify-email')
|
||||||
|
.query({ token: verifyEmailToken })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Auth middleware', () => {
|
||||||
|
test('should call next with no errors if access token is valid', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith();
|
||||||
|
expect(req.user._id).toEqual(userOne._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if access token is not found in header', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const req = httpMocks.createRequest();
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if access token is not a valid jwt token', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: 'Bearer randomToken' } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if the token is not an access token', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
||||||
|
const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${refreshToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if access token is generated with an invalid secret', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
||||||
|
const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS, 'invalidSecret');
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if access token is expired', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const expires = moment().subtract(1, 'minutes');
|
||||||
|
const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS);
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with unauthorized error if user is not found', async () => {
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth()(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with forbidden error if user does not have required rights and userId is not in params', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth('anyRight')(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.objectContaining({ statusCode: httpStatus.FORBIDDEN, message: 'Forbidden' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with no errors if user does not have required rights but userId is in params', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
headers: { Authorization: `Bearer ${userOneAccessToken}` },
|
||||||
|
params: { userId: userOne._id.toHexString() },
|
||||||
|
});
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth('anyRight')(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call next with no errors if user has required rights', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
headers: { Authorization: `Bearer ${adminAccessToken}` },
|
||||||
|
params: { userId: userOne._id.toHexString() },
|
||||||
|
});
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth(...roleRights.get('admin'))(req, httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
const request = require('supertest');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const app = require('../../src/app');
|
||||||
|
const config = require('../../src/config/config');
|
||||||
|
|
||||||
|
describe('Auth routes', () => {
|
||||||
|
describe('GET /v1/docs', () => {
|
||||||
|
test('should return 404 when running in production', async () => {
|
||||||
|
config.env = 'production';
|
||||||
|
await request(app).get('/v1/docs').send().expect(httpStatus.NOT_FOUND);
|
||||||
|
config.env = process.env.NODE_ENV;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,625 @@
|
||||||
|
const request = require('supertest');
|
||||||
|
const faker = require('faker');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const app = require('../../src/app');
|
||||||
|
const setupTestDB = require('../utils/setupTestDB');
|
||||||
|
const { User } = require('../../src/models');
|
||||||
|
const { userOne, userTwo, admin, insertUsers } = require('../fixtures/user.fixture');
|
||||||
|
const { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture');
|
||||||
|
|
||||||
|
setupTestDB();
|
||||||
|
|
||||||
|
describe('User routes', () => {
|
||||||
|
describe('POST /v1/users', () => {
|
||||||
|
let newUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
newUser = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: 'password1',
|
||||||
|
role: 'user',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 201 and successfully create new user if data is ok', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.CREATED);
|
||||||
|
|
||||||
|
expect(res.body).not.toHaveProperty('password');
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
id: expect.anything(),
|
||||||
|
name: newUser.name,
|
||||||
|
email: newUser.email,
|
||||||
|
role: newUser.role,
|
||||||
|
isEmailVerified: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbUser = await User.findById(res.body.id);
|
||||||
|
expect(dbUser).toBeDefined();
|
||||||
|
expect(dbUser.password).not.toBe(newUser.password);
|
||||||
|
expect(dbUser).toMatchObject({ name: newUser.name, email: newUser.email, role: newUser.role, isEmailVerified: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able to create an admin as well', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
newUser.role = 'admin';
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.CREATED);
|
||||||
|
|
||||||
|
expect(res.body.role).toBe('admin');
|
||||||
|
|
||||||
|
const dbUser = await User.findById(res.body.id);
|
||||||
|
expect(dbUser.role).toBe('admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if access token is missing', async () => {
|
||||||
|
await request(app).post('/v1/users').send(newUser).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 403 error if logged in user is not admin', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if email is invalid', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
newUser.email = 'invalidEmail';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if email is already used', async () => {
|
||||||
|
await insertUsers([admin, userOne]);
|
||||||
|
newUser.email = userOne.email;
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if password length is less than 8 characters', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
newUser.password = 'passwo1';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if password does not contain both letters and numbers', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
newUser.password = 'password';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
newUser.password = '1111111';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if role is neither user nor admin', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
newUser.role = 'invalid';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(newUser)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /v1/users', () => {
|
||||||
|
test('should return 200 and apply the default query options', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(3);
|
||||||
|
expect(res.body.results[0]).toEqual({
|
||||||
|
id: userOne._id.toHexString(),
|
||||||
|
name: userOne.name,
|
||||||
|
email: userOne.email,
|
||||||
|
role: userOne.role,
|
||||||
|
isEmailVerified: userOne.isEmailVerified,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if access token is missing', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
await request(app).get('/v1/users').send().expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 403 if a non-admin is trying to access all users', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly apply filter on name field', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ name: userOne.name })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 1,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(1);
|
||||||
|
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly apply filter on role field', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ role: 'user' })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 2,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(2);
|
||||||
|
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
|
||||||
|
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly sort the returned array if descending sort param is specified', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ sortBy: 'role:desc' })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(3);
|
||||||
|
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
|
||||||
|
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
|
||||||
|
expect(res.body.results[2].id).toBe(admin._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly sort the returned array if ascending sort param is specified', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ sortBy: 'role:asc' })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(3);
|
||||||
|
expect(res.body.results[0].id).toBe(admin._id.toHexString());
|
||||||
|
expect(res.body.results[1].id).toBe(userOne._id.toHexString());
|
||||||
|
expect(res.body.results[2].id).toBe(userTwo._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly sort the returned array if multiple sorting criteria are specified', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ sortBy: 'role:desc,name:asc' })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(3);
|
||||||
|
|
||||||
|
const expectedOrder = [userOne, userTwo, admin].sort((a, b) => {
|
||||||
|
if (a.role < b.role) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.role > b.role) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return a.name < b.name ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
expectedOrder.forEach((user, index) => {
|
||||||
|
expect(res.body.results[index].id).toBe(user._id.toHexString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should limit returned array if limit param is specified', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ limit: 2 })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 1,
|
||||||
|
limit: 2,
|
||||||
|
totalPages: 2,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(2);
|
||||||
|
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
|
||||||
|
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the correct page if page and limit params are specified', async () => {
|
||||||
|
await insertUsers([userOne, userTwo, admin]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/v1/users')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.query({ page: 2, limit: 2 })
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
results: expect.any(Array),
|
||||||
|
page: 2,
|
||||||
|
limit: 2,
|
||||||
|
totalPages: 2,
|
||||||
|
totalResults: 3,
|
||||||
|
});
|
||||||
|
expect(res.body.results).toHaveLength(1);
|
||||||
|
expect(res.body.results[0].id).toBe(admin._id.toHexString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /v1/users/:userId', () => {
|
||||||
|
test('should return 200 and the user object if data is ok', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).not.toHaveProperty('password');
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
id: userOne._id.toHexString(),
|
||||||
|
email: userOne.email,
|
||||||
|
name: userOne.name,
|
||||||
|
role: userOne.role,
|
||||||
|
isEmailVerified: userOne.isEmailVerified,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if access token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).get(`/v1/users/${userOne._id}`).send().expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 403 error if user is trying to get another user', async () => {
|
||||||
|
await insertUsers([userOne, userTwo]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/v1/users/${userTwo._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 200 and the user object if admin is trying to get another user', async () => {
|
||||||
|
await insertUsers([userOne, admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if userId is not a valid mongo id', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/v1/users/invalidId')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 error if user is not found', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /v1/users/:userId', () => {
|
||||||
|
test('should return 204 if data is ok', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
const dbUser = await User.findById(userOne._id);
|
||||||
|
expect(dbUser).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if access token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
|
||||||
|
await request(app).delete(`/v1/users/${userOne._id}`).send().expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 403 error if user is trying to delete another user', async () => {
|
||||||
|
await insertUsers([userOne, userTwo]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete(`/v1/users/${userTwo._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 204 if admin is trying to delete another user', async () => {
|
||||||
|
await insertUsers([userOne, admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.NO_CONTENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if userId is not a valid mongo id', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete('/v1/users/invalidId')
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 error if user already is not found', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send()
|
||||||
|
.expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /v1/users/:userId', () => {
|
||||||
|
test('should return 200 and successfully update user if data is ok', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: 'newPassword1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
|
||||||
|
expect(res.body).not.toHaveProperty('password');
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
id: userOne._id.toHexString(),
|
||||||
|
name: updateBody.name,
|
||||||
|
email: updateBody.email,
|
||||||
|
role: 'user',
|
||||||
|
isEmailVerified: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbUser = await User.findById(userOne._id);
|
||||||
|
expect(dbUser).toBeDefined();
|
||||||
|
expect(dbUser.password).not.toBe(updateBody.password);
|
||||||
|
expect(dbUser).toMatchObject({ name: updateBody.name, email: updateBody.email, role: 'user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 error if access token is missing', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = { name: faker.name.findName() };
|
||||||
|
|
||||||
|
await request(app).patch(`/v1/users/${userOne._id}`).send(updateBody).expect(httpStatus.UNAUTHORIZED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 403 if user is updating another user', async () => {
|
||||||
|
await insertUsers([userOne, userTwo]);
|
||||||
|
const updateBody = { name: faker.name.findName() };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userTwo._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 200 and successfully update user if admin is updating another user', async () => {
|
||||||
|
await insertUsers([userOne, admin]);
|
||||||
|
const updateBody = { name: faker.name.findName() };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 if admin is updating another user that is not found', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
const updateBody = { name: faker.name.findName() };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 error if userId is not a valid mongo id', async () => {
|
||||||
|
await insertUsers([admin]);
|
||||||
|
const updateBody = { name: faker.name.findName() };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/invalidId`)
|
||||||
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if email is invalid', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = { email: 'invalidEmail' };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if email is already taken', async () => {
|
||||||
|
await insertUsers([userOne, userTwo]);
|
||||||
|
const updateBody = { email: userTwo.email };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not return 400 if email is my email', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = { email: userOne.email };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if password length is less than 8 characters', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = { password: 'passwo1' };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 400 if password does not contain both letters and numbers', async () => {
|
||||||
|
await insertUsers([userOne]);
|
||||||
|
const updateBody = { password: 'password' };
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
updateBody.password = '11111111';
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/v1/users/${userOne._id}`)
|
||||||
|
.set('Authorization', `Bearer ${userOneAccessToken}`)
|
||||||
|
.send(updateBody)
|
||||||
|
.expect(httpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,168 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const httpStatus = require('http-status');
|
||||||
|
const httpMocks = require('node-mocks-http');
|
||||||
|
const { errorConverter, errorHandler } = require('../../../src/middlewares/error');
|
||||||
|
const ApiError = require('../../../src/utils/ApiError');
|
||||||
|
const config = require('../../../src/config/config');
|
||||||
|
const logger = require('../../../src/config/logger');
|
||||||
|
|
||||||
|
describe('Error middlewares', () => {
|
||||||
|
describe('Error converter', () => {
|
||||||
|
test('should return the same ApiError object it was called with', () => {
|
||||||
|
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert an Error to ApiError and preserve its status and message', () => {
|
||||||
|
const error = new Error('Any error');
|
||||||
|
error.statusCode = httpStatus.BAD_REQUEST;
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
message: error.message,
|
||||||
|
isOperational: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert an Error without status to ApiError with status 500', () => {
|
||||||
|
const error = new Error('Any error');
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
statusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
message: error.message,
|
||||||
|
isOperational: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert an Error without message to ApiError with default message of that http status', () => {
|
||||||
|
const error = new Error();
|
||||||
|
error.statusCode = httpStatus.BAD_REQUEST;
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
message: httpStatus[error.statusCode],
|
||||||
|
isOperational: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert a Mongoose error to ApiError with status 400 and preserve its message', () => {
|
||||||
|
const error = new mongoose.Error('Any mongoose error');
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
statusCode: httpStatus.BAD_REQUEST,
|
||||||
|
message: error.message,
|
||||||
|
isOperational: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert any other object to ApiError with status 500 and its message', () => {
|
||||||
|
const error = {};
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||||
|
expect(next).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
statusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
||||||
|
isOperational: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handler', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send proper error response and put the error message in res.locals', () => {
|
||||||
|
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||||
|
const res = httpMocks.createResponse();
|
||||||
|
const sendSpy = jest.spyOn(res, 'send');
|
||||||
|
|
||||||
|
errorHandler(error, httpMocks.createRequest(), res);
|
||||||
|
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ code: error.statusCode, message: error.message }));
|
||||||
|
expect(res.locals.errorMessage).toBe(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should put the error stack in the response if in development mode', () => {
|
||||||
|
config.env = 'development';
|
||||||
|
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||||
|
const res = httpMocks.createResponse();
|
||||||
|
const sendSpy = jest.spyOn(res, 'send');
|
||||||
|
|
||||||
|
errorHandler(error, httpMocks.createRequest(), res);
|
||||||
|
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ code: error.statusCode, message: error.message, stack: error.stack })
|
||||||
|
);
|
||||||
|
config.env = process.env.NODE_ENV;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send internal server error status and message if in production mode and error is not operational', () => {
|
||||||
|
config.env = 'production';
|
||||||
|
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error', false);
|
||||||
|
const res = httpMocks.createResponse();
|
||||||
|
const sendSpy = jest.spyOn(res, 'send');
|
||||||
|
|
||||||
|
errorHandler(error, httpMocks.createRequest(), res);
|
||||||
|
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.locals.errorMessage).toBe(error.message);
|
||||||
|
config.env = process.env.NODE_ENV;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should preserve original error status and message if in production mode and error is operational', () => {
|
||||||
|
config.env = 'production';
|
||||||
|
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||||
|
const res = httpMocks.createResponse();
|
||||||
|
const sendSpy = jest.spyOn(res, 'send');
|
||||||
|
|
||||||
|
errorHandler(error, httpMocks.createRequest(), res);
|
||||||
|
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: error.statusCode,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
config.env = process.env.NODE_ENV;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const setupTestDB = require('../../../utils/setupTestDB');
|
||||||
|
const paginate = require('../../../../src/models/plugins/paginate.plugin');
|
||||||
|
|
||||||
|
const projectSchema = mongoose.Schema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
projectSchema.virtual('tasks', {
|
||||||
|
ref: 'Task',
|
||||||
|
localField: '_id',
|
||||||
|
foreignField: 'project',
|
||||||
|
});
|
||||||
|
|
||||||
|
projectSchema.plugin(paginate);
|
||||||
|
const Project = mongoose.model('Project', projectSchema);
|
||||||
|
|
||||||
|
const taskSchema = mongoose.Schema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
type: mongoose.SchemaTypes.ObjectId,
|
||||||
|
ref: 'Project',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskSchema.plugin(paginate);
|
||||||
|
const Task = mongoose.model('Task', taskSchema);
|
||||||
|
|
||||||
|
setupTestDB();
|
||||||
|
|
||||||
|
describe('paginate plugin', () => {
|
||||||
|
describe('populate option', () => {
|
||||||
|
test('should populate the specified data fields', async () => {
|
||||||
|
const project = await Project.create({ name: 'Project One' });
|
||||||
|
const task = await Task.create({ name: 'Task One', project: project._id });
|
||||||
|
|
||||||
|
const taskPages = await Task.paginate({ _id: task._id }, { populate: 'project' });
|
||||||
|
|
||||||
|
expect(taskPages.results[0].project).toHaveProperty('_id', project._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should populate nested fields', async () => {
|
||||||
|
const project = await Project.create({ name: 'Project One' });
|
||||||
|
const task = await Task.create({ name: 'Task One', project: project._id });
|
||||||
|
|
||||||
|
const projectPages = await Project.paginate({ _id: project._id }, { populate: 'tasks.project' });
|
||||||
|
const { tasks } = projectPages.results[0];
|
||||||
|
|
||||||
|
expect(tasks).toHaveLength(1);
|
||||||
|
expect(tasks[0]).toHaveProperty('_id', task._id);
|
||||||
|
expect(tasks[0].project).toHaveProperty('_id', project._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { toJSON } = require('../../../../src/models/plugins');
|
||||||
|
|
||||||
|
describe('toJSON plugin', () => {
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
connection = mongoose.createConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace _id with id', () => {
|
||||||
|
const schema = mongoose.Schema();
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model();
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('_id');
|
||||||
|
expect(doc.toJSON()).toHaveProperty('id', doc._id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove __v', () => {
|
||||||
|
const schema = mongoose.Schema();
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model();
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('__v');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove createdAt and updatedAt', () => {
|
||||||
|
const schema = mongoose.Schema({}, { timestamps: true });
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model();
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('createdAt');
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('updatedAt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove any path set as private', () => {
|
||||||
|
const schema = mongoose.Schema({
|
||||||
|
public: { type: String },
|
||||||
|
private: { type: String, private: true },
|
||||||
|
});
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model({ public: 'some public value', private: 'some private value' });
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('private');
|
||||||
|
expect(doc.toJSON()).toHaveProperty('public');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove any nested paths set as private', () => {
|
||||||
|
const schema = mongoose.Schema({
|
||||||
|
public: { type: String },
|
||||||
|
nested: {
|
||||||
|
private: { type: String, private: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model({
|
||||||
|
public: 'some public value',
|
||||||
|
nested: {
|
||||||
|
private: 'some nested private value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('nested.private');
|
||||||
|
expect(doc.toJSON()).toHaveProperty('public');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should also call the schema toJSON transform function', () => {
|
||||||
|
const schema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
public: { type: String },
|
||||||
|
private: { type: String },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toJSON: {
|
||||||
|
transform: (doc, ret) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
delete ret.private;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
schema.plugin(toJSON);
|
||||||
|
const Model = connection.model('Model', schema);
|
||||||
|
const doc = new Model({ public: 'some public value', private: 'some private value' });
|
||||||
|
expect(doc.toJSON()).not.toHaveProperty('private');
|
||||||
|
expect(doc.toJSON()).toHaveProperty('public');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
const faker = require('faker');
|
||||||
|
const { User } = require('../../../src/models');
|
||||||
|
|
||||||
|
describe('User model', () => {
|
||||||
|
describe('User validation', () => {
|
||||||
|
let newUser;
|
||||||
|
beforeEach(() => {
|
||||||
|
newUser = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: 'password1',
|
||||||
|
role: 'user',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly validate a valid user', async () => {
|
||||||
|
await expect(new User(newUser).validate()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a validation error if email is invalid', async () => {
|
||||||
|
newUser.email = 'invalidEmail';
|
||||||
|
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a validation error if password length is less than 8 characters', async () => {
|
||||||
|
newUser.password = 'passwo1';
|
||||||
|
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a validation error if password does not contain numbers', async () => {
|
||||||
|
newUser.password = 'password';
|
||||||
|
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a validation error if password does not contain letters', async () => {
|
||||||
|
newUser.password = '11111111';
|
||||||
|
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a validation error if role is unknown', async () => {
|
||||||
|
newUser.role = 'invalid';
|
||||||
|
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User toJSON()', () => {
|
||||||
|
test('should not return user password when toJSON is called', () => {
|
||||||
|
const newUser = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: 'password1',
|
||||||
|
role: 'user',
|
||||||
|
};
|
||||||
|
expect(new User(newUser).toJSON()).not.toHaveProperty('password');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const config = require('../../src/config/config');
|
||||||
|
|
||||||
|
const setupTestDB = () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await mongoose.connect(config.mongoose.url, config.mongoose.options);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all(Object.values(mongoose.connection.collections).map(async (collection) => collection.deleteMany()));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await mongoose.disconnect();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = setupTestDB;
|
Loading…
Reference in New Issue