-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1de1d8e
commit ef73bd4
Showing
17 changed files
with
1,349 additions
and
1,009 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,2 @@ | ||
node_modules | ||
bin | ||
|
||
tsconfig.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = { | ||
root: true, | ||
parser: '@typescript-eslint/parser', | ||
plugins: ['@typescript-eslint'], | ||
extends: ['airbnb-typescript/base'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import express from 'express'; | ||
import helmet from 'helmet'; | ||
import xss from 'xss-clean'; | ||
import ExpressMongoSanitize from 'express-mongo-sanitize'; | ||
import compression from 'compression'; | ||
import cors from 'cors'; | ||
import passport from 'passport'; | ||
import cookieParser from 'cookie-parser'; | ||
import config from './config/config'; | ||
import { successHandler, errorHandler } from './config/morgan'; | ||
import jwtStrategy from './config/passport'; | ||
|
||
const app = express(); | ||
|
||
if (config.env !== 'test') { | ||
app.use(successHandler); | ||
app.use(errorHandler); | ||
} | ||
|
||
// set security HTTP headers | ||
app.use(helmet()); | ||
|
||
// use cookie parser for jwt | ||
app.use(cookieParser()); | ||
|
||
// enable cors | ||
app.use(cors()); | ||
app.options('*', cors()); | ||
|
||
// 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(ExpressMongoSanitize()); | ||
|
||
// gzip compression | ||
app.use(compression()); | ||
|
||
// jwt authentication | ||
app.use(passport.initialize()); | ||
passport.use('jwt', jwtStrategy); | ||
|
||
export default app; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import Joi from 'joi'; | ||
import path from 'path'; | ||
import dotenv from 'dotenv'; | ||
|
||
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}`); | ||
} | ||
|
||
export default { | ||
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, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import winston from 'winston'; | ||
import config from './config'; | ||
|
||
interface LoggingInfo { | ||
level: string; | ||
message: string; | ||
} | ||
|
||
const enumerateErrorFormat = winston.format((info: LoggingInfo) => { | ||
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((info: LoggingInfo) => `${info.level}: ${info.message}`) | ||
), | ||
transports: [ | ||
new winston.transports.Console({ | ||
stderrLevels: ['error'], | ||
}), | ||
], | ||
}); | ||
|
||
export default logger; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import morgan from 'morgan'; | ||
import { Request, Response } from 'express'; | ||
import config from './config'; | ||
import logger from './logger'; | ||
|
||
morgan.token('message', (req: Request, res: Response) => 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`; | ||
|
||
export const successHandler = morgan(successResponseFormat, { | ||
skip: (req: Request, res: Response) => res.statusCode >= 400, | ||
stream: { write: (message: string) => logger.info(message.trim()) }, | ||
}); | ||
|
||
export const errorHandler = morgan(errorResponseFormat, { | ||
skip: (req: Request, res: Response) => res.statusCode < 400, | ||
stream: { write: (message: string) => logger.error(message.trim()) }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Request } from 'express'; | ||
import { Strategy as JwtStrategy } from 'passport-jwt'; | ||
import tokenTypes from './tokens'; | ||
import config from './config'; | ||
|
||
const { User } = require('../models'); | ||
|
||
interface Payload { | ||
sub: string; | ||
iat: number; | ||
exp: number; | ||
type: string; | ||
} | ||
|
||
const cookieExtractor = function (req: Request): string { | ||
let token = null; | ||
if (req && req.cookies) { | ||
token = req.cookies.jwt; | ||
} | ||
return token; | ||
}; | ||
|
||
const jwtOptions = { | ||
secretOrKey: config.jwt.secret, | ||
jwtFromRequest: cookieExtractor, | ||
}; | ||
|
||
const jwtVerify = async (payload: Payload, done: any) => { | ||
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); | ||
|
||
export default jwtStrategy; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
const allRoles = { | ||
user: [], | ||
admin: ['getUsers', 'manageUsers'], | ||
}; | ||
|
||
const roles = Object.keys(allRoles); | ||
const roleRights = new Map(Object.entries(allRoles)); | ||
|
||
export default { | ||
roles, | ||
roleRights, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default { | ||
ACCESS: 'access', | ||
REFRESH: 'refresh', | ||
RESET_PASSWORD: 'resetPassword', | ||
VERIFY_EMAIL: 'verifyEmail', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
declare module 'xss-clean'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import mongoose, { ConnectOptions } from 'mongoose'; | ||
import app from './app'; | ||
import config from './config/config'; | ||
import logger from './config/logger'; | ||
|
||
let server: any; | ||
mongoose.connect(config.mongoose.url, config.mongoose.options as ConnectOptions).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: string) => { | ||
logger.error(error); | ||
exitHandler(); | ||
}; | ||
|
||
process.on('uncaughtException', unexpectedErrorHandler); | ||
process.on('unhandledRejection', unexpectedErrorHandler); | ||
|
||
process.on('SIGTERM', () => { | ||
logger.info('SIGTERM received'); | ||
if (server) { | ||
server.close(); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
class ApiError extends Error { | ||
statusCode: number; | ||
|
||
isOperational: boolean; | ||
|
||
override stack?: string; | ||
|
||
constructor(statusCode: number, message: string, isOperational = true, stack = '') { | ||
super(message); | ||
this.statusCode = statusCode; | ||
this.isOperational = isOperational; | ||
if (stack) { | ||
this.stack = stack; | ||
} else { | ||
Error.captureStackTrace(this, this.constructor); | ||
} | ||
} | ||
} | ||
|
||
export default ApiError; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Request, Response, NextFunction } from 'express'; | ||
|
||
const catchAsync = (fn: any) => (req: Request, res: Response, next: NextFunction) => { | ||
Promise.resolve(fn(req, res, next)).catch((err) => next(err)); | ||
}; | ||
|
||
export default catchAsync; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* Create an object composed of the picked object properties | ||
* @param {Record<string, any>} object | ||
* @param {string[]} keys | ||
* @returns {Object} | ||
*/ | ||
const pick = (object: Record<string, any>, keys: string[]) => { | ||
return keys.reduce((obj: any, key: string) => { | ||
if (object && Object.prototype.hasOwnProperty.call(object, key)) { | ||
// eslint-disable-next-line no-param-reassign | ||
obj[key] = object[key]; | ||
} | ||
return obj; | ||
}, {}); | ||
}; | ||
|
||
export default pick; |
Oops, something went wrong.