├── .gitignore ├── models ├── index.js ├── account.js ├── organization.js └── user.js ├── src ├── utils │ ├── responder.js │ ├── cli.js │ ├── common.js │ ├── logger.js │ └── dbconnector.js ├── signup │ ├── signup_controller.js │ ├── signup_service.js │ └── signup_dataprovider.js ├── user │ ├── user_service.js │ ├── user_dataprovider.js │ └── user_controller.js └── organization │ ├── organization_service.js │ ├── organization_dataprovider.js │ └── organization_controller.js ├── routes ├── signup.js ├── auth.js ├── user.js └── organization.js ├── config └── config.json ├── migrations ├── 20200401090711-create-organization.js ├── 20200402081802-create-account.js └── 20200401090845-create-user.js ├── package.json ├── passport.js ├── app.js ├── bin └── www └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | log -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const dbConnector = require('../src/utils/dbconnector'); 3 | 4 | let dbRepo = {}; 5 | 6 | dbConnector.addSequelizeConnectionToRepo(dbRepo, 'default'); 7 | 8 | module.exports = dbRepo; 9 | -------------------------------------------------------------------------------- /src/utils/responder.js: -------------------------------------------------------------------------------- 1 | 2 | let Responder = { 3 | 4 | sendResponse: (response, statusCode, status, data, message) => { 5 | response.status(statusCode).json({ 6 | status: status, 7 | data: data, 8 | message: message 9 | }); 10 | } 11 | }; 12 | 13 | module.exports = Responder; 14 | -------------------------------------------------------------------------------- /routes/signup.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const signupController = require('../src/signup/signup_controller'); 4 | 5 | router.post('/', function (request, response, next) { 6 | signupController.newAccountSignup(request, response, next); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /src/utils/cli.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const exec = util.promisify(require('child_process').exec); 3 | 4 | let CLI = { 5 | executeCommand: async (command) => { 6 | return new Promise(function(resolve, reject) { 7 | exec(command, (error, stdout, stderr) => { 8 | if (error) { 9 | return reject(error); 10 | } 11 | resolve(true); 12 | }); 13 | }); 14 | } 15 | 16 | }; 17 | 18 | module.exports = CLI; 19 | -------------------------------------------------------------------------------- /src/signup/signup_controller.js: -------------------------------------------------------------------------------- 1 | const signupService = require('./signup_service'); 2 | const responder = require('../utils/responder'); 3 | 4 | let SignupController = { 5 | newAccountSignup: async (request, response, next) => { 6 | try { 7 | let body = request.body; 8 | let account = await signupService.newAccountSignup(body); 9 | responder.sendResponse(response, 200, "success", account, "Account created successfully."); 10 | } catch (error) { 11 | return next(error); 12 | } 13 | } 14 | }; 15 | 16 | module.exports = SignupController; 17 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": null, 5 | "database": "multi_tenant_poc_dev", 6 | "host": "127.0.0.1", 7 | "dialect": "mysql" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "multi_tenant_poc_test", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | }, 16 | "production": { 17 | "username": "root", 18 | "password": null, 19 | "database": "multi_tenant_poc_prod", 20 | "host": "127.0.0.1", 21 | "dialect": "mysql" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /models/account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class Account extends Model { 5 | static associate(models) { } 6 | } 7 | Account.init( 8 | { 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | unique: true, 13 | }, 14 | domain: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | unique: true, 18 | }, 19 | owner: { 20 | type: DataTypes.STRING, 21 | allowNull: false, 22 | unique: true, 23 | } 24 | }, 25 | { 26 | sequelize, 27 | modelName: "Account", 28 | } 29 | ); 30 | return Account; 31 | }; -------------------------------------------------------------------------------- /models/organization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class Organization extends Model { 5 | static associate(models) { 6 | Organization.hasMany(models.User, { 7 | foreignKey: "organizationId", 8 | }); 9 | } 10 | } 11 | Organization.init( 12 | { 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | unique: true 17 | }, 18 | domain: { 19 | type: DataTypes.STRING, 20 | allowNull: false, 21 | unique: true 22 | } 23 | }, 24 | { 25 | sequelize, 26 | modelName: "Organization", 27 | } 28 | ); 29 | return Organization; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | const dbRepo = require('../../models'); 2 | const dbConnector = require('./dbconnector'); 3 | const signupDataProvider = require('../signup/signup_dataprovider'); 4 | 5 | let Common = { 6 | 7 | getDBKeyFromRequest: async (request) => { 8 | let tenant_id = request.headers['x-tenant-id']; 9 | let dbKey = 'default'; 10 | if(tenant_id) { 11 | dbKey = `tenant_${tenant_id}`; 12 | } 13 | if(!dbRepo[dbKey]) { 14 | let account = await signupDataProvider.getAccount(tenant_id); 15 | if(account) { 16 | dbConnector.addSequelizeConnectionToRepo(dbRepo, dbKey); 17 | } else { 18 | dbKey = 'default'; 19 | } 20 | } 21 | return dbKey; 22 | } 23 | }; 24 | 25 | module.exports = Common; 26 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const jwt = require('jsonwebtoken'); 4 | const passport = require('passport'); 5 | 6 | router.post('/login', function (req, res, next) { 7 | 8 | passport.authenticate('local', {session: false}, (err, user, info) => { 9 | if (err || !user) { 10 | return res.status(400).json({ 11 | message: info ? info.message : 'Login failed', 12 | user : user 13 | }); 14 | } 15 | 16 | req.login(user, {session: false}, (err) => { 17 | if (err) { 18 | res.send(err); 19 | } 20 | 21 | const token = jwt.sign(user, "your_jwt_secret"); 22 | 23 | return res.json({user, token}); 24 | }); 25 | })(req, res); 26 | }); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const userController = require('../src/user/user_controller'); 4 | 5 | router.get('/', function (request, response, next) { 6 | userController.getUsers(request, response, next); 7 | }); 8 | 9 | router.get('/:userId', function (request, response, next) { 10 | userController.getUser(request, response, next); 11 | }); 12 | 13 | router.post('/', function (request, response, next) { 14 | userController.createUser(request, response, next); 15 | }); 16 | 17 | router.put('/:userId', function (request, response, next) { 18 | userController.updateUser(request, response, next); 19 | }); 20 | 21 | router.delete('/:userId', function (request, response, next) { 22 | userController.deleteUser(request, response, next); 23 | }); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /src/user/user_service.js: -------------------------------------------------------------------------------- 1 | const userDataProvider = require('./user_dataprovider'); 2 | 3 | let UserService = { 4 | 5 | getUsers: async(dbKey) => { 6 | let users = await userDataProvider.getUsers(dbKey); 7 | return users; 8 | }, 9 | 10 | getUser: async(userId, dbKey) => { 11 | let user = await userDataProvider.getUser(userId, dbKey); 12 | return user; 13 | }, 14 | 15 | createUser: async(body, dbKey) => { 16 | let user = await userDataProvider.createUser(body, dbKey); 17 | return user; 18 | }, 19 | 20 | updateUser: async(dbKey) => { 21 | let user = await userDataProvider.updateUser(); 22 | return user; 23 | }, 24 | 25 | deleteUser: async(userId, dbKey) => { 26 | let user = await userDataProvider.deleteUser(userId, dbKey); 27 | return user; 28 | } 29 | 30 | }; 31 | 32 | module.exports = UserService; 33 | -------------------------------------------------------------------------------- /migrations/20200401090711-create-organization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Organizations', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | allowNull: false, 13 | unique: true, 14 | type: Sequelize.STRING 15 | }, 16 | domain: { 17 | allowNull: false, 18 | unique: true, 19 | type: Sequelize.STRING 20 | }, 21 | createdAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | }, 25 | updatedAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | } 29 | }); 30 | }, 31 | down: (queryInterface, Sequelize) => { 32 | return queryInterface.dropTable('Organizations'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /routes/organization.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const organizationController = require('../src/organization/organization_controller'); 4 | 5 | router.get('/', function (request, response, next) { 6 | organizationController.getOrganizations(request, response, next); 7 | }); 8 | 9 | router.get('/:organizationId', function (request, response, next) { 10 | organizationController.getOrganization(request, response, next); 11 | }); 12 | 13 | router.post('/', function (request, response, next) { 14 | organizationController.createOrganization(request, response, next); 15 | }); 16 | 17 | router.put('/:organizationId', function (request, response, next) { 18 | organizationController.updateOrganization(request, response, next); 19 | }); 20 | 21 | router.delete('/:organizationId', function (request, response, next) { 22 | organizationController.deleteOrganization(request, response, next); 23 | }); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class User extends Model { 5 | static associate(models) { 6 | User.belongsTo(models.Organization, { 7 | foreignKey: "organizationId", 8 | }); 9 | } 10 | } 11 | User.init( 12 | { 13 | firstName: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | lastName: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | email: { 22 | type: DataTypes.STRING, 23 | allowNull: false, 24 | unique: true 25 | }, 26 | password: { 27 | type: DataTypes.STRING, 28 | allowNull: false 29 | }, 30 | isSuperAdmin: { 31 | type: DataTypes.BOOLEAN, 32 | defaultValue: false 33 | } 34 | }, 35 | { 36 | sequelize, 37 | modelName: "User", 38 | } 39 | ); 40 | return User; 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const { splat, combine, timestamp, printf, colorize, label, level} = winston.format; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const myFormat = printf(({ timestamp, level, label, message, meta }) => { 7 | return `${timestamp} ${level} [${label}] ${message} ${meta? JSON.stringify(meta) : ''}`; 8 | }); 9 | 10 | const logDir = 'log'; 11 | if (!fs.existsSync(logDir)) { 12 | fs.mkdirSync(logDir); 13 | } 14 | const filename = path.join(logDir, 'server.log'); 15 | 16 | const logger = winston.createLogger({ 17 | format: combine( 18 | timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 19 | label({ label: path.basename(module.parent.filename) }), 20 | splat(), 21 | colorize(), 22 | myFormat 23 | ), 24 | transports: [ 25 | new winston.transports.Console(), 26 | new winston.transports.File({ filename }) 27 | ] 28 | }); 29 | 30 | module.exports = logger; 31 | -------------------------------------------------------------------------------- /migrations/20200402081802-create-account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Accounts', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | allowNull: false, 13 | unique: true, 14 | type: Sequelize.STRING 15 | }, 16 | domain: { 17 | allowNull: false, 18 | unique: true, 19 | type: Sequelize.STRING 20 | }, 21 | owner: { 22 | allowNull: false, 23 | type: Sequelize.STRING 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable('Accounts'); 37 | } 38 | }; -------------------------------------------------------------------------------- /src/signup/signup_service.js: -------------------------------------------------------------------------------- 1 | const signupDataProvider = require('./signup_dataprovider'); 2 | const userDataProvider = require('../user/user_dataprovider'); 3 | const logger = require('../utils/logger'); 4 | 5 | let SignupService = { 6 | 7 | newAccountSignup: async (body) => { 8 | logger.info(`Create Account in DB[Name: ${body.name}, Domain: ${body.domain}]`); 9 | let account = await signupDataProvider.createAccount({ "name": body.name, "domain": body.domain, "owner": body.email }); 10 | 11 | logger.info(`Create Tenant for Account[ID: ${account.id}]`); 12 | await signupDataProvider.createTenantDB(account.id); 13 | 14 | logger.info(`Add User to Tenant DB[Email: ${body.email}]`); 15 | await userDataProvider.createUser({ 16 | "firstName": body.firstName, 17 | "lastName": body.lastName, 18 | "email": body.email, 19 | "password": body.password, 20 | "isSuperAdmin": true 21 | }, `tenant_${account.id}`); 22 | 23 | return account; 24 | } 25 | }; 26 | 27 | module.exports = SignupService; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-tenant-node-app", 3 | "version": "1.0.0", 4 | "description": "Multi-Tenant Node App", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon ./bin/www", 8 | "test": "mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/daniccan/multi-tenant-node-app.git" 13 | }, 14 | "keywords": [ 15 | "Multi-tenant", 16 | "Boilerplate" 17 | ], 18 | "author": "daniccan", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/daniccan/multi-tenant-node-app/issues" 22 | }, 23 | "homepage": "https://github.com/daniccan/multi-tenant-node-app#readme", 24 | "dependencies": { 25 | "body-parser": "^1.20.0", 26 | "express": "^4.18.1", 27 | "jsonwebtoken": "^8.5.1", 28 | "morgan": "^1.10.0", 29 | "mysql2": "^2.3.3", 30 | "nodemon": "^2.0.20", 31 | "passport": "^0.6.0", 32 | "passport-jwt": "^4.0.0", 33 | "passport-local": "^1.0.0", 34 | "sequelize": "^6.23.0", 35 | "sequelize-cli": "^6.4.1", 36 | "sqlite3": "^5.1.1", 37 | "winston": "^3.8.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/organization/organization_service.js: -------------------------------------------------------------------------------- 1 | const organizationDataProvider = require('./organization_dataprovider'); 2 | 3 | let OrganizationService = { 4 | 5 | getOrganizations: async(dbKey) => { 6 | let organizations = await organizationDataProvider.getOrganizations(dbKey); 7 | return organizations; 8 | }, 9 | 10 | getOrganization: async(organizationId, dbKey) => { 11 | let organization = await organizationDataProvider.getOrganization(organizationId, dbKey); 12 | return organization; 13 | }, 14 | 15 | createOrganization: async(body, dbKey) => { 16 | let organization = await organizationDataProvider.createOrganization(body, dbKey); 17 | return organization; 18 | }, 19 | 20 | updateOrganization: async(dbKey) => { 21 | let organization = await organizationDataProvider.updateOrganization(); 22 | return organization; 23 | }, 24 | 25 | deleteOrganization: async(organizationId, dbKey) => { 26 | let organization = await organizationDataProvider.deleteOrganization(organizationId, dbKey); 27 | return organization; 28 | } 29 | 30 | }; 31 | 32 | module.exports = OrganizationService; -------------------------------------------------------------------------------- /migrations/20200401090845-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Users', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | firstName: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | lastName: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | email: { 20 | allowNull: false, 21 | unique: true, 22 | type: Sequelize.STRING 23 | }, 24 | password: { 25 | allowNull: false, 26 | type: Sequelize.STRING 27 | }, 28 | isSuperAdmin: { 29 | defautValue: false, 30 | type: Sequelize.BOOLEAN 31 | }, 32 | organizationId: { 33 | references: { 34 | model: 'Organizations', 35 | key: 'id' 36 | }, 37 | onUpdate: 'CASCADE', 38 | onDelete: 'SET NULL', 39 | type: Sequelize.INTEGER 40 | }, 41 | createdAt: { 42 | allowNull: false, 43 | type: Sequelize.DATE 44 | }, 45 | updatedAt: { 46 | allowNull: false, 47 | type: Sequelize.DATE 48 | } 49 | }); 50 | }, 51 | down: (queryInterface, Sequelize) => { 52 | return queryInterface.dropTable('Users'); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/dbconnector.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const env = process.env.NODE_ENV || 'development'; 5 | const config = require(__dirname + '/../../config/config.json')[env]; 6 | const modelsDir = path.resolve(__dirname + '/../../models'); 7 | 8 | let DBConnector = { 9 | 10 | addSequelizeConnectionToRepo: (dbRepo, dbKey) => { 11 | const db = {}; 12 | 13 | let sequelize; 14 | if (dbKey === 'default') { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } else { 17 | sequelize = new Sequelize(dbKey, config.username, config.password, config); 18 | } 19 | 20 | fs 21 | .readdirSync(modelsDir) 22 | .filter(file => { 23 | return (file.indexOf('.') !== 0) && (file !== 'index.js') && (file.slice(-3) === '.js'); 24 | }) 25 | .forEach(file => { 26 | const model = require(path.join(modelsDir, file))( 27 | sequelize, 28 | Sequelize.DataTypes 29 | ); 30 | db[model.name] = model; 31 | }); 32 | 33 | Object.keys(db).forEach(modelName => { 34 | if (db[modelName].associate) { 35 | db[modelName].associate(db); 36 | } 37 | }); 38 | 39 | db.sequelize = sequelize; 40 | db.Sequelize = Sequelize; 41 | 42 | dbRepo[dbKey] = db; 43 | 44 | return dbRepo; 45 | } 46 | }; 47 | 48 | module.exports = DBConnector; -------------------------------------------------------------------------------- /passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const passportJWT = require('passport-jwt'); 3 | const ExtractJWT = passportJWT.ExtractJwt; 4 | 5 | const JWTStrategy = passportJWT.Strategy; 6 | const LocalStrategy = require('passport-local').Strategy; 7 | 8 | const dbRepo = require('./models'); 9 | 10 | const common = require('./src/utils/common'); 11 | 12 | passport.use(new LocalStrategy( 13 | { 14 | usernameField: 'email', 15 | passwordField: 'password', 16 | passReqToCallback: true 17 | }, 18 | async function(request, email, password, done) { 19 | let dbKey = await common.getDBKeyFromRequest(request); 20 | const User = dbRepo[dbKey].User; 21 | return User.findOne({ where: { email: email, password: password }, attributes: { exclude: ['password'] } }) 22 | .then(user => { 23 | if(!user) { 24 | return done(null, false, {message: 'Invalid email or password.'}); 25 | } 26 | return done(null, user.dataValues, {message: 'Logged In Successfully.'}); 27 | }).catch(err => done(err)); 28 | } 29 | )); 30 | 31 | passport.use(new JWTStrategy( 32 | { 33 | jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), 34 | secretOrKey: 'your_jwt_secret', 35 | passReqToCallback: true 36 | }, 37 | async function (request, jwtPayload, done) { 38 | let dbKey = await common.getDBKeyFromRequest(request); 39 | const User = dbRepo[dbKey].User; 40 | return User.findByPk(jwtPayload.id) 41 | .then(user => { 42 | return done(null, user); 43 | }) 44 | .catch(err => { 45 | return done(err); 46 | }); 47 | } 48 | )); 49 | -------------------------------------------------------------------------------- /src/user/user_dataprovider.js: -------------------------------------------------------------------------------- 1 | const dbRepo = require('../../models'); 2 | 3 | let UserDataProvider = { 4 | 5 | getUsers: async(dbKey) => { 6 | const User = dbRepo[dbKey].User; 7 | return new Promise(function(resolve, reject) { 8 | User.findAll({ attributes: { exclude: ['password'] } }) 9 | .then(data => { 10 | resolve(data); 11 | }).catch(err => { 12 | reject(err); 13 | }); 14 | }); 15 | }, 16 | 17 | getUser: async(userId, dbKey) => { 18 | const User = dbRepo[dbKey].User; 19 | return new Promise(function(resolve, reject) { 20 | User.findOne({ where: { id: userId }, attributes: { exclude: ['password'] } }) 21 | .then(data => { 22 | resolve(data); 23 | }).catch(err => { 24 | reject(err); 25 | }); 26 | }); 27 | }, 28 | 29 | createUser: async(body, dbKey) => { 30 | const User = dbRepo[dbKey].User; 31 | return new Promise(function(resolve, reject) { 32 | User.create(body) 33 | .then(data => { 34 | resolve(data); 35 | }).catch(err => { 36 | reject(err); 37 | }); 38 | }); 39 | }, 40 | 41 | updateUser: async() => { 42 | return null; 43 | }, 44 | 45 | deleteUser: async(userId, dbKey) => { 46 | const User = dbRepo[dbKey].User; 47 | return new Promise(function(resolve, reject) { 48 | User.destroy({ where: { id: userId } }) 49 | .then(data => { 50 | resolve(data); 51 | }).catch(err => { 52 | reject(err); 53 | }); 54 | }); 55 | } 56 | 57 | }; 58 | 59 | module.exports = UserDataProvider; 60 | -------------------------------------------------------------------------------- /src/organization/organization_dataprovider.js: -------------------------------------------------------------------------------- 1 | const dbRepo = require('../../models'); 2 | 3 | let OrganizationDataProvider = { 4 | 5 | getOrganizations: async(dbKey) => { 6 | const Organization = dbRepo[dbKey].Organization; 7 | return new Promise(function(resolve, reject) { 8 | Organization.findAll() 9 | .then(data => { 10 | resolve(data); 11 | }).catch(err => { 12 | reject(err); 13 | }); 14 | }); 15 | }, 16 | 17 | getOrganization: async(organizationId, dbKey) => { 18 | const Organization = dbRepo[dbKey].Organization; 19 | return new Promise(function(resolve, reject) { 20 | Organization.findOne({ where: { id: organizationId } }) 21 | .then(data => { 22 | resolve(data); 23 | }).catch(err => { 24 | reject(err); 25 | }); 26 | }); 27 | }, 28 | 29 | createOrganization: async(body, dbKey) => { 30 | const Organization = dbRepo[dbKey].Organization; 31 | return new Promise(function(resolve, reject) { 32 | Organization.create(body) 33 | .then(data => { 34 | resolve(data); 35 | }).catch(err => { 36 | reject(err); 37 | }); 38 | }); 39 | }, 40 | 41 | updateOrganization: async() => { 42 | return null; 43 | }, 44 | 45 | deleteOrganization: async(organizationId, dbKey) => { 46 | const Organization = dbRepo[dbKey].Organization; 47 | return new Promise(function(resolve, reject) { 48 | Organization.destroy({ where: { id: organizationId } }) 49 | .then(data => { 50 | resolve(data); 51 | }).catch(err => { 52 | reject(err); 53 | }); 54 | }); 55 | } 56 | 57 | }; 58 | 59 | module.exports = OrganizationDataProvider; 60 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const logger = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | const signupRouter = require('./routes/signup'); 6 | const authRouter = require('./routes/auth'); 7 | const userRouter = require('./routes/user'); 8 | const organizationRouter = require('./routes/organization'); 9 | const passport = require('passport'); 10 | require('./passport'); 11 | 12 | const app = express(); 13 | 14 | app.use(logger('dev')); 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: false })); 17 | app.use(bodyParser.json()); 18 | app.use(bodyParser.urlencoded({ extended: true })); 19 | 20 | // allow-cors 21 | app.use(function (req, res, next) { 22 | res.header("Access-Control-Allow-Origin", "*"); 23 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 24 | res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); 25 | if ('OPTIONS' === req.method) { 26 | //respond with 200 27 | res.send(200); 28 | } 29 | else { 30 | //move on 31 | next(); 32 | } 33 | }); 34 | 35 | app.use('/api/v1/accounts/signup', signupRouter); 36 | app.use('/api/v1/auth', authRouter); 37 | app.use('/api/v1/users', passport.authenticate('jwt', {session: false}), userRouter); 38 | app.use('/api/v1/organizations', passport.authenticate('jwt', {session: false}), organizationRouter); 39 | 40 | // catch 404 and forward to error handler 41 | app.use(function (req, res, next) { 42 | next(createError(404)); 43 | }); 44 | 45 | // error handler 46 | app.use(function (err, req, res, next) { 47 | // render the error page 48 | res.status(err.status || 500); 49 | res.json({ 50 | status: 'error', 51 | data: err.message, 52 | message: 'Something went wrong!!! Please try again later.' 53 | }); 54 | }); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('app.multi-tenant-node-app.api:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } -------------------------------------------------------------------------------- /src/signup/signup_dataprovider.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const env = process.env.NODE_ENV || 'development'; 3 | const config = require('../../config/config.json')[env]; 4 | const migrationPath = path.resolve(__dirname.replace(/ /g, '\\ ') + '/../../migrations'); 5 | 6 | const dbRepo = require('../../models'); 7 | const Account = dbRepo['default'].Account; 8 | 9 | const cli = require('../utils/cli'); 10 | const logger = require('../utils/logger'); 11 | const dbConnector = require('../utils/dbconnector'); 12 | 13 | let SignupDataProvider = { 14 | 15 | createAccount: async (account) => { 16 | return new Promise(function (resolve, reject) { 17 | Account.create(account) 18 | .then(data => { 19 | resolve(data); 20 | }).catch(err => { 21 | reject(err); 22 | }); 23 | }); 24 | }, 25 | 26 | createTenantDB: async (accountId) => { 27 | let connectionString = `${config.dialect}://${config.username}:${config.password}@${config.host}/tenant_${accountId}`; 28 | if (!config.password) { 29 | connectionString = `${config.dialect}://${config.username}@${config.host}/tenant_${accountId}`; 30 | } 31 | 32 | logger.info(`Create Database for Tenant[Name: tenant_${accountId}]`); 33 | await cli.executeCommand(`npx sequelize db:create --url ${connectionString}`); 34 | 35 | logger.info(`Run Migrations on Tenant Database[Name: tenant_${accountId}]`); 36 | await cli.executeCommand(`npx sequelize db:migrate --url ${connectionString} --migrations-path=${migrationPath}`); 37 | 38 | dbConnector.addSequelizeConnectionToRepo(dbRepo, `tenant_${accountId}`); 39 | }, 40 | 41 | getAccount: async (accountId) => { 42 | return new Promise(function (resolve, reject) { 43 | Account.findOne({ where: { id: accountId } }) 44 | .then(data => { 45 | resolve(data); 46 | }).catch(err => { 47 | reject(err); 48 | }); 49 | }); 50 | } 51 | }; 52 | 53 | module.exports = SignupDataProvider; 54 | -------------------------------------------------------------------------------- /src/user/user_controller.js: -------------------------------------------------------------------------------- 1 | const userService = require('./user_service'); 2 | const responder = require('../utils/responder'); 3 | const common = require('../utils/common'); 4 | 5 | let UserController = { 6 | 7 | getUsers: async (request, response, next) => { 8 | try { 9 | let dbKey = await common.getDBKeyFromRequest(request); 10 | let users = await userService.getUsers(dbKey); 11 | responder.sendResponse(response, 200, "success", users, "Users retrieved successfully."); 12 | } catch (error) { 13 | return next(error); 14 | } 15 | }, 16 | 17 | getUser: async (request, response, next) => { 18 | try { 19 | let dbKey = await common.getDBKeyFromRequest(request); 20 | let userId = request.params.userId; 21 | let user = await userService.getUser(userId, dbKey); 22 | responder.sendResponse(response, 200, "success", user, "User retrieved successfully."); 23 | } catch (error) { 24 | return next(error); 25 | } 26 | }, 27 | 28 | createUser: async (request, response, next) => { 29 | try { 30 | let dbKey = await common.getDBKeyFromRequest(request); 31 | let body = request.body; 32 | let user = await userService.createUser(body, dbKey); 33 | responder.sendResponse(response, 200, "success", user, "User created successfully."); 34 | } catch (error) { 35 | return next(error); 36 | } 37 | }, 38 | 39 | updateUser: async (request, response, next) => { 40 | try { 41 | let dbKey = await common.getDBKeyFromRequest(request); 42 | let user = await userService.updateUser(dbKey); 43 | response.status(200).json({ 44 | status: "success", 45 | data: user, 46 | message: "User updated successfully." 47 | }); 48 | } catch (error) { 49 | return next(error); 50 | } 51 | }, 52 | 53 | deleteUser: async (request, response, next) => { 54 | try { 55 | let dbKey = await common.getDBKeyFromRequest(request); 56 | let userId = request.params.userId; 57 | let user = await userService.deleteUser(userId, dbKey); 58 | responder.sendResponse(response, 200, "success", user, "User deleted successfully."); 59 | } catch (error) { 60 | return next(error); 61 | } 62 | } 63 | 64 | }; 65 | 66 | module.exports = UserController; 67 | -------------------------------------------------------------------------------- /src/organization/organization_controller.js: -------------------------------------------------------------------------------- 1 | const organizationService = require('./organization_service'); 2 | const responder = require('../utils/responder'); 3 | const common = require('../utils/common'); 4 | 5 | let OrganizationController = { 6 | 7 | getOrganizations: async (request, response, next) => { 8 | try { 9 | let dbKey = await common.getDBKeyFromRequest(request); 10 | let organizations = await organizationService.getOrganizations(dbKey); 11 | responder.sendResponse(response, 200, "success", organizations, "Organizations retrieved successfully."); 12 | } catch (error) { 13 | return next(error); 14 | } 15 | }, 16 | 17 | getOrganization: async (request, response, next) => { 18 | try { 19 | let dbKey = await common.getDBKeyFromRequest(request); 20 | let organizationId = request.params.organizationId; 21 | let organization = await organizationService.getOrganization(organizationId, dbKey); 22 | responder.sendResponse(response, 200, "success", organization, "Organization retrieved successfully."); 23 | } catch (error) { 24 | return next(error); 25 | } 26 | }, 27 | 28 | createOrganization: async (request, response, next) => { 29 | try { 30 | let dbKey = await common.getDBKeyFromRequest(request); 31 | let body = request.body; 32 | let organization = await organizationService.createOrganization(body, dbKey); 33 | responder.sendResponse(response, 200, "success", organization, "Organization created successfully."); 34 | } catch (error) { 35 | return next(error); 36 | } 37 | }, 38 | 39 | updateOrganization: async (request, response, next) => { 40 | try { 41 | let dbKey = await common.getDBKeyFromRequest(request); 42 | let organization = await organizationService.updateOrganization(dbKey); 43 | response.status(200).json({ 44 | status: "success", 45 | data: organization, 46 | message: "Organization updated successfully." 47 | }); 48 | } catch (error) { 49 | return next(error); 50 | } 51 | }, 52 | 53 | deleteOrganization: async (request, response, next) => { 54 | try { 55 | let dbKey = await common.getDBKeyFromRequest(request); 56 | let organizationId = request.params.organizationId; 57 | let organization = await organizationService.deleteOrganization(organizationId, dbKey); 58 | responder.sendResponse(response, 200, "success", organization, "Organization deleted successfully."); 59 | } catch (error) { 60 | return next(error); 61 | } 62 | } 63 | 64 | }; 65 | 66 | module.exports = OrganizationController; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multi-tenant-node-app 2 | 3 | ## What is this? 4 | 5 | This is a proof-of-concept (POC), multi-tenant [RESTful Web Services](https://restfulapi.net/) application built on top of [NodeJS](https://nodejs.org/en/). 6 | 7 | ## Get Started 8 | 9 | * Install dependencies. 10 | ``` 11 | npm install 12 | ``` 13 | * Verify the Database configuration in `config/config.json`. 14 | * Create DB using Sequelize. 15 | ``` 16 | npx sequelize-cli db:create 17 | ``` 18 | * Run migrations on the main database. 19 | ``` 20 | npx sequelize-cli db:migrate 21 | ``` 22 | * Start the application. 23 | ``` 24 | npm start 25 | ``` 26 | * Perform API requests. 27 | 28 | ## APIs 29 | 30 | ### HTTP Headers 31 | 32 | * For APIs other than `Signup` and `Authentication`, pass the `JWT Token` in the `Authorization` header of the request. 33 | 34 | ``` 35 | Authorization: Bearer 36 | ``` 37 | 38 | * For APIs other than `Signup`, pass the `Tenant ID` in the `X-TENANT-ID` custom header of the request. 39 | 40 | ``` 41 | X-TENANT-ID: 42 | ``` 43 | 44 | ### Signup 45 | 46 | * POST `/api/v1/accounts/signup` 47 | 48 | ``` 49 | { 50 | "name": "Alphabet", 51 | "domain": "alphabet.com", 52 | "firstName": "John", 53 | "lastName": "Doe", 54 | "email": "johndoe@alphabet.com", 55 | "password": "5f4dcc3b5aa765d61d8327deb882cf99" 56 | } 57 | ``` 58 | 59 | ### Authentication 60 | 61 | * POST `/api/v1/auth/login` 62 | 63 | ``` 64 | { 65 | "email": "johndoe@google.com", 66 | "password": "5f4dcc3b5aa765d61d8327deb882cf99" 67 | } 68 | ``` 69 | 70 | ### Organizations 71 | 72 | * GET `/api/v1/organizations` 73 | 74 | * GET `/api/v1/organizations/:organizationId` 75 | 76 | * POST `/api/v1/organizations` 77 | 78 | ``` 79 | { 80 | "name": "Google", 81 | "domain": "google.com" 82 | } 83 | ``` 84 | 85 | * DELETE `/api/v1/organizations/:organizationId` 86 | 87 | ### Users 88 | 89 | * GET `/api/v1/users` 90 | 91 | * GET `/api/v1/users/:userId` 92 | 93 | * POST `/api/v1/users` 94 | 95 | ``` 96 | { 97 | "firstName": "John", 98 | "lastName": "Doe", 99 | "email": "johndoe@google.com", 100 | "password": "5f4dcc3b5aa765d61d8327deb882cf99" 101 | } 102 | ``` 103 | 104 | * DELETE `/api/v1/users/:userId` 105 | 106 | ## Libraries 107 | 108 | * Core - [Express](https://www.npmjs.com/package/express) + [Body Parser](https://www.npmjs.com/package/body-parser) 109 | * ORM - [Sequelize](https://www.npmjs.com/package/sequelize) + [Sequelize CLI](https://www.npmjs.com/package/sequelize-cli) 110 | * Databases - [MySQL2](https://www.npmjs.com/package/mysql2) + [SQLite3](https://www.npmjs.com/package/sqlite3) 111 | * Logger - [Winston](https://www.npmjs.com/package/winston) + [Morgan](https://www.npmjs.com/package/morgan) 112 | * Monitoring - [Nodemon](https://www.npmjs.com/package/nodemon) 113 | * Authentication - [Passport](https://www.npmjs.com/package/passport) + [JWT](https://www.npmjs.com/package/passport-jwt) 114 | --------------------------------------------------------------------------------