├── .env.example ├── .gitignore ├── .sequelizerc ├── README.md ├── app.ts ├── auth ├── auth.ts ├── oauth2.ts └── roles.ts ├── config └── config.ts ├── errors ├── AuthError.ts ├── BaseError.ts ├── DatabaseError.ts ├── EmailError.ts ├── ErrorHandler.ts ├── InternalServerError.ts ├── MessageError.ts ├── NotFoundError.ts ├── RegistrationError.ts ├── TestError.ts └── ValidationError.ts ├── lib └── logger.ts ├── local-infra.yml ├── managers ├── AuthManager.ts ├── ClientManager.ts ├── EmailManager.ts ├── MessageManager.ts ├── S3Manager.ts └── UserManager.ts ├── models ├── dtos │ └── UserDTO.ts ├── entities │ ├── AccessToken.ts │ ├── AuthorizationCode.ts │ ├── BaseModel.ts │ ├── Client.ts │ └── User.ts ├── enums │ ├── MessageQueueEnum.ts │ └── RoleEnum.ts └── index.ts ├── package.json ├── routes ├── AuthRouter.ts ├── BaseRouter.ts ├── ClientRouter.ts ├── UserRouter.ts └── index.ts ├── seeders └── 20180304161652-seed-user.js ├── server.ts ├── tests ├── test-helper.ts └── users.test.ts ├── tsconfig.json ├── tslint.json ├── typings.d.ts ├── utils.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | JWT_SECRET=qfPFRkqM4ZF92h9NeZtqAArQ 2 | 3 | FACEBOOK_CLIENT_ID={your facebook client id} 4 | FACEBOOK_SECRET={your facebook secret} 5 | 6 | GOOGLE_CLIENT_ID={your google client id} 7 | GOOGLE_SECRET={your google secret} 8 | 9 | DB_NAME=seed 10 | DB_USERNAME=seed 11 | DB_PASSWORD=password 12 | 13 | AWS_REGION={your region} 14 | AWS_ACCESS_KEY_ID={your access key} 15 | AWS_SECRET_ACCESS_KEY={your secret key} 16 | 17 | LOG_LEVEL=debug 18 | 19 | NODE_ENV=development 20 | 21 | S3_PROFILE_PIC_URL= {your s3 profile pic url e.g., https://s3.amazonaws.com/yourapp/profileImages} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bower_components/ 3 | node_modules/ 4 | tmp/ 5 | .tmp/ 6 | .sass-cache 7 | .tmp/* 8 | dist/ 9 | assets/ 10 | .DS_Store 11 | npm-debug.log 12 | tests/sqlite.db 13 | *.log 14 | *.iml 15 | .env -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('dist/config', 'config.js') 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express Typescript Seed 2 | [Node.js](https://nodejs.org) with [Express 4](http://expressjs.com/4x) written in [Typescript](https://www.typescriptlang.org/) 3 | 4 | [PostgreSQL](https://www.postgresql.org) database under [Sequelize](http://docs.sequelizejs.com/) ORM 5 | 6 | OAuth2 with [Passport](http://passportjs.org/) 7 | 8 | Roles based access with [Connect Roles](https://github.com/ForbesLindesay/connect-roles) 9 | 10 | Message brokering with [RabbitMQ](https://www.rabbitmq.com/) for running background tasks like sending e-mails and uploading images to S3 11 | 12 | Environment based configuration using [Dotenv](https://www.npmjs.com/package/dotenv) 13 | 14 | Integration Testing with [SuperTest](https://github.com/visionmedia/supertest) 15 | 16 | ## Quickstart 17 | ``` 18 | npm install -g yarn 19 | yarn install 20 | # install docker https://docs.docker.com/install/ 21 | docker stack deploy -c local-infra.yml infra 22 | # connect to postgres via postgres:password@localhost:5432 23 | # create 'seed' user with password 'password' 24 | # create 'seed' database and set the 'seed' user as the owner 25 | cp .env.example .env 26 | yarn run start 27 | # wait for app to start 28 | yarn global add sequelize-cli 29 | sequelize db:seed:all 30 | ``` 31 | 32 | ## Environment Setup 33 | This project uses the [Dotenv](https://www.npmjs.com/package/dotenv) library to load sensitive data such 34 | as database passwords and client secrets. 35 | 36 | There is a `.env.example` file included at the root of this project as an example, rename it to '.env' (.env is not under version control). Update the `.env` file with the pertinent information 37 | for your project. 38 | 39 | ### RabbitMQ 40 | Install and run [RabbitMQ](https://www.rabbitmq.com/) with the default settings (or use the provided local-infra.yml in conjunction with docker-compose/swarm) 41 | 42 | ### Database 43 | You will need a [PostgreSQL](https://www.postgresql.org) database running on localhost:5432 (or use the provided local-infra.yml in conjunction with docker-compose/swarm) 44 | 45 | The setup of PostgreSQL is beyond the scope of this guide. Please reference the [Install Guides](https://wiki.postgresql.org/wiki/Detailed_installation_guides) 46 | for help installing PostgreSQL on your machine. 47 | 48 | Once PostgreSQL is installed and running, create a new database called `seed`. Create a new user named `seed`. Make this user the owner of the newly created database. 49 | 50 | Since the tables defined in the entities do not already exist, Sequelize will attempt to build them once you start the server. 51 | 52 | ## Running the app 53 | yarn install 54 | yarn run start 55 | You can also run the app in debug mode and attach a [Debugger](https://www.jetbrains.com/help/webstorm/run-debug-configuration-attach-to-node-js-chrome.html) in Webstorm 56 | 57 | yarn run debug 58 | 59 | Once the app is running and the tables are created, you can seed the database with the sequelize-cli. 60 | Install the sequelize-cli by running 61 | 62 | yarn global add sequelize-cli 63 | 64 | then run 65 | 66 | sequelize db:seed:all 67 | 68 | ### Running the tests 69 | yarn run test 70 | 71 | ## Contact 72 | Kevin Kolz - kckolz@gmail.com 73 | 74 | ## License 75 | MIT 76 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Server 3 | */ 4 | import { Server } from "./server"; 5 | 6 | Server.initializeApp().then(() => { 7 | console.log((" App is running at http://localhost:%d in %s mode"), Server.app.get("port"), Server.app.get("env")); 8 | }); -------------------------------------------------------------------------------- /auth/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | import * as passport from "passport"; 5 | import * as LocalStrategy from "passport-local"; 6 | import * as ClientPasswordStrategy from "passport-oauth2-client-password"; 7 | import * as BearerStrategy from "passport-http-bearer"; 8 | import * as bcrypt from "bcrypt"; 9 | import {BasicStrategy} from "passport-http"; 10 | import {User} from "../models/entities/User"; 11 | import {AccessToken} from "../models/entities/AccessToken"; 12 | import {Client} from "../models/entities/Client"; 13 | import {sign, verify} from 'jsonwebtoken'; 14 | import {AuthError} from "../errors/AuthError"; 15 | 16 | const FacebookTokenStrategy = require('passport-facebook-token'); 17 | 18 | export class Auth { 19 | 20 | static serializeUser() { 21 | passport.serializeUser(function (user: any, done) { 22 | done(null, user.id); 23 | }); 24 | 25 | passport.deserializeUser(function (id: number, done) { 26 | User.find({where: {id}}).then(function (user: User) { 27 | done(null, user); 28 | }); 29 | }); 30 | } 31 | 32 | /** 33 | * LocalStrategy 34 | * 35 | * This strategy is used to authenticate users based on a username and password. 36 | * Anytime a request is made to authorize an application, we must ensure that 37 | * a user is logged in before asking them to approve the request. 38 | */ 39 | static useLocalStrategy() { 40 | passport.use(new LocalStrategy( async(userName, password, done) => { 41 | let user = await User.findOne({where: {email: userName}}); 42 | if(user) { 43 | const authorized = await this.comparePasswords(password, user.password); 44 | if(authorized) { 45 | return done(null, user); 46 | } else { 47 | return done(null, false) 48 | } 49 | } else { 50 | return done("No user found", false); 51 | } 52 | })); 53 | } 54 | 55 | static async comparePasswords(pass1: string | undefined, pass2: string | undefined): Promise { 56 | if(pass1 && pass2) { 57 | return bcrypt.compare(pass1, pass2); 58 | } else { 59 | return false; 60 | } 61 | } 62 | 63 | /** 64 | * BearerStrategy 65 | * 66 | * This strategy is used to authenticate users based on an access token (aka a 67 | * bearer token). The user must have previously authorized a client 68 | * application, which is issued an access token to make requests on behalf of 69 | * the authorizing user. 70 | */ 71 | static useBearerStrategy() { 72 | passport.use(new BearerStrategy((token, done) => { 73 | AccessToken.findOne({where: {token: token}}).then(accessToken => { 74 | if (accessToken) { 75 | const jwtSecret: string | undefined = process.env.JWT_SECRET; 76 | if(jwtSecret) { 77 | verify(accessToken.token, jwtSecret, (err, decodedToken: any) => { 78 | if (decodedToken && accessToken.userId === decodedToken.id) { 79 | User.find({where: {id: accessToken.userId}}).then(user => { 80 | return done(null, user); 81 | }).catch(error => { 82 | return done(new AuthError(error.message), false); 83 | }); 84 | } else { 85 | done(new AuthError(err.message), false); 86 | } 87 | }); 88 | } 89 | } else { 90 | return done(new AuthError("Unauthorized"), false); 91 | } 92 | }).catch(function (error) { 93 | return done(new AuthError(error.message), false); 94 | }); 95 | })); 96 | } 97 | 98 | 99 | /** 100 | * BasicStrategy & ClientPasswordStrategy 101 | * 102 | * These strategies are used to authenticate registered OAuth clients. They are 103 | * employed to protect the `token` endpoint, which consumers use to obtain 104 | * access tokens. The OAuth 2.0 specification suggests that clients use the 105 | * HTTP Basic scheme to authenticate. Use of the client password strategy 106 | * allows clients to send the same credentials in the request body (as opposed 107 | * to the `Authorization` header). While this approach is not recommended by 108 | * the specification, in practice it is quite common. 109 | */ 110 | static useBasicStrategy() { 111 | passport.use(new BasicStrategy( 112 | function (clientId, clientSecret, done) { 113 | Client.findOne({ 114 | where: {clientId: clientId} 115 | }).then(function (client: any) { 116 | if (!client) return done(null, false); 117 | if (!bcrypt.compareSync(clientSecret, client.clientSecret)) return done(null, false); 118 | return done(null, client); 119 | }).catch(function (error) { 120 | return done(error); 121 | }); 122 | } 123 | )); 124 | 125 | passport.use(new ClientPasswordStrategy( 126 | function (clientId, clientSecret, done) { 127 | Client.findOne({ 128 | where: {clientId: clientId} 129 | }).then(function (client: any) { 130 | if (!client) return done(null, false); 131 | if (!bcrypt.compareSync(clientSecret, client.clientSecret)) return done(null, false); 132 | return done(null, client); 133 | }).catch(function (error) { 134 | return done(error); 135 | }); 136 | } 137 | )); 138 | } 139 | 140 | static useFacebookTokenStrategy() { 141 | passport.use(new FacebookTokenStrategy({ 142 | clientID: process.env.FACEBOOK_CLIENT_ID, 143 | clientSecret: process.env.FACEBOOK_CLIENT_SECRET 144 | }, (accessToken, refreshToken, profile, done) => { 145 | const parsedProfile = profile._json; 146 | User.findOne({where: {email: parsedProfile.email}}).then(user => { 147 | if (user) { 148 | AccessToken.findOne({where: {userId: user.id}}).then(accessToken => { 149 | if (accessToken) { 150 | const jwtSecret: string | undefined = process.env.JWT_SECRET; 151 | if(jwtSecret) { 152 | verify(accessToken.token, jwtSecret, (err, decodedToken: any) => { 153 | if (decodedToken && accessToken.userId === decodedToken.id) { 154 | return done(null, accessToken); 155 | } else { 156 | return done(new AuthError(err.message), false); 157 | } 158 | }); 159 | } else { 160 | return done(new AuthError("JWT Secret undefined"), false); 161 | } 162 | } else { 163 | const jwtSecret: string | undefined = process.env.JWT_SECRET; 164 | if(jwtSecret) { 165 | sign(user, jwtSecret, {expiresIn: "10h"}, (error, token) => { 166 | AccessToken.create({ 167 | token: token, 168 | userId: user.id 169 | }).then((accessToken: AccessToken) => { 170 | return done(null, accessToken); 171 | }).catch(error => { 172 | return done(error, false); 173 | }); 174 | }); 175 | } else { 176 | return done(new AuthError("JWT Secret undefined"), false); 177 | } 178 | 179 | } 180 | }); 181 | } else { 182 | return done("No account found with email: " + parsedProfile.email); 183 | } 184 | }); 185 | } 186 | )); 187 | } 188 | 189 | public static getBearerMiddleware() { 190 | return passport.authenticate('bearer', {session: false, failWithError: true}); 191 | } 192 | } 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /auth/oauth2.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | import * as oauth2orize from "oauth2orize"; 5 | import * as passport from "passport"; 6 | import {AccessToken, Client} from "../models"; 7 | import {sign, verify} from "jsonwebtoken"; 8 | import {AuthError} from "../errors/AuthError"; 9 | 10 | export class Oauth2 { 11 | 12 | private server; 13 | private jwtSecret: string | undefined; 14 | 15 | constructor() { 16 | this.server = oauth2orize.createServer(); 17 | this.serializeClient(); 18 | this.registerGrants(); 19 | this.jwtSecret = process.env.JWT_SECRET; 20 | } 21 | 22 | // token endpoint 23 | // 24 | // `token` middleware handles client requests to exchange authorization grants 25 | // for access tokens. Based on the grant type being exchanged, the above 26 | // exchange middleware will be invoked to handle the request. Clients must 27 | // authenticate when making requests to this endpoint. 28 | public getTokenEndpoint() { 29 | return [ 30 | passport.authenticate(['local'], { session: false }), 31 | this.server.token(), 32 | this.server.errorHandler() 33 | ]; 34 | } 35 | 36 | // Register serialialization and deserialization functions. 37 | // 38 | // When a client redirects a user to user authorization endpoint, an 39 | // authorization transaction is initiated. To complete the transaction, the 40 | // user must authenticate and approve the authorization request. Because this 41 | // may involve multiple HTTP request/response exchanges, the transaction is 42 | // stored in the session. 43 | // 44 | // An application must supply serialization functions, which determine how the 45 | // client object is serialized into the session. Typically this will be a 46 | // simple matter of serializing the client's ID, and deserializing by finding 47 | // the client by ID from the database. 48 | private serializeClient() { 49 | 50 | this.server.serializeClient(function(client, done) { 51 | return done(null, client.id); 52 | }); 53 | 54 | this.server.deserializeClient(function(id, done) { 55 | Client.findById(id).then(function(client) { 56 | return done(null, client); 57 | }, function(error) { 58 | return done(error); 59 | }); 60 | }); 61 | } 62 | 63 | 64 | // Register supported grant types. 65 | // 66 | // OAuth 2.0 specifies a framework that allows users to grant client 67 | // applications limited access to their protected resources. It does this 68 | // through a process of the user granting access, and the client exchanging 69 | // the grant for an access token. 70 | private registerGrants() { 71 | this.registerPasswordGrant(); 72 | // this.registerClientCredentialGrant(); 73 | } 74 | 75 | // PASSWORD GRANT TYPE 76 | // Exchange user id and password for access tokens. The callback accepts the 77 | // `client`, which is exchanging the user's name and password from the 78 | // authorization request for verification. If these values are validated, the 79 | // application issues an access token on behalf of the user who authorized the code. 80 | private registerPasswordGrant() { 81 | 82 | this.server.exchange(oauth2orize.exchange.password((athlete, username, password, scope, done) => { 83 | 84 | AccessToken.findOne({where: {userId: athlete.id}}).then(accessToken => { 85 | if(accessToken) { 86 | if(this.jwtSecret) { 87 | verify(accessToken.token, this.jwtSecret, (err, decodedToken: any) => { 88 | if(err) { 89 | accessToken.destroy().then(() => { 90 | if(this.jwtSecret) { 91 | sign(athlete.dataValues, this.jwtSecret, { expiresIn: "10h"}, (err, encodedToken) => { 92 | if(err) { 93 | return done(err); 94 | } 95 | AccessToken.create({ 96 | token: encodedToken, 97 | userId: athlete.id 98 | }).then((accessToken: AccessToken) => { 99 | return done(null, accessToken.token); 100 | }).catch((error) => { 101 | return done(error); 102 | }); 103 | }); 104 | } else { 105 | return done(new AuthError("JWT Secret Undefined"), false); 106 | } 107 | 108 | }); 109 | } else if (decodedToken && accessToken.userId === decodedToken.id) { 110 | return done(null, accessToken.token); 111 | } else { 112 | return done(new AuthError("Token Validation Error"), false); 113 | } 114 | }); 115 | } else { 116 | return done(new AuthError("JWT Secret Undefined"), false); 117 | } 118 | 119 | } else { 120 | if(this.jwtSecret) { 121 | sign(athlete.dataValues, this.jwtSecret, { expiresIn: "10h"}, (err, encodedToken) => { 122 | if(err) { 123 | return done(err); 124 | } 125 | AccessToken.create({ 126 | token: encodedToken, 127 | userId: athlete.id 128 | }).then((accessToken: AccessToken) => { 129 | return done(null, accessToken.token); 130 | }).catch((error) => { 131 | return done(new AuthError(error.message)); 132 | }); 133 | }); 134 | } else { 135 | return done(new AuthError("JWT Secret Undefined")); 136 | } 137 | 138 | } 139 | }); 140 | 141 | })); 142 | } 143 | 144 | // CLIENT CREDENTIAL GRANT TYPE 145 | // Exchange the client id and password/secret for an access token. The callback accepts the 146 | // `client`, which is exchanging the client's id and password/secret from the 147 | // authorization request for verification. If these values are validated, the 148 | // application issues an access token on behalf of the client who authorized the code. 149 | private registerClientCredentialGrant() { 150 | 151 | this.server.exchange(oauth2orize.exchange.clientCredentials(function(client, scope, done) { 152 | 153 | Client.findOne({ 154 | where: {clientId: client.clientId} 155 | }).then(function(localClient: any) { 156 | if(localClient === null) return done(null, false); 157 | if(localClient.clientSecret !== client.clientSecret) return done(null, false); 158 | const token = sign(localClient, this.jwtSecret, { expiresIn: "10h"}); 159 | AccessToken.create({ 160 | token: token, 161 | clientId: client.dataValues.id 162 | }).then(function(accessToken) { 163 | return done(null, accessToken); 164 | }).catch(function(error) { 165 | return done(error); 166 | }); 167 | }).catch(function(error) { 168 | return done(error); 169 | }); 170 | })); 171 | } 172 | } -------------------------------------------------------------------------------- /auth/roles.ts: -------------------------------------------------------------------------------- 1 | import {AuthError} from "../errors/AuthError"; 2 | import {RoleEnum} from "../models/enums/RoleEnum"; 3 | 4 | const ConnectRoles = require('connect-roles'); 5 | 6 | export class Roles { 7 | 8 | public static connectRoles; 9 | 10 | public static middleware() { 11 | return Roles.connectRoles.middleware(); 12 | } 13 | 14 | public static is(role: RoleEnum) { 15 | return Roles.connectRoles.is(role.toString()); 16 | } 17 | 18 | public static buildRoles() { 19 | 20 | Roles.connectRoles = new ConnectRoles({ 21 | failureHandler: function (req, res, action) { 22 | const error = new AuthError('Access Denied - You don\'t have permission to: ' + action); 23 | res.status(403).json(error); 24 | }, 25 | async: true 26 | }); 27 | 28 | Roles.connectRoles.use(RoleEnum.ADMIN, function (req) { 29 | if (req.user.role === 'admin') { 30 | return true; 31 | } 32 | }); 33 | 34 | Roles.connectRoles.use('modify user', function (req) { 35 | if(Roles.isAdmin(req.user)) { 36 | return true; 37 | } else { 38 | return req.user.id === req.params.id || req.user.email === req.query.email; 39 | } 40 | }); 41 | } 42 | 43 | private static isAdmin(user): boolean { 44 | return user.role === 'admin'; 45 | } 46 | } -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv').config(); 2 | 3 | export const development = { 4 | database: process.env.DB_NAME, 5 | username: process.env.DB_USERNAME, 6 | password: process.env.DB_PASSWORD, 7 | // Defaults for Postgres 8 | "host": "127.0.0.1", 9 | "port": 5432, 10 | "dialect": "postgres", 11 | "logging": false 12 | }; 13 | 14 | export const test = { 15 | dialect: "sqlite", 16 | storage: 'tests/sqlite.db', 17 | logging: false 18 | }; 19 | 20 | export const production = { 21 | database: process.env.DB_NAME, 22 | username: process.env.DB_USERNAME, 23 | password: process.env.DB_PASSWORD, 24 | host: "127.0.0.1", 25 | port: 5432, 26 | dialect: "postgres", 27 | logging: false 28 | }; 29 | 30 | export const facebook = { 31 | clientID: process.env.FACEBOOK_CLIENT_ID, 32 | clientSecret: process.env.FACEBOOK_SECRET, 33 | callbackURL: 'http://localhost:3000/auth/facebook/callback', 34 | profileFields: ['id', 'name', 'displayName', 'picture', 'email'], 35 | }; 36 | 37 | export const google = { 38 | clientID: process.env.GOOGLE_CLIENT_ID, 39 | clientSecret: process.env.GOOGLE_SECRET, 40 | callbackURL: 'http://localhost:3000/auth/google/callback', 41 | }; 42 | -------------------------------------------------------------------------------- /errors/AuthError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class AuthError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 100, AuthError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/BaseError.ts: -------------------------------------------------------------------------------- 1 | export class BaseError implements Error { 2 | 3 | public message: string; 4 | public code: number; 5 | public name: string; 6 | 7 | constructor(errorString: string, code: number, name: string) { 8 | this.message = errorString; 9 | this.code = code; 10 | this.name = name; 11 | } 12 | } -------------------------------------------------------------------------------- /errors/DatabaseError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class DatabaseError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 102, DatabaseError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/EmailError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class EmailError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 105, EmailError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import {RegistrationError} from "./RegistrationError"; 2 | import {AuthError} from "./AuthError"; 3 | import {DatabaseError} from "./DatabaseError"; 4 | import {NotFoundError} from "./NotFoundError"; 5 | import {DatabaseError as SequelizeError, ValidationError as SequelizeValidationError} from "sequelize"; 6 | import {InternalServerError} from "./InternalServerError"; 7 | import {ValidationError} from "./ValidationError"; 8 | import {logger} from "../lib/logger"; 9 | 10 | export function errorHandler(error, req, res, next) { 11 | 12 | if (error instanceof RegistrationError) { 13 | logger.error(error); 14 | return res.status(400).json(error); 15 | } 16 | if (error instanceof AuthError) { 17 | logger.error(error); 18 | return res.status(401).json(error); 19 | } 20 | if (error.name === 'AuthenticationError') { 21 | logger.error(error.message); 22 | return res.status(403).json(error); 23 | } 24 | if (error instanceof DatabaseError) { 25 | logger.error(error); 26 | return res.status(500).json(error); 27 | } 28 | if (error instanceof SequelizeError) { 29 | logger.error(error.message); 30 | return res.status(500).json(new DatabaseError(error.message)); 31 | } 32 | if (error instanceof NotFoundError) { 33 | logger.error(error); 34 | return res.status(404).json(error); 35 | } 36 | if (error instanceof SequelizeValidationError) { 37 | logger.error(error.message); 38 | return res.status(400).json(new ValidationError(error.message)); 39 | } 40 | logger.error(error.message); 41 | return res.status(500).json(new InternalServerError(error.message)); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /errors/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class InternalServerError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 105, InternalServerError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/MessageError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class MessageError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 106, MessageError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class NotFoundError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 103, NotFoundError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/RegistrationError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class RegistrationError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 101, 'RegistrationError'); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/TestError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class TestError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 107, TestError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import {BaseError} from "./BaseError"; 2 | 3 | export class ValidationError extends BaseError { 4 | constructor(errorString: string) { 5 | super(errorString, 109, ValidationError.name); 6 | } 7 | } -------------------------------------------------------------------------------- /lib/logger.ts: -------------------------------------------------------------------------------- 1 | import moment = require("moment"); 2 | 3 | const winston = require("winston"); 4 | 5 | const level = process.env.LOG_LEVEL || 'debug'; 6 | 7 | export const logger = new winston.Logger({ 8 | transports: [ 9 | new winston.transports.Console({ 10 | level: level, 11 | timestamp: function () { 12 | return moment().format('YYYY-MM-DD hh:mm:ss').trim(); 13 | } 14 | }) 15 | ] 16 | }); -------------------------------------------------------------------------------- /local-infra.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | db: 6 | image: postgres 7 | ports: 8 | - 5432:5432 9 | environment: 10 | POSTGRES_PASSWORD: password 11 | PGDATA: /var/lib/postgresql/data/pgdata 12 | volumes: 13 | - pg-data:/var/lib/postgresql/data/pgdata 14 | 15 | rabbitmq: 16 | image: rabbitmq 17 | ports: 18 | - 5672:5672 19 | - 8080:15672 20 | volumes: 21 | - rabbit-data:/var/lib/rabbitmq 22 | 23 | volumes: 24 | pg-data: 25 | rabbit-data: 26 | -------------------------------------------------------------------------------- /managers/AuthManager.ts: -------------------------------------------------------------------------------- 1 | import {User} from "../models/entities/User"; 2 | import {AccessToken} from "../models/entities/AccessToken"; 3 | import {NotFoundError} from "../errors/NotFoundError"; 4 | 5 | export class AuthManager { 6 | 7 | constructor() { 8 | } 9 | 10 | public async getUserByToken(token: string): Promise { 11 | const storedToken = await AccessToken.find({where: {token: token}, include: [User]}); 12 | if (storedToken && storedToken.user) { 13 | return storedToken.user; 14 | } else { 15 | throw new NotFoundError("No user found with provided token"); 16 | } 17 | } 18 | 19 | public async getAccessTokenForUser(userId: string): Promise { 20 | const accessToken = await AccessToken.findOne({where: {userId: userId}}); 21 | if(accessToken) { 22 | return accessToken; 23 | } else { 24 | throw new NotFoundError("No access token found for the provided user"); 25 | } 26 | } 27 | 28 | public async logout(user: User): Promise { 29 | const accessToken = await AccessToken.findOne({where: {userId: user.id}}); 30 | if(accessToken) { 31 | return accessToken.destroy(); 32 | } else { 33 | throw new NotFoundError("No access token found for the provided user"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /managers/ClientManager.ts: -------------------------------------------------------------------------------- 1 | import {Client} from "../models/entities/Client"; 2 | import {NotFoundError} from "../errors/NotFoundError"; 3 | 4 | export class ClientManager { 5 | 6 | constructor() { 7 | } 8 | 9 | public async createClient(clientId, clientSecret) { 10 | let newClient = new Client({clientId, clientSecret}); 11 | return newClient.save(); 12 | } 13 | 14 | public async updateClient(clientId, clientSecret) { 15 | let client = await Client.find({where: {clientId: clientId}}); 16 | if(client) { 17 | client.clientId = clientId; 18 | client.clientSecret = clientSecret; 19 | return client.save(); 20 | } else { 21 | throw new NotFoundError("No client found with that id"); 22 | } 23 | } 24 | 25 | public async deleteClient(clientId) { 26 | let client = await Client.find({where: {clientId: clientId}}); 27 | if(client) { 28 | return client.destroy(); 29 | } else { 30 | throw new NotFoundError("No client found with that id"); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /managers/EmailManager.ts: -------------------------------------------------------------------------------- 1 | import {MessageManager} from "./MessageManager"; 2 | import {MessageQueueEnum} from "../models/enums/MessageQueueEnum"; 3 | import AWS = require('aws-sdk'); 4 | import {logger} from "../lib/logger"; 5 | 6 | export class EmailManager extends MessageManager { 7 | 8 | private ses: AWS.SES; 9 | 10 | constructor() { 11 | super(MessageQueueEnum.EMAIL); 12 | this.ses = new AWS.SES(); 13 | } 14 | 15 | public sendMail(destinations: Array, returnAddress: string, source: string, messageSubject: string, messageBody: string): void { 16 | const email = { 17 | Destination: { 18 | ToAddresses: destinations 19 | }, 20 | Message: { 21 | Body: { 22 | Html: { 23 | Charset: 'UTF-8', 24 | Data: messageBody 25 | }, 26 | }, 27 | Subject: { 28 | Charset: 'UTF-8', 29 | Data: messageSubject 30 | } 31 | }, 32 | ReturnPath: returnAddress, 33 | Source: source 34 | }; 35 | 36 | this.publish(email).then(() => { 37 | logger.debug('Email published to queue'); 38 | }, ()=> { 39 | logger.error('Failed to publish email to queue'); 40 | }); 41 | } 42 | 43 | public async handleMessage(message) { 44 | this.ses.sendEmail(message, (err, data) => { 45 | if (err) { 46 | throw err; 47 | } 48 | else { 49 | return data; 50 | } 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /managers/MessageManager.ts: -------------------------------------------------------------------------------- 1 | import {MessageError} from "../errors/MessageError"; 2 | import {logger} from "../lib/logger"; 3 | 4 | export abstract class MessageManager { 5 | 6 | private queue: string; 7 | 8 | // This must be instantiated when the server starts 9 | static connection; 10 | 11 | constructor(queue: string) { 12 | this.queue = queue; 13 | this.subscribe().then(() => { 14 | logger.debug('Message queue subscribed'); 15 | }, () => { 16 | logger.error('Message queue subscription failed'); 17 | }); 18 | } 19 | 20 | public abstract async handleMessage(message): Promise; 21 | 22 | public async publish(content) { 23 | try { 24 | 25 | const channel = await MessageManager.connection.createChannel(); 26 | 27 | channel.assertQueue(this.queue, { 28 | // Ensure that the queue is not deleted when server restarts 29 | durable: true 30 | }).then(() => { 31 | channel.sendToQueue(this.queue, Buffer.from(JSON.stringify(content)), { 32 | // Store queued elements on disk 33 | persistent: true, 34 | contentType: 'application/json' 35 | }); 36 | return true; 37 | }, (error) => { 38 | throw error; 39 | }); 40 | 41 | } catch (error) { 42 | throw new MessageError(error.message); 43 | } 44 | } 45 | 46 | public async subscribe() { 47 | try { 48 | 49 | const channel = await MessageManager.connection.createChannel(); 50 | 51 | channel.assertQueue(this.queue, { 52 | // Ensure that the queue is not deleted when server restarts 53 | durable: true 54 | }).then(() => { 55 | 56 | // Only request 1 unacked message from queue 57 | // This value indicates how many messages we want to process in parallel 58 | channel.prefetch(1); 59 | 60 | channel.consume(this.queue, messageData => { 61 | 62 | if (messageData === null) { 63 | return; 64 | } 65 | 66 | // Decode message contents 67 | const message = JSON.parse(messageData.content.toString()); 68 | 69 | this.handleMessage(message).then(() => { 70 | return channel.ack(messageData); 71 | }, () => { 72 | return channel.nack(messageData); 73 | }); 74 | }); 75 | }, (error) => { 76 | throw error; 77 | }); 78 | 79 | } catch (error) { 80 | throw new MessageError(error.message); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /managers/S3Manager.ts: -------------------------------------------------------------------------------- 1 | import {MessageManager} from "./MessageManager"; 2 | import {MessageQueueEnum} from "../models/enums/MessageQueueEnum"; 3 | import AWS = require('aws-sdk'); 4 | import {logger} from "../lib/logger"; 5 | 6 | export class S3Manager extends MessageManager { 7 | 8 | private s3: AWS.S3; 9 | 10 | constructor() { 11 | super(MessageQueueEnum.IMAGE_UPLOAD); 12 | this.s3 = new AWS.S3({params: {Bucket: 'your bucket name'}}); 13 | } 14 | 15 | public uploadImage(image, destination, fileName): void { 16 | 17 | const imageParams = { 18 | Key: destination + '/' +fileName, 19 | Body: image.buffer.toString('base64'), 20 | ACL: 'public-read' 21 | }; 22 | 23 | this.publish(imageParams).then(() => { 24 | logger.debug('Image published to queue'); 25 | }, ()=> { 26 | logger.error('Failed to publish image to queue'); 27 | }); 28 | } 29 | 30 | public async handleMessage(message) { 31 | 32 | message.Body = new Buffer(message.Body, 'base64'); 33 | 34 | this.s3.upload(message, function(err, data) { 35 | if (err) { 36 | throw err; 37 | } else { 38 | return data; 39 | } 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /managers/UserManager.ts: -------------------------------------------------------------------------------- 1 | import {User} from "../models/entities/User"; 2 | import {NotFoundError} from "../errors/NotFoundError"; 3 | import {RoleEnum} from "../models/enums/RoleEnum"; 4 | import {Auth} from "../auth/auth"; 5 | import {AuthError} from "../errors/AuthError"; 6 | import {S3Manager} from "./S3Manager"; 7 | import {logger} from "../lib/logger"; 8 | 9 | export class UserManager { 10 | 11 | constructor() { 12 | 13 | } 14 | 15 | public async createUser(email: string, password: string, firstName: string, lastName: string, role: RoleEnum, profilePicUrl?: string) { 16 | 17 | const newUser = new User({ 18 | firstName, 19 | lastName, 20 | email, 21 | password, 22 | profilePicUrl, 23 | role 24 | }); 25 | return newUser.save(); 26 | } 27 | 28 | public async updateUser(userId: string, email: string, firstName: string, lastName: string, role: RoleEnum, profilePicUrl?: string): Promise { 29 | 30 | const user = await User.find({where: {id: userId}}); 31 | if (user) { 32 | user.email = email || user.email; 33 | user.firstName = firstName || user.firstName; 34 | user.lastName = lastName || user.lastName; 35 | user.profilePicUrl = profilePicUrl || user.profilePicUrl; 36 | user.role = role; 37 | return user.save(); 38 | } else { 39 | logger.error("No user found"); 40 | throw new NotFoundError("No user found"); 41 | } 42 | } 43 | 44 | public async updateProfileImage(userId: string, image: Express.Multer.File): Promise { 45 | 46 | const s3Manager = new S3Manager(); 47 | const user = await User.find({where: {id: userId}}); 48 | if (user) { 49 | s3Manager.uploadImage(image, 'profileImages', userId); 50 | user.profilePicUrl = `${process.env.S3_PROFILE_PIC_URL}/${userId}`; 51 | return user.save(); 52 | } else { 53 | logger.error("No user found"); 54 | throw new NotFoundError("No user found"); 55 | } 56 | } 57 | 58 | public async findByEmail(email: string) { 59 | const user = await User.findOne({where: {email: email}}); 60 | if (user) { 61 | return user; 62 | } else { 63 | logger.error("No user found with the provided email"); 64 | throw new NotFoundError("No user found with the provided email"); 65 | } 66 | } 67 | 68 | public async deleteUser(userId: string): Promise { 69 | const user = await User.find({where: {id: userId}}); 70 | if (user) { 71 | await user.destroy(); 72 | return user; 73 | } else { 74 | logger.error("No user found"); 75 | throw new NotFoundError("No user found"); 76 | } 77 | } 78 | 79 | public async updatePassword(userId: string, currentPassword: string, newPassword: string): Promise { 80 | const user = await User.find({where: {id: userId}}); 81 | if (user) { 82 | const authorized = await Auth.comparePasswords(currentPassword, user.password); 83 | if (authorized) { 84 | user.password = newPassword; 85 | return user.save(); 86 | } else { 87 | logger.error("Current password incorrect"); 88 | throw new AuthError("Current password incorrect"); 89 | } 90 | } else { 91 | logger.error("No user found"); 92 | throw new NotFoundError("No user found"); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /models/dtos/UserDTO.ts: -------------------------------------------------------------------------------- 1 | import {User} from "../entities/User"; 2 | 3 | export class UserDTO { 4 | 5 | public id: string; 6 | public firstName: string; 7 | public lastName: string; 8 | public email: string; 9 | public profilePicUrl: string; 10 | 11 | constructor(user: User) { 12 | this.id = user.id; 13 | this.firstName = user.firstName; 14 | this.lastName = user.lastName; 15 | this.email = user.email; 16 | this.profilePicUrl = user.profilePicUrl; 17 | } 18 | } -------------------------------------------------------------------------------- /models/entities/AccessToken.ts: -------------------------------------------------------------------------------- 1 | import {BelongsTo, Column, DataType, ForeignKey, Table} from 'sequelize-typescript'; 2 | import {BaseModel} from "./BaseModel"; 3 | import {User} from "./User"; 4 | 5 | @Table 6 | export class AccessToken extends BaseModel { 7 | 8 | @Column(DataType.TEXT) 9 | token: string; 10 | 11 | @BelongsTo(() => User) 12 | user: User; 13 | 14 | @ForeignKey(() => User) 15 | @Column(DataType.UUID) 16 | userId: string; 17 | 18 | @Column 19 | clientId: string; 20 | } 21 | -------------------------------------------------------------------------------- /models/entities/AuthorizationCode.ts: -------------------------------------------------------------------------------- 1 | import {Table, Column, Model} from 'sequelize-typescript'; 2 | import {BaseModel} from "./BaseModel"; 3 | 4 | @Table 5 | export class AuthorizationCode extends BaseModel { 6 | 7 | @Column 8 | code: string; 9 | 10 | @Column 11 | redirectURI: string; 12 | 13 | @Column 14 | clientId: string; 15 | 16 | @Column 17 | userId: string; 18 | } 19 | -------------------------------------------------------------------------------- /models/entities/BaseModel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, CreatedAt, Default, DeletedAt, IsUUID, Model, PrimaryKey, DataType, Table, 3 | UpdatedAt 4 | } from 'sequelize-typescript'; 5 | 6 | @Table 7 | export class BaseModel extends Model { 8 | 9 | @IsUUID(4) 10 | @PrimaryKey 11 | @Default(DataType.UUIDV4) 12 | @Column(DataType.UUID) 13 | id: string; 14 | 15 | @CreatedAt 16 | creationDate: Date; 17 | 18 | @UpdatedAt 19 | updatedOn: Date; 20 | 21 | @DeletedAt 22 | deletionDate: Date; 23 | } 24 | -------------------------------------------------------------------------------- /models/entities/Client.ts: -------------------------------------------------------------------------------- 1 | import {AllowNull, BeforeSave, Column, Table, Unique} from 'sequelize-typescript'; 2 | import {Utils} from "../../utils"; 3 | import {BaseModel} from "./BaseModel"; 4 | 5 | @Table 6 | export class Client extends BaseModel { 7 | 8 | @AllowNull(false) 9 | @Unique 10 | @Column 11 | clientId: string; 12 | 13 | @AllowNull(false) 14 | @Column 15 | clientSecret: string; 16 | 17 | @BeforeSave 18 | static encryptPassword(instance: Client) { 19 | instance.clientSecret = Utils.encryptPassword(instance.clientSecret); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /models/entities/User.ts: -------------------------------------------------------------------------------- 1 | import {AllowNull, BeforeSave, Column, DataType, HasOne, IsEmail, Table, Unique} from 'sequelize-typescript'; 2 | import {AccessToken} from "./AccessToken"; 3 | import {BaseModel} from "./BaseModel"; 4 | import {RoleEnum} from "../enums/RoleEnum"; 5 | import {Utils} from "../../utils"; 6 | 7 | @Table 8 | export class User extends BaseModel { 9 | 10 | @AllowNull(false) 11 | @Column 12 | firstName: string; 13 | 14 | @AllowNull(false) 15 | @Column 16 | lastName: string; 17 | 18 | @AllowNull(false) 19 | @IsEmail 20 | @Unique 21 | @Column 22 | email: string; 23 | 24 | @AllowNull(false) 25 | @Column 26 | password: string; 27 | 28 | @Column(DataType.ENUM(RoleEnum.ADMIN, RoleEnum.MEMBER)) 29 | role: RoleEnum; 30 | 31 | @HasOne(() => AccessToken) 32 | accessToken: AccessToken; 33 | 34 | @Column 35 | profilePicUrl: string; 36 | 37 | @BeforeSave 38 | static encryptPassword(instance: User) { 39 | instance.password = Utils.encryptPassword(instance.password); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /models/enums/MessageQueueEnum.ts: -------------------------------------------------------------------------------- 1 | export enum MessageQueueEnum { 2 | EMAIL = "email", 3 | IMAGE_UPLOAD = "imageUpload" 4 | } -------------------------------------------------------------------------------- /models/enums/RoleEnum.ts: -------------------------------------------------------------------------------- 1 | export enum RoleEnum { 2 | ADMIN = "admin", 3 | MEMBER = "member" 4 | } -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | import {Sequelize} from 'sequelize-typescript'; 2 | import {AccessToken} from "./entities/AccessToken"; 3 | import {AuthorizationCode} from "./entities/AuthorizationCode"; 4 | import {Client} from "./entities/Client"; 5 | import {User} from "./entities/User"; 6 | 7 | export {Sequelize} from 'sequelize-typescript'; 8 | export {AccessToken} from "./entities/AccessToken"; 9 | export {AuthorizationCode} from "./entities/AuthorizationCode"; 10 | export {Client} from "./entities/Client"; 11 | export {User} from "./entities/User"; 12 | 13 | /** 14 | * All models must be imported from this file or else they will not be registered with Sequelize 15 | */ 16 | 17 | export class Models { 18 | 19 | public sequelize: Sequelize; 20 | 21 | constructor(config: any) { 22 | this.sequelize = new Sequelize(config); 23 | } 24 | 25 | public initModels() { 26 | this.sequelize.addModels(this.getModels()); 27 | return this.sequelize.sync({force: process.env.NODE_ENV === 'test'}); 28 | } 29 | 30 | // TODO Scan models folder to build list 31 | private getModels() { 32 | return [ 33 | AccessToken, AuthorizationCode, Client, User 34 | ]; 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript-seed", 3 | "version": "1.0.0", 4 | "private": false, 5 | "author": "Kevin Kolz ", 6 | "scripts": { 7 | "start": "yarn run build && yarn run serve", 8 | "test": "yarn run build && rimraf tests/sqlite.db && NODE_ENV=test jest --forceExit --verbose dist/tests/*.test.js", 9 | "build": "yarn run clean && tsc", 10 | "serve": "tsc-watch --outDir ./dist --onSuccess \"node ./dist/app.js\"", 11 | "tslint": "tslint -c tslint.json -p tsconfig.json", 12 | "debug": "yarn run build && yarn run serve-debug", 13 | "serve-debug": "tsc-watch --outDir ./dist --onSuccess \"node --inspect=9222 --trace-warnings ./dist/app.js\"", 14 | "clean": "rimraf dist" 15 | }, 16 | "dependencies": { 17 | "amqplib": "^0.5.2", 18 | "aws-sdk": "^2.175.0", 19 | "bcrypt": "^1.0.3", 20 | "body-parser": "^1.14.1", 21 | "compression": "^1.7.1", 22 | "connect-ensure-login": "^0.1.1", 23 | "connect-roles": "^3.1.2", 24 | "debug": "^3.1.0", 25 | "dotenv": "^4.0.0", 26 | "errorhandler": "^1.4.2", 27 | "express": "^4.13.3", 28 | "express-limiter": "^1.6.1", 29 | "jsonwebtoken": "^8.1.0", 30 | "method-override": "^2.3.5", 31 | "moment": "^2.19.3", 32 | "morgan": "^1.6.1", 33 | "multer": "^1.3.0", 34 | "oauth2orize": "^1.2.0", 35 | "passport": "^0.4.0", 36 | "passport-facebook-token": "^3.3.0", 37 | "passport-google-oauth20": "^1.0.0", 38 | "passport-http": "^0.3.0", 39 | "passport-http-bearer": "^1.0.1", 40 | "passport-http-oauth": "^0.1.3", 41 | "passport-local": "^1.0.0", 42 | "passport-oauth2": "^1.1.2", 43 | "passport-oauth2-client-password": "^0.1.2", 44 | "pg": "^7.4.0", 45 | "pg-hstore": "^2.3.2", 46 | "reflect-metadata": "^0.1.10", 47 | "rimraf": "^2.6.2", 48 | "sequelize": "^4.22.15", 49 | "sequelize-typescript": "^0.5.0", 50 | "winston": "^2.4.0" 51 | }, 52 | "devDependencies": { 53 | "@types/async": "^2.0.40", 54 | "@types/bcrypt-nodejs": "0.0.30", 55 | "@types/body-parser": "^1.16.2", 56 | "@types/compression": "0.0.33", 57 | "@types/errorhandler": "0.0.30", 58 | "@types/express": "^4.0.35", 59 | "@types/jest": "^22.0.1", 60 | "@types/jsonwebtoken": "^7.2.4", 61 | "@types/lodash": "^4.14.63", 62 | "@types/morgan": "^1.7.32", 63 | "@types/multer": "^1.3.6", 64 | "@types/node": "^8.0.53", 65 | "@types/passport": "^0.3.3", 66 | "@types/request": "0.0.45", 67 | "@types/sequelize": "^4.0.79", 68 | "@types/supertest": "^2.0.4", 69 | "jest": "^22.0.6", 70 | "passport-debug": "^0.1.15", 71 | "sqlite3": "^3.1.1", 72 | "supertest": "^3.0.0", 73 | "ts-node": "^3.3.0", 74 | "tsc-watch": "^1.0.13", 75 | "tslint": "^5.0.0", 76 | "typescript": "^2.6.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /routes/AuthRouter.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { Oauth2 } from "../auth/oauth2"; 3 | import * as passport from "passport"; 4 | import {AuthManager} from "../managers/AuthManager"; 5 | import {Auth} from "../auth/auth"; 6 | import {BaseRouter} from "./BaseRouter"; 7 | 8 | export class AuthRouter extends BaseRouter { 9 | 10 | private authManager: AuthManager; 11 | 12 | constructor() { 13 | super(); 14 | this.authManager = new AuthManager(); 15 | this.buildRoutes(); 16 | } 17 | 18 | public async logout(req: express.Request, res: express.Response, next: express.NextFunction) { 19 | try { 20 | const user = await this.authManager.logout(req.user); 21 | res.json(user); 22 | } catch(error) { 23 | next(error); 24 | } 25 | } 26 | 27 | private buildRoutes() { 28 | const oath = new Oauth2(); 29 | this.router.post("/token", oath.getTokenEndpoint()); 30 | this.router.post('/facebook/token', passport.authenticate('facebook-token'), (req, res) => { 31 | res.json({token: req.user.token}); 32 | }); 33 | this.router.post("/logout", Auth.getBearerMiddleware(), this.logout.bind(this)); 34 | } 35 | 36 | 37 | 38 | } -------------------------------------------------------------------------------- /routes/BaseRouter.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | export class BaseRouter { 4 | 5 | public router: express.Router; 6 | 7 | constructor() { 8 | this.router = express.Router(); 9 | const limiter = require('express-limiter')(this.router); 10 | limiter({ 11 | lookup: 'user.id', 12 | // 150 requests per hour 13 | total: 150, 14 | expire: 1000 * 60 * 60 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /routes/ClientRouter.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import {Client} from "../models"; 3 | import {Auth} from "../auth/auth"; 4 | import {ClientManager} from "../managers/ClientManager"; 5 | 6 | export class ClientRouter { 7 | 8 | public router: express.Router; 9 | private clientManager: ClientManager; 10 | 11 | constructor() { 12 | this.clientManager = new ClientManager(); 13 | this.router = express.Router(); 14 | this.buildRoutes(); 15 | } 16 | 17 | public async get(req: express.Request, res: express.Response, next: express.NextFunction) { 18 | try { 19 | const clients = await Client.findAll(); 20 | res.json(clients); 21 | } catch(error) { 22 | next(error); 23 | } 24 | } 25 | 26 | public async getById(req: express.Request, res: express.Response, next: express.NextFunction) { 27 | try { 28 | const client = await Client.findOne({ where: {clientId: req.params.id} }); 29 | res.json(client); 30 | } catch(error) { 31 | next(error); 32 | } 33 | } 34 | 35 | public async post(req: express.Request, res: express.Response, next: express.NextFunction) { 36 | try { 37 | const newClient = await this.clientManager.createClient(req.body.clientId, req.body.clientSecret); 38 | res.json(newClient); 39 | } catch(error) { 40 | next(error); 41 | } 42 | } 43 | 44 | public async put(req: express.Request, res: express.Response, next: express.NextFunction) { 45 | try { 46 | const updatedClient = await this.clientManager.updateClient(req.body.clientId, req.body.clientSecret); 47 | res.json(updatedClient); 48 | } catch(error) { 49 | next(error); 50 | } 51 | } 52 | 53 | public async delete(req: express.Request, res: express.Response, next: express.NextFunction) { 54 | try { 55 | const client = this.clientManager.deleteClient(req.body.clientId); 56 | res.json(client); 57 | } catch(error) { 58 | next(error); 59 | } 60 | } 61 | 62 | private buildRoutes() { 63 | this.router.get("/", Auth.getBearerMiddleware(), this.get.bind(this)); 64 | this.router.get("/:id", Auth.getBearerMiddleware(), this.getById.bind(this)); 65 | this.router.post("/", Auth.getBearerMiddleware(), this.post.bind(this)); 66 | this.router.put("/:id", Auth.getBearerMiddleware(), this.put.bind(this)); 67 | this.router.delete("/:id", Auth.getBearerMiddleware(), this.delete.bind(this)); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /routes/UserRouter.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import {User} from "../models"; 3 | import {UserManager} from "../managers/UserManager"; 4 | import {Auth} from "../auth/auth"; 5 | import {UserDTO} from "../models/dtos/UserDTO"; 6 | import {Roles} from "../auth/roles"; 7 | import * as multer from 'multer'; 8 | import {BaseRouter} from "./BaseRouter"; 9 | 10 | export class UserRouter extends BaseRouter { 11 | 12 | private userManager: UserManager; 13 | private uploadHandler: any; 14 | 15 | constructor() { 16 | super(); 17 | this.userManager = new UserManager(); 18 | this.uploadHandler = multer({ storage: multer.memoryStorage() }); // configure multer to use memory storage 19 | this.buildRoutes(); 20 | } 21 | 22 | public async get(req: express.Request, res: express.Response, next: express.NextFunction) { 23 | try { 24 | if (req.query.email) { 25 | const user = await this.userManager.findByEmail(req.query.email); 26 | res.json(new UserDTO(user)); 27 | } else { 28 | const users = await User.findAll(); 29 | const userDTOs = users.map(user => { 30 | return new UserDTO(user); 31 | }); 32 | res.json(userDTOs); 33 | } 34 | } catch (error) { 35 | next(error); 36 | } 37 | } 38 | 39 | public async post(req: express.Request, res: express.Response, next: express.NextFunction) { 40 | try { 41 | const user = await this.userManager.createUser(req.body.email, req.body.password, req.body.firstName, req.body.lastName, req.body.role, req.body.profilePicUrl); 42 | res.json(new UserDTO(user)); 43 | } catch (error) { 44 | next(error); 45 | } 46 | } 47 | 48 | public async put(req: express.Request, res: express.Response, next: express.NextFunction) { 49 | try { 50 | const user = await this.userManager.updateUser(req.params.id, req.body.email, req.body.firstName, req.body.lastName, req.body.role, req.body.profilePicUrl); 51 | res.json(new UserDTO(user)); 52 | } catch (error) { 53 | next(error); 54 | } 55 | } 56 | 57 | public async delete(req: express.Request, res: express.Response, next: express.NextFunction) { 58 | try { 59 | const user = await this.userManager.deleteUser(req.params.id); 60 | res.json(user); 61 | } catch (error) { 62 | next(error); 63 | } 64 | } 65 | 66 | public async getByToken(req: express.Request, res: express.Response, next: express.NextFunction) { 67 | try { 68 | res.json(new UserDTO(req.user)); 69 | } catch (error) { 70 | next(error); 71 | } 72 | } 73 | 74 | public async changePassword(req: express.Request, res: express.Response, next: express.NextFunction) { 75 | try { 76 | const user = await this.userManager.updatePassword(req.params.id, req.body.currentPassword, req.body.newPassword); 77 | res.json(new UserDTO(user)); 78 | } catch (error) { 79 | next(error); 80 | } 81 | } 82 | 83 | public async uploadProfileImage(req: express.Request, res: express.Response, next: express.NextFunction) { 84 | try { 85 | const user = await this.userManager.updateProfileImage(req.params.id, req.file); 86 | res.json(new UserDTO(user)); 87 | } catch (error) { 88 | next(error); 89 | } 90 | } 91 | 92 | private buildRoutes() { 93 | this.router.get("/", Auth.getBearerMiddleware(), Roles.connectRoles.can('modify user'), this.get.bind(this)); 94 | this.router.post("/", this.post.bind(this)); 95 | this.router.delete("/:id", Auth.getBearerMiddleware(), Roles.connectRoles.can('modify user'), this.delete.bind(this)); 96 | this.router.put("/:id", Auth.getBearerMiddleware(), Roles.connectRoles.can('modify user'), this.put.bind(this)); 97 | this.router.get("/current", Auth.getBearerMiddleware(), this.getByToken.bind(this)); 98 | this.router.put("/:id/password", Auth.getBearerMiddleware(), Roles.connectRoles.can('modify user'), this.changePassword.bind(this)); 99 | this.router.put("/:id/profileImage", Auth.getBearerMiddleware(), Roles.connectRoles.can('modify user'), this.uploadHandler.single('profileImage'), this.uploadProfileImage.bind(this)); 100 | } 101 | } -------------------------------------------------------------------------------- /routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import {ClientRouter} from "./ClientRouter"; 3 | import {AuthRouter} from "./AuthRouter"; 4 | import {UserRouter} from "./UserRouter"; 5 | 6 | export class Router { 7 | 8 | public static initializeRoutes(app: express.Express) { 9 | app.use('/clients', new ClientRouter().router); 10 | app.use('/oauth', new AuthRouter().router); 11 | app.use('/users', new UserRouter().router); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /seeders/20180304161652-seed-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.bulkInsert('User', [ 6 | { 7 | id: '87cf44fe-cc1f-4fa7-a936-d15dbb122bcc', 8 | firstName: 'Test', 9 | lastName: 'Admin', 10 | email: 'test.admin@gmail.com', 11 | password: '$2a$10$xYkomxBysjVimxtP7flrTee/iiueeZp5e/FRmu9NKyTpeLpS7O43a', 12 | role: 'admin', 13 | creationDate: '1986-07-16 04:05:06', 14 | updatedOn: '1999-01-08 04:05:06' 15 | }, 16 | { 17 | id: '436e49ad-3b04-40d6-b8da-f6d26f45ed17', 18 | firstName: 'Test', 19 | lastName: 'Member', 20 | email: 'test.member@gmail.com', 21 | password: '$2a$10$xYkomxBysjVimxtP7flrTee/iiueeZp5e/FRmu9NKyTpeLpS7O43a', 22 | role: 'member', 23 | creationDate: '1988-01-08 04:05:06', 24 | updatedOn: '1999-01-08 04:05:06' 25 | }], {}); 26 | }, 27 | 28 | down: (queryInterface, Sequelize) => { 29 | return queryInterface.bulkDelete('User', null, {}); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as compression from "compression"; 3 | import * as bodyParser from "body-parser"; 4 | import * as passport from "passport"; 5 | import * as config from "./config/config"; 6 | import * as http from "http"; 7 | import {Auth} from "./auth/auth"; 8 | import {Models} from "./models"; 9 | import {Router} from "./routes/index"; 10 | import {errorHandler} from "./errors/ErrorHandler"; 11 | import {Roles} from "./auth/roles"; 12 | import {MessageManager} from "./managers/MessageManager"; 13 | import {InternalServerError} from "./errors/InternalServerError"; 14 | import {logger} from "./lib/logger"; 15 | import morgan = require("morgan"); 16 | 17 | const amqplib = require('amqplib'); 18 | 19 | export class Server { 20 | 21 | public static app: express.Express; 22 | 23 | // TODO Make all of this async 24 | constructor() {} 25 | 26 | public static async initializeApp(): Promise { 27 | try { 28 | require('dotenv').config(); 29 | 30 | Server.app = express(); 31 | 32 | // Configure application 33 | Server.configureApp(); 34 | 35 | // Initialize OAuth 36 | Server.initializeAuth(); 37 | 38 | // Initialize role based security 39 | Server.initializeRoles(); 40 | 41 | // Initialize Routes 42 | Router.initializeRoutes(Server.app); 43 | 44 | // Initialize RabbitMQ Connection 45 | amqplib.connect('amqp://localhost').then(connection => { 46 | MessageManager.connection = connection; 47 | }, () => { 48 | logger.error("Could not initialize RabbitMQ Server"); 49 | throw new InternalServerError("Cannot connect to RabbitMQ Server"); 50 | }); 51 | 52 | Server.app.use(errorHandler); 53 | 54 | process.on('unhandledRejection', (reason, p) => { 55 | logger.error('Unhandled Rejection at: Promise', p, 'reason:', reason); 56 | }); 57 | 58 | // Initialize Database then bootstrap application 59 | try { 60 | await Server.initializeDatabase(); 61 | } catch(error) { 62 | logger.error("Failed to initialize database", error); 63 | } 64 | 65 | return Server.app.listen(Server.app.get("port")); 66 | 67 | } catch(error) { 68 | throw new InternalServerError(error.message); 69 | } 70 | 71 | } 72 | 73 | private static initializeDatabase() { 74 | const nodeEnv = process.env.NODE_ENV; 75 | if(nodeEnv) { 76 | const sequelizeConfig = config[nodeEnv]; 77 | const models = new Models(sequelizeConfig); 78 | return models.initModels(); 79 | } else { 80 | throw new InternalServerError("No NODE ENV set"); 81 | } 82 | } 83 | 84 | private static initializeAuth() { 85 | Server.app.use(passport.initialize()); 86 | Auth.serializeUser(); 87 | Auth.useBasicStrategy(); 88 | Auth.useBearerStrategy(); 89 | Auth.useLocalStrategy(); 90 | Auth.useFacebookTokenStrategy(); 91 | } 92 | 93 | private static initializeRoles() { 94 | Roles.buildRoles(); 95 | Server.app.use(Roles.middleware()); 96 | } 97 | 98 | private static configureApp() { 99 | 100 | // all environments 101 | Server.app.set("port", process.env.PORT || 3000); 102 | Server.app.use(bodyParser.urlencoded({ extended: true })); 103 | Server.app.use(bodyParser.json()); 104 | Server.app.use(compression()); 105 | Server.app.use(morgan('dev', { 106 | skip: function (req, res) { 107 | return res.statusCode < 400; 108 | }, stream: process.stderr 109 | })); 110 | 111 | Server.app.use(morgan('dev', { 112 | skip: function (req, res) { 113 | return res.statusCode >= 400; 114 | }, stream: process.stdout 115 | })); 116 | } 117 | } -------------------------------------------------------------------------------- /tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import {Server} from "../server"; 2 | import request = require("supertest"); 3 | import {UserManager} from "../managers/UserManager"; 4 | import {RoleEnum} from "../models/enums/RoleEnum"; 5 | import {User} from "../models"; 6 | 7 | export class TestHelper { 8 | 9 | static async initializeTestSuite() { 10 | try { 11 | return await Server.initializeApp(); 12 | } catch (error) { 13 | throw error; 14 | } 15 | } 16 | 17 | static async getAuthToken(role: RoleEnum, email?: string): Promise { 18 | try { 19 | let userName; 20 | if(email) { 21 | userName = email; 22 | } else { 23 | userName = await TestHelper.getEmailForRole(role); 24 | } 25 | console.log('USERNAME: ', userName); 26 | const response = await request(Server.app) 27 | .post('/oauth/token') 28 | .send({ 29 | 'username': userName, 30 | 'password': 'password', 31 | 'grant_type': 'password' 32 | }) 33 | .set('Accept', 'application/json'); 34 | 35 | expect(response.body).toEqual(expect.objectContaining({ 36 | access_token: expect.any(String) 37 | })); 38 | return response.body.access_token; 39 | } catch (error) { 40 | throw error; 41 | } 42 | } 43 | 44 | static async createMember(email?: string): Promise { 45 | const userManager = new UserManager(); 46 | return userManager.createUser(email || 'test.member@gmail.com', 'password', 'Test', 'Member', RoleEnum.MEMBER); 47 | } 48 | 49 | static async createAdmin(): Promise { 50 | const userManager = new UserManager(); 51 | return userManager.createUser('test.admin@gmail.com', 'password', 'Test', 'Admin', RoleEnum.ADMIN); 52 | } 53 | 54 | private static async getEmailForRole(role: RoleEnum): Promise { 55 | switch(role) { 56 | case RoleEnum.ADMIN: 57 | return 'test.admin@gmail.com'; 58 | case RoleEnum.MEMBER: 59 | return 'test.member@gmail.com'; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /tests/users.test.ts: -------------------------------------------------------------------------------- 1 | import {Server} from "../server"; 2 | import {TestHelper} from "./test-helper"; 3 | import {User} from "../models/entities/User"; 4 | import * as http from "http"; 5 | 6 | const request = require('supertest'); 7 | let testServer: http.Server; 8 | let memberToken: string; 9 | let adminToken: string; 10 | let member: User; 11 | let admin: User; 12 | 13 | beforeAll(async () => { 14 | testServer = await TestHelper.initializeTestSuite(); 15 | member = await TestHelper.createMember(); 16 | admin = await TestHelper.createAdmin(); 17 | adminToken = await TestHelper.getAuthToken(admin.role); 18 | memberToken = await TestHelper.getAuthToken(member.role); 19 | }); 20 | 21 | describe('Get all users', () => { 22 | test('should respond with all users', async () => { 23 | console.log('ADMIN TOKEN: ', adminToken); 24 | const response = await request(Server.app) 25 | .get('/users') 26 | .set('Authorization', 'Bearer ' + adminToken) 27 | .set('Accept', 'application/json'); 28 | expect(response.statusCode).toBe(200); 29 | expect(response.body).toHaveLength(2); 30 | }); 31 | 32 | it('should respond with 403', async function () { 33 | const response = await request(Server.app) 34 | .get('/users') 35 | .set('Authorization', 'Bearer ' + memberToken) 36 | .set('Accept', 'application/json'); 37 | expect(response.statusCode).toBe(403); 38 | }); 39 | }); 40 | 41 | describe('Get user by email', () => { 42 | test('should respond with a member when member requests itself', async () => { 43 | const response = await request(Server.app) 44 | .get(`/users?email=${member.email}`) 45 | .set('Authorization', 'Bearer ' + memberToken) 46 | .set('Accept', 'application/json'); 47 | expect(response.statusCode).toBe(200); 48 | expect(response.body).toEqual(expect.objectContaining({ 49 | id: expect.any(String), 50 | firstName: 'Test', 51 | lastName: 'Member', 52 | email: 'test.member@gmail.com', 53 | profilePicUrl: null 54 | })); 55 | }); 56 | 57 | test('should respond with a member when admin requests member', async () => { 58 | const response = await request(Server.app) 59 | .get(`/users?email=${member.email}`) 60 | .set('Authorization', 'Bearer ' + adminToken) 61 | .set('Accept', 'application/json'); 62 | expect(response.statusCode).toBe(200); 63 | expect(response.body).toEqual(expect.objectContaining({ 64 | id: expect.any(String), 65 | firstName: 'Test', 66 | lastName: 'Member', 67 | email: 'test.member@gmail.com', 68 | profilePicUrl: null 69 | })); 70 | }); 71 | 72 | test('should respond with a forbidden error when member requests admin', async () => { 73 | const response = await request(Server.app) 74 | .get(`/users?email=test.admin@gmail.com`) 75 | .set('Authorization', 'Bearer ' + memberToken) 76 | .set('Accept', 'application/json'); 77 | expect(response.statusCode).toBe(403); 78 | }); 79 | }); 80 | 81 | describe('Create New User', () => { 82 | test('should respond with a newly created user', async () => { 83 | const newUser = { 84 | "firstName": "Stephanie", 85 | "lastName": "Tipper", 86 | "profilePicUrl": "", 87 | "email": "tipper@gmail.com", 88 | "password": "password", 89 | "role": "member" 90 | }; 91 | const response = await request(Server.app) 92 | .post('/users') 93 | .type('json') 94 | .set('Authorization', 'Bearer ' + adminToken) 95 | .set('Accept', 'application/json') 96 | .send(newUser); 97 | expect(response.statusCode).toBe(200); 98 | expect(response.body).toEqual(expect.objectContaining({ 99 | id: expect.any(String), 100 | firstName: "Stephanie", 101 | lastName: "Tipper", 102 | profilePicUrl: "", 103 | email: "tipper@gmail.com" 104 | })); 105 | }); 106 | 107 | test('should fail with email required', async () => { 108 | const newUser = { 109 | "firstName": "Stephanie", 110 | "lastName": "Tipper", 111 | "profilePicUrl": "", 112 | "password": "password", 113 | "role": "member" 114 | }; 115 | const response = await request(Server.app) 116 | .post('/users') 117 | .type('json') 118 | .set('Authorization', 'Bearer ' + adminToken) 119 | .set('Accept', 'application/json') 120 | .send(newUser); 121 | expect(response.statusCode).toBe(400); 122 | }); 123 | }); 124 | 125 | describe('Get Current User', () => { 126 | test('member should be able retrieve himself', async () => { 127 | const response = await request(Server.app) 128 | .get(`/users/current`) 129 | .set('Authorization', 'Bearer ' + memberToken) 130 | .set('Accept', 'application/json'); 131 | expect(response.statusCode).toBe(200); 132 | expect(response.body).toEqual(expect.objectContaining({ 133 | id: expect.any(String), 134 | firstName: 'Test', 135 | lastName: 'Member', 136 | email: 'test.member@gmail.com', 137 | profilePicUrl: null 138 | })); 139 | }); 140 | }); 141 | 142 | describe('Change Password', () => { 143 | test('member should be able to change his own password', async () => { 144 | const response = await request(Server.app) 145 | .put(`/users/${member.id}/password`) 146 | .type('json') 147 | .set('Authorization', 'Bearer ' + memberToken) 148 | .set('Accept', 'application/json') 149 | .send({currentPassword: 'password', newPassword: 'newPassword'}); 150 | expect(response.statusCode).toBe(200); 151 | expect(response.body).toEqual(expect.objectContaining({ 152 | id: expect.any(String), 153 | firstName: 'Test', 154 | lastName: 'Member', 155 | email: 'test.member@gmail.com', 156 | profilePicUrl: null 157 | })); 158 | }); 159 | 160 | test('member should be able to retrieve token with new password', async () => { 161 | const response = await request(Server.app) 162 | .post('/oauth/token') 163 | .send({ 164 | 'username': member.email, 165 | 'password': 'newPassword', 166 | 'grant_type': 'password' 167 | }) 168 | .set('Accept', 'application/json'); 169 | expect(response.statusCode).toBe(200); 170 | expect(response.body).toEqual(expect.objectContaining({ 171 | access_token: expect.any(String) 172 | })); 173 | }); 174 | 175 | test('member should not be able to change admin password', async () => { 176 | const response = await request(Server.app) 177 | .put(`/users/${admin.id}/password`) 178 | .type('json') 179 | .set('Authorization', 'Bearer ' + memberToken) 180 | .set('Accept', 'application/json') 181 | .send({currentPassword: 'password', newPassword: 'newPassword'}); 182 | expect(response.statusCode).toBe(403); 183 | }); 184 | }); 185 | 186 | describe('Update User', () => { 187 | test('member should be able to update self', async () => { 188 | const response = await request(Server.app) 189 | .put(`/users/${member.id}`) 190 | .type('json') 191 | .set('Authorization', 'Bearer ' + memberToken) 192 | .set('Accept', 'application/json') 193 | .send({lastName: 'New Name'}); 194 | expect(response.statusCode).toBe(200); 195 | expect(response.body).toEqual(expect.objectContaining({ 196 | id: expect.any(String), 197 | firstName: "Test", 198 | lastName: "New Name", 199 | profilePicUrl: null, 200 | email: "test.member@gmail.com" 201 | })); 202 | }); 203 | 204 | test('member should not be able to update admin', async () => { 205 | const response = await request(Server.app) 206 | .put(`/users/${admin.id}`) 207 | .type('json') 208 | .set('Authorization', 'Bearer ' + memberToken) 209 | .set('Accept', 'application/json') 210 | .send({lastName: 'New Name'}); 211 | expect(response.statusCode).toBe(403); 212 | }); 213 | 214 | test('admin should be able to update member', async () => { 215 | const response = await request(Server.app) 216 | .put(`/users/${member.id}`) 217 | .type('json') 218 | .set('Authorization', 'Bearer ' + adminToken) 219 | .set('Accept', 'application/json') 220 | .send({lastName: 'New New Name'}); 221 | expect(response.statusCode).toBe(200); 222 | expect(response.body).toEqual(expect.objectContaining({ 223 | id: expect.any(String), 224 | firstName: "Test", 225 | lastName: "New New Name", 226 | profilePicUrl: null, 227 | email: "test.member@gmail.com" 228 | })); 229 | }); 230 | }); 231 | 232 | describe('Delete User', () => { 233 | 234 | test('member should not be able to delete admin', async () => { 235 | const response = await request(Server.app) 236 | .delete(`/users/${admin.id}`) 237 | .type('json') 238 | .set('Authorization', 'Bearer ' + memberToken) 239 | .set('Accept', 'application/json'); 240 | expect(response.statusCode).toBe(403); 241 | }); 242 | 243 | test('admin should be able to delete member', async () => { 244 | const response = await request(Server.app) 245 | .delete(`/users/${member.id}`) 246 | .type('json') 247 | .set('Authorization', 'Bearer ' + adminToken) 248 | .set('Accept', 'application/json'); 249 | expect(response.statusCode).toBe(200); 250 | }); 251 | }); 252 | 253 | afterAll(function () { 254 | testServer.close(); 255 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "baseUrl": ".", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "strictNullChecks": true 12 | } 13 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "one-line": [ 13 | true, 14 | "check-open-brace" 15 | ], 16 | "no-var-keyword": true, 17 | "semicolon": [ 18 | true, 19 | "always", 20 | "ignore-bound-class-methods" 21 | ], 22 | "typedef-whitespace": [ 23 | true, 24 | { 25 | "call-signature": "nospace", 26 | "index-signature": "nospace", 27 | "parameter": "nospace", 28 | "property-declaration": "nospace", 29 | "variable-declaration": "nospace" 30 | }, 31 | { 32 | "call-signature": "onespace", 33 | "index-signature": "onespace", 34 | "parameter": "onespace", 35 | "property-declaration": "onespace", 36 | "variable-declaration": "onespace" 37 | } 38 | ], 39 | "no-internal-module": true, 40 | "prefer-const": true, 41 | "jsdoc-format": true 42 | } 43 | } -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | 3 | export class Utils { 4 | 5 | /** 6 | * Return a random int, used by `utils.uid()` 7 | * 8 | * @param {Number} min 9 | * @param {Number} max 10 | * @return {Number} 11 | */ 12 | public static getRandomInt(min: number, max: number): number { 13 | return Math.floor(Math.random() * (max - min + 1)) + min; 14 | } 15 | 16 | public static encryptPassword(password: string): string { 17 | const salt = bcrypt.genSaltSync(10); 18 | return bcrypt.hashSync(password, salt); 19 | } 20 | } --------------------------------------------------------------------------------