nodejs-express-starter 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +3 -0
- package/.editorconfig +9 -0
- package/.env.example +22 -0
- package/.eslintignore +2 -0
- package/.eslintrc.json +32 -0
- package/.gitignore +14 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +4 -0
- package/Dockerfile +15 -0
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/bin/createNodejsApp.js +105 -0
- package/docker-compose.dev.yml +4 -0
- package/docker-compose.prod.yml +4 -0
- package/docker-compose.test.yml +4 -0
- package/docker-compose.yml +30 -0
- package/jest.config.js +9 -0
- package/package.json +117 -0
- package/src/app.js +82 -0
- package/src/config/config.js +64 -0
- package/src/config/logger.js +26 -0
- package/src/config/morgan.js +25 -0
- package/src/config/passport.js +30 -0
- package/src/config/roles.js +12 -0
- package/src/config/tokens.js +10 -0
- package/src/controllers/auth.controller.js +59 -0
- package/src/controllers/index.js +2 -0
- package/src/controllers/user.controller.js +43 -0
- package/src/docs/components.yml +92 -0
- package/src/docs/swaggerDef.js +21 -0
- package/src/index.js +57 -0
- package/src/middlewares/auth.js +33 -0
- package/src/middlewares/error.js +47 -0
- package/src/middlewares/rateLimiter.js +11 -0
- package/src/middlewares/requestId.js +14 -0
- package/src/middlewares/validate.js +21 -0
- package/src/models/index.js +2 -0
- package/src/models/plugins/index.js +2 -0
- package/src/models/plugins/paginate.plugin.js +70 -0
- package/src/models/plugins/toJSON.plugin.js +43 -0
- package/src/models/token.model.js +44 -0
- package/src/models/user.model.js +91 -0
- package/src/routes/v1/auth.route.js +291 -0
- package/src/routes/v1/docs.route.js +21 -0
- package/src/routes/v1/health.route.js +43 -0
- package/src/routes/v1/index.js +39 -0
- package/src/routes/v1/user.route.js +252 -0
- package/src/services/auth.service.js +99 -0
- package/src/services/email.service.js +63 -0
- package/src/services/index.js +4 -0
- package/src/services/token.service.js +123 -0
- package/src/services/user.service.js +89 -0
- package/src/utils/ApiError.js +14 -0
- package/src/utils/catchAsync.js +5 -0
- package/src/utils/pick.js +17 -0
- package/src/validations/auth.validation.js +60 -0
- package/src/validations/custom.validation.js +21 -0
- package/src/validations/index.js +2 -0
- package/src/validations/user.validation.js +54 -0
- package/tests/fixtures/token.fixture.js +14 -0
- package/tests/fixtures/user.fixture.js +46 -0
- package/tests/integration/auth.test.js +587 -0
- package/tests/integration/docs.test.js +14 -0
- package/tests/integration/health.test.js +32 -0
- package/tests/integration/user.test.js +625 -0
- package/tests/unit/middlewares/error.test.js +168 -0
- package/tests/unit/models/plugins/paginate.plugin.test.js +61 -0
- package/tests/unit/models/plugins/toJSON.plugin.test.js +89 -0
- package/tests/unit/models/user.model.test.js +57 -0
- package/tests/utils/setupTestDB.js +18 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
services:
|
|
2
|
+
node-app:
|
|
3
|
+
build: .
|
|
4
|
+
image: node-app
|
|
5
|
+
environment:
|
|
6
|
+
- MONGODB_URL=mongodb://mongodb:27017/node-boilerplate
|
|
7
|
+
ports:
|
|
8
|
+
- '3000:3000'
|
|
9
|
+
depends_on:
|
|
10
|
+
- mongodb
|
|
11
|
+
volumes:
|
|
12
|
+
- .:/usr/src/node-app
|
|
13
|
+
networks:
|
|
14
|
+
- node-network
|
|
15
|
+
|
|
16
|
+
mongodb:
|
|
17
|
+
image: mongo:4.2.1-bionic
|
|
18
|
+
ports:
|
|
19
|
+
- '27017:27017'
|
|
20
|
+
volumes:
|
|
21
|
+
- dbdata:/data/db
|
|
22
|
+
networks:
|
|
23
|
+
- node-network
|
|
24
|
+
|
|
25
|
+
volumes:
|
|
26
|
+
dbdata:
|
|
27
|
+
|
|
28
|
+
networks:
|
|
29
|
+
node-network:
|
|
30
|
+
driver: bridge
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
testEnvironmentOptions: {
|
|
4
|
+
NODE_ENV: 'test',
|
|
5
|
+
},
|
|
6
|
+
restoreMocks: true,
|
|
7
|
+
coveragePathIgnorePatterns: ['node_modules', 'src/config', 'src/app.js', 'tests'],
|
|
8
|
+
coverageReporters: ['text', 'lcov', 'clover', 'html'],
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodejs-express-starter",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Create a Node.js app for building production-ready RESTful APIs using Express, by running one command",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nodejs-express-starter": "./bin/createNodejsApp.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"repository": "https://github.com/Ahlyab/express-backend-starter-js.git",
|
|
10
|
+
"author": "Ahlyab <ahalyabasad@gmail.com>",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"tests",
|
|
16
|
+
".env.example",
|
|
17
|
+
".gitignore",
|
|
18
|
+
".eslintrc.json",
|
|
19
|
+
".eslintignore",
|
|
20
|
+
".prettierrc.json",
|
|
21
|
+
".prettierignore",
|
|
22
|
+
".editorconfig",
|
|
23
|
+
"docker-compose.yml",
|
|
24
|
+
"docker-compose.dev.yml",
|
|
25
|
+
"docker-compose.prod.yml",
|
|
26
|
+
"docker-compose.test.yml",
|
|
27
|
+
"Dockerfile",
|
|
28
|
+
".dockerignore",
|
|
29
|
+
"jest.config.js",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"start": "node src/index.js",
|
|
38
|
+
"dev": "cross-env NODE_ENV=development nodemon src/index.js",
|
|
39
|
+
"test": "cross-env NODE_ENV=test jest -i --colors --verbose --detectOpenHandles",
|
|
40
|
+
"test:watch": "cross-env NODE_ENV=test jest -i --watchAll",
|
|
41
|
+
"coverage": "cross-env NODE_ENV=test jest -i --coverage",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"lint:fix": "eslint . --fix",
|
|
44
|
+
"prettier": "npx prettier --check \"**/*.js\"",
|
|
45
|
+
"prettier:fix": "npx prettier --write \"**/*.js\"",
|
|
46
|
+
"docker:prod": "docker compose -f docker-compose.yml -f docker-compose.prod.yml up --build",
|
|
47
|
+
"docker:prod:down": "docker compose -f docker-compose.yml -f docker-compose.prod.yml down",
|
|
48
|
+
"docker:dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build",
|
|
49
|
+
"docker:dev:down": "docker compose -f docker-compose.yml -f docker-compose.dev.yml down",
|
|
50
|
+
"docker:test": "docker compose -f docker-compose.yml -f docker-compose.test.yml up --build",
|
|
51
|
+
"docker:test:down": "docker compose -f docker-compose.yml -f docker-compose.test.yml down",
|
|
52
|
+
"prepare": "husky"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"node",
|
|
56
|
+
"node.js",
|
|
57
|
+
"boilerplate",
|
|
58
|
+
"generator",
|
|
59
|
+
"express",
|
|
60
|
+
"rest",
|
|
61
|
+
"api",
|
|
62
|
+
"mongodb",
|
|
63
|
+
"mongoose",
|
|
64
|
+
"es6",
|
|
65
|
+
"es7",
|
|
66
|
+
"es8",
|
|
67
|
+
"es9",
|
|
68
|
+
"jest",
|
|
69
|
+
"travis",
|
|
70
|
+
"docker",
|
|
71
|
+
"passport",
|
|
72
|
+
"joi",
|
|
73
|
+
"eslint",
|
|
74
|
+
"prettier"
|
|
75
|
+
],
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"bcryptjs": "^2.4.3",
|
|
78
|
+
"compression": "^1.7.4",
|
|
79
|
+
"cors": "^2.8.5",
|
|
80
|
+
"cross-env": "^7.0.0",
|
|
81
|
+
"dotenv": "^16.4.5",
|
|
82
|
+
"express": "^4.21.0",
|
|
83
|
+
"express-mongo-sanitize": "^2.2.0",
|
|
84
|
+
"express-rate-limit": "^7.4.0",
|
|
85
|
+
"helmet": "^7.1.0",
|
|
86
|
+
"http-status": "^1.7.4",
|
|
87
|
+
"joi": "^17.13.3",
|
|
88
|
+
"jsonwebtoken": "^9.0.2",
|
|
89
|
+
"moment": "^2.30.1",
|
|
90
|
+
"mongoose": "^6.13.0",
|
|
91
|
+
"morgan": "^1.10.0",
|
|
92
|
+
"nodemailer": "^8.0.2",
|
|
93
|
+
"passport": "^0.7.0",
|
|
94
|
+
"passport-jwt": "^4.0.1",
|
|
95
|
+
"swagger-jsdoc": "^6.2.8",
|
|
96
|
+
"swagger-ui-express": "^5.0.0",
|
|
97
|
+
"validator": "^13.12.0",
|
|
98
|
+
"winston": "^3.14.2"
|
|
99
|
+
},
|
|
100
|
+
"devDependencies": {
|
|
101
|
+
"@faker-js/faker": "^9.2.0",
|
|
102
|
+
"eslint": "^8.57.0",
|
|
103
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
104
|
+
"eslint-config-prettier": "^9.1.0",
|
|
105
|
+
"eslint-plugin-import": "^2.29.1",
|
|
106
|
+
"eslint-plugin-jest": "^27.9.0",
|
|
107
|
+
"eslint-plugin-prettier": "^5.2.0",
|
|
108
|
+
"eslint-plugin-security": "1.7.1",
|
|
109
|
+
"husky": "^9.0.11",
|
|
110
|
+
"jest": "^29.7.0",
|
|
111
|
+
"lint-staged": "^15.2.10",
|
|
112
|
+
"node-mocks-http": "^1.14.0",
|
|
113
|
+
"nodemon": "^3.1.4",
|
|
114
|
+
"prettier": "^3.3.3",
|
|
115
|
+
"supertest": "^7.0.0"
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const helmet = require('helmet');
|
|
3
|
+
const mongoSanitize = require('express-mongo-sanitize');
|
|
4
|
+
const compression = require('compression');
|
|
5
|
+
const cors = require('cors');
|
|
6
|
+
const passport = require('passport');
|
|
7
|
+
const httpStatus = require('http-status');
|
|
8
|
+
const config = require('./config/config');
|
|
9
|
+
const morgan = require('./config/morgan');
|
|
10
|
+
const { jwtStrategy } = require('./config/passport');
|
|
11
|
+
const { authLimiter } = require('./middlewares/rateLimiter');
|
|
12
|
+
const routes = require('./routes/v1');
|
|
13
|
+
const healthRoute = require('./routes/v1/health.route');
|
|
14
|
+
const { errorConverter, errorHandler } = require('./middlewares/error');
|
|
15
|
+
const ApiError = require('./utils/ApiError');
|
|
16
|
+
const requestId = require('./middlewares/requestId');
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
|
|
20
|
+
// request ID for tracing (must be early)
|
|
21
|
+
app.use(requestId);
|
|
22
|
+
|
|
23
|
+
// health check endpoints (before auth, for load balancers/orchestration)
|
|
24
|
+
app.use('/health', healthRoute);
|
|
25
|
+
|
|
26
|
+
if (config.env !== 'test') {
|
|
27
|
+
app.use(morgan.successHandler);
|
|
28
|
+
app.use(morgan.errorHandler);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// set security HTTP headers
|
|
32
|
+
app.use(
|
|
33
|
+
helmet({
|
|
34
|
+
contentSecurityPolicy: config.env === 'production',
|
|
35
|
+
crossOriginEmbedderPolicy: false,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// parse json request body
|
|
40
|
+
app.use(express.json({ limit: '10kb' }));
|
|
41
|
+
|
|
42
|
+
// parse urlencoded request body
|
|
43
|
+
app.use(express.urlencoded({ extended: true }));
|
|
44
|
+
|
|
45
|
+
// sanitize request data
|
|
46
|
+
app.use(mongoSanitize());
|
|
47
|
+
|
|
48
|
+
// gzip compression
|
|
49
|
+
app.use(compression());
|
|
50
|
+
|
|
51
|
+
// enable cors
|
|
52
|
+
const corsOptions = {
|
|
53
|
+
origin: config.corsOrigin === '*' ? true : config.corsOrigin,
|
|
54
|
+
credentials: true,
|
|
55
|
+
};
|
|
56
|
+
app.use(cors(corsOptions));
|
|
57
|
+
app.options('*', cors(corsOptions));
|
|
58
|
+
|
|
59
|
+
// jwt authentication
|
|
60
|
+
app.use(passport.initialize());
|
|
61
|
+
passport.use('jwt', jwtStrategy);
|
|
62
|
+
|
|
63
|
+
// limit repeated failed requests to auth endpoints
|
|
64
|
+
if (config.env === 'production') {
|
|
65
|
+
app.use('/v1/auth', authLimiter);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// v1 api routes
|
|
69
|
+
app.use('/v1', routes);
|
|
70
|
+
|
|
71
|
+
// send back a 404 error for any unknown api request
|
|
72
|
+
app.use((req, res, next) => {
|
|
73
|
+
next(new ApiError(httpStatus.NOT_FOUND, 'Not found'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// convert error to ApiError, if needed
|
|
77
|
+
app.use(errorConverter);
|
|
78
|
+
|
|
79
|
+
// handle error
|
|
80
|
+
app.use(errorHandler);
|
|
81
|
+
|
|
82
|
+
module.exports = app;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const dotenv = require('dotenv');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Joi = require('joi');
|
|
4
|
+
|
|
5
|
+
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
|
6
|
+
|
|
7
|
+
const envVarsSchema = Joi.object()
|
|
8
|
+
.keys({
|
|
9
|
+
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
|
|
10
|
+
PORT: Joi.number().default(3000),
|
|
11
|
+
LOG_LEVEL: Joi.string().valid('error', 'warn', 'info', 'http', 'verbose', 'debug').default('info'),
|
|
12
|
+
CORS_ORIGIN: Joi.string().allow('').default('*').description('CORS allowed origins, comma-separated'),
|
|
13
|
+
MONGODB_URL: Joi.string().required().description('Mongo DB url'),
|
|
14
|
+
JWT_SECRET: Joi.string().required().description('JWT secret key'),
|
|
15
|
+
JWT_ACCESS_EXPIRATION_MINUTES: Joi.number().default(30).description('minutes after which access tokens expire'),
|
|
16
|
+
JWT_REFRESH_EXPIRATION_DAYS: Joi.number().default(30).description('days after which refresh tokens expire'),
|
|
17
|
+
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: Joi.number()
|
|
18
|
+
.default(10)
|
|
19
|
+
.description('minutes after which reset password token expires'),
|
|
20
|
+
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()
|
|
21
|
+
.default(10)
|
|
22
|
+
.description('minutes after which verify email token expires'),
|
|
23
|
+
SMTP_HOST: Joi.string().description('server that will send the emails'),
|
|
24
|
+
SMTP_PORT: Joi.number().description('port to connect to the email server'),
|
|
25
|
+
SMTP_USERNAME: Joi.string().description('username for email server'),
|
|
26
|
+
SMTP_PASSWORD: Joi.string().description('password for email server'),
|
|
27
|
+
EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),
|
|
28
|
+
})
|
|
29
|
+
.unknown();
|
|
30
|
+
|
|
31
|
+
const { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(process.env);
|
|
32
|
+
|
|
33
|
+
if (error) {
|
|
34
|
+
throw new Error(`Config validation error: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
env: envVars.NODE_ENV,
|
|
39
|
+
port: envVars.PORT,
|
|
40
|
+
logLevel: envVars.LOG_LEVEL,
|
|
41
|
+
corsOrigin: envVars.CORS_ORIGIN && envVars.CORS_ORIGIN !== '*' ? envVars.CORS_ORIGIN.split(',').map((o) => o.trim()) : '*',
|
|
42
|
+
mongoose: {
|
|
43
|
+
url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
|
|
44
|
+
options: {},
|
|
45
|
+
},
|
|
46
|
+
jwt: {
|
|
47
|
+
secret: envVars.JWT_SECRET,
|
|
48
|
+
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
|
|
49
|
+
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
|
|
50
|
+
resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
|
|
51
|
+
verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
|
|
52
|
+
},
|
|
53
|
+
email: {
|
|
54
|
+
smtp: {
|
|
55
|
+
host: envVars.SMTP_HOST,
|
|
56
|
+
port: envVars.SMTP_PORT,
|
|
57
|
+
auth: {
|
|
58
|
+
user: envVars.SMTP_USERNAME,
|
|
59
|
+
pass: envVars.SMTP_PASSWORD,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
from: envVars.EMAIL_FROM,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const winston = require('winston');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
|
|
4
|
+
const enumerateErrorFormat = winston.format((info) => {
|
|
5
|
+
if (info instanceof Error) {
|
|
6
|
+
Object.assign(info, { message: info.stack });
|
|
7
|
+
}
|
|
8
|
+
return info;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const logger = winston.createLogger({
|
|
12
|
+
level: config.logLevel || (config.env === 'development' ? 'debug' : 'info'),
|
|
13
|
+
format: winston.format.combine(
|
|
14
|
+
enumerateErrorFormat(),
|
|
15
|
+
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
|
|
16
|
+
winston.format.splat(),
|
|
17
|
+
winston.format.printf(({ level, message }) => `${level}: ${message}`),
|
|
18
|
+
),
|
|
19
|
+
transports: [
|
|
20
|
+
new winston.transports.Console({
|
|
21
|
+
stderrLevels: ['error'],
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = logger;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const morgan = require('morgan');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
|
|
5
|
+
morgan.token('message', (req, res) => res.locals.errorMessage || '');
|
|
6
|
+
morgan.token('request-id', (req) => req.id || '-');
|
|
7
|
+
|
|
8
|
+
const getIpFormat = () => (config.env === 'production' ? ':remote-addr - ' : '');
|
|
9
|
+
const successResponseFormat = `${getIpFormat()}[:request-id] :method :url :status - :response-time ms`;
|
|
10
|
+
const errorResponseFormat = `${getIpFormat()}[:request-id] :method :url :status - :response-time ms - message: :message`;
|
|
11
|
+
|
|
12
|
+
const successHandler = morgan(successResponseFormat, {
|
|
13
|
+
skip: (req, res) => res.statusCode >= 400,
|
|
14
|
+
stream: { write: (message) => logger.info(message.trim()) },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const errorHandler = morgan(errorResponseFormat, {
|
|
18
|
+
skip: (req, res) => res.statusCode < 400,
|
|
19
|
+
stream: { write: (message) => logger.error(message.trim()) },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
successHandler,
|
|
24
|
+
errorHandler,
|
|
25
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
const { tokenTypes } = require('./tokens');
|
|
4
|
+
const { User } = require('../models');
|
|
5
|
+
|
|
6
|
+
const jwtOptions = {
|
|
7
|
+
secretOrKey: config.jwt.secret,
|
|
8
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const jwtVerify = async (payload, done) => {
|
|
12
|
+
try {
|
|
13
|
+
if (payload.type !== tokenTypes.ACCESS) {
|
|
14
|
+
throw new Error('Invalid token type');
|
|
15
|
+
}
|
|
16
|
+
const user = await User.findById(payload.sub);
|
|
17
|
+
if (!user) {
|
|
18
|
+
return done(null, false);
|
|
19
|
+
}
|
|
20
|
+
done(null, user);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
done(error, false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
jwtStrategy,
|
|
30
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const httpStatus = require('http-status');
|
|
2
|
+
const catchAsync = require('../utils/catchAsync');
|
|
3
|
+
const { authService, userService, tokenService, emailService } = require('../services');
|
|
4
|
+
|
|
5
|
+
const register = catchAsync(async (req, res) => {
|
|
6
|
+
const user = await userService.createUser(req.body);
|
|
7
|
+
const tokens = await tokenService.generateAuthTokens(user);
|
|
8
|
+
res.status(httpStatus.CREATED).send({ user, tokens });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const login = catchAsync(async (req, res) => {
|
|
12
|
+
const { email, password } = req.body;
|
|
13
|
+
const user = await authService.loginUserWithEmailAndPassword(email, password);
|
|
14
|
+
const tokens = await tokenService.generateAuthTokens(user);
|
|
15
|
+
res.send({ user, tokens });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const logout = catchAsync(async (req, res) => {
|
|
19
|
+
await authService.logout(req.body.refreshToken);
|
|
20
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const refreshTokens = catchAsync(async (req, res) => {
|
|
24
|
+
const tokens = await authService.refreshAuth(req.body.refreshToken);
|
|
25
|
+
res.send({ ...tokens });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const forgotPassword = catchAsync(async (req, res) => {
|
|
29
|
+
const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email);
|
|
30
|
+
await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken);
|
|
31
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const resetPassword = catchAsync(async (req, res) => {
|
|
35
|
+
await authService.resetPassword(req.query.token, req.body.password);
|
|
36
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const sendVerificationEmail = catchAsync(async (req, res) => {
|
|
40
|
+
const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user);
|
|
41
|
+
await emailService.sendVerificationEmail(req.user.email, verifyEmailToken);
|
|
42
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const verifyEmail = catchAsync(async (req, res) => {
|
|
46
|
+
await authService.verifyEmail(req.query.token);
|
|
47
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
register,
|
|
52
|
+
login,
|
|
53
|
+
logout,
|
|
54
|
+
refreshTokens,
|
|
55
|
+
forgotPassword,
|
|
56
|
+
resetPassword,
|
|
57
|
+
sendVerificationEmail,
|
|
58
|
+
verifyEmail,
|
|
59
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const httpStatus = require('http-status');
|
|
2
|
+
const pick = require('../utils/pick');
|
|
3
|
+
const ApiError = require('../utils/ApiError');
|
|
4
|
+
const catchAsync = require('../utils/catchAsync');
|
|
5
|
+
const { userService } = require('../services');
|
|
6
|
+
|
|
7
|
+
const createUser = catchAsync(async (req, res) => {
|
|
8
|
+
const user = await userService.createUser(req.body);
|
|
9
|
+
res.status(httpStatus.CREATED).send(user);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const getUsers = catchAsync(async (req, res) => {
|
|
13
|
+
const filter = pick(req.query, ['name', 'role']);
|
|
14
|
+
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
|
15
|
+
const result = await userService.queryUsers(filter, options);
|
|
16
|
+
res.send(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const getUser = catchAsync(async (req, res) => {
|
|
20
|
+
const user = await userService.getUserById(req.params.userId);
|
|
21
|
+
if (!user) {
|
|
22
|
+
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
|
23
|
+
}
|
|
24
|
+
res.send(user);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const updateUser = catchAsync(async (req, res) => {
|
|
28
|
+
const user = await userService.updateUserById(req.params.userId, req.body);
|
|
29
|
+
res.send(user);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const deleteUser = catchAsync(async (req, res) => {
|
|
33
|
+
await userService.deleteUserById(req.params.userId);
|
|
34
|
+
res.status(httpStatus.NO_CONTENT).send();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
createUser,
|
|
39
|
+
getUsers,
|
|
40
|
+
getUser,
|
|
41
|
+
updateUser,
|
|
42
|
+
deleteUser,
|
|
43
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
components:
|
|
2
|
+
schemas:
|
|
3
|
+
User:
|
|
4
|
+
type: object
|
|
5
|
+
properties:
|
|
6
|
+
id:
|
|
7
|
+
type: string
|
|
8
|
+
email:
|
|
9
|
+
type: string
|
|
10
|
+
format: email
|
|
11
|
+
name:
|
|
12
|
+
type: string
|
|
13
|
+
role:
|
|
14
|
+
type: string
|
|
15
|
+
enum: [user, admin]
|
|
16
|
+
example:
|
|
17
|
+
id: 5ebac534954b54139806c112
|
|
18
|
+
email: fake@example.com
|
|
19
|
+
name: fake name
|
|
20
|
+
role: user
|
|
21
|
+
|
|
22
|
+
Token:
|
|
23
|
+
type: object
|
|
24
|
+
properties:
|
|
25
|
+
token:
|
|
26
|
+
type: string
|
|
27
|
+
expires:
|
|
28
|
+
type: string
|
|
29
|
+
format: date-time
|
|
30
|
+
example:
|
|
31
|
+
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg
|
|
32
|
+
expires: 2020-05-12T16:18:04.793Z
|
|
33
|
+
|
|
34
|
+
AuthTokens:
|
|
35
|
+
type: object
|
|
36
|
+
properties:
|
|
37
|
+
access:
|
|
38
|
+
$ref: '#/components/schemas/Token'
|
|
39
|
+
refresh:
|
|
40
|
+
$ref: '#/components/schemas/Token'
|
|
41
|
+
|
|
42
|
+
Error:
|
|
43
|
+
type: object
|
|
44
|
+
properties:
|
|
45
|
+
code:
|
|
46
|
+
type: number
|
|
47
|
+
message:
|
|
48
|
+
type: string
|
|
49
|
+
|
|
50
|
+
responses:
|
|
51
|
+
DuplicateEmail:
|
|
52
|
+
description: Email already taken
|
|
53
|
+
content:
|
|
54
|
+
application/json:
|
|
55
|
+
schema:
|
|
56
|
+
$ref: '#/components/schemas/Error'
|
|
57
|
+
example:
|
|
58
|
+
code: 400
|
|
59
|
+
message: Email already taken
|
|
60
|
+
Unauthorized:
|
|
61
|
+
description: Unauthorized
|
|
62
|
+
content:
|
|
63
|
+
application/json:
|
|
64
|
+
schema:
|
|
65
|
+
$ref: '#/components/schemas/Error'
|
|
66
|
+
example:
|
|
67
|
+
code: 401
|
|
68
|
+
message: Please authenticate
|
|
69
|
+
Forbidden:
|
|
70
|
+
description: Forbidden
|
|
71
|
+
content:
|
|
72
|
+
application/json:
|
|
73
|
+
schema:
|
|
74
|
+
$ref: '#/components/schemas/Error'
|
|
75
|
+
example:
|
|
76
|
+
code: 403
|
|
77
|
+
message: Forbidden
|
|
78
|
+
NotFound:
|
|
79
|
+
description: Not found
|
|
80
|
+
content:
|
|
81
|
+
application/json:
|
|
82
|
+
schema:
|
|
83
|
+
$ref: '#/components/schemas/Error'
|
|
84
|
+
example:
|
|
85
|
+
code: 404
|
|
86
|
+
message: Not found
|
|
87
|
+
|
|
88
|
+
securitySchemes:
|
|
89
|
+
bearerAuth:
|
|
90
|
+
type: http
|
|
91
|
+
scheme: bearer
|
|
92
|
+
bearerFormat: JWT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { version } = require('../../package.json');
|
|
2
|
+
const config = require('../config/config');
|
|
3
|
+
|
|
4
|
+
const swaggerDef = {
|
|
5
|
+
openapi: '3.0.0',
|
|
6
|
+
info: {
|
|
7
|
+
title: 'Express Backend Starter JS',
|
|
8
|
+
version,
|
|
9
|
+
license: {
|
|
10
|
+
name: 'MIT',
|
|
11
|
+
url: 'https://github.com/Ahlyab/express-backend-starter-js/blob/master/LICENSE',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
servers: [
|
|
15
|
+
{
|
|
16
|
+
url: `http://localhost:${config.port}/v1`,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = swaggerDef;
|