From 8f588ffc511674b7c80e844a15903b8671c7bc3b Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 21 Apr 2020 22:47:27 +0200 Subject: [PATCH] WIP Multi-tenant architecture. --- server/config/config.js | 25 ++++ server/config/systemKnexfile.js | 20 +++ server/knexfile.js | 52 -------- server/package.json | 6 +- server/src/app.js | 3 +- server/src/database/knex.js | 2 +- server/src/database/manager.js | 17 +-- .../20190822214242_create_users_table.js | 4 - .../index.js => database/objection.js} | 0 .../http/controllers/AccountOpeningBalance.js | 8 +- server/src/http/controllers/AccountTypes.js | 2 +- server/src/http/controllers/Accounting.js | 81 +++++++++++- server/src/http/controllers/Accounts.js | 44 +++++-- server/src/http/controllers/Authentication.js | 115 +++++++++++++++--- server/src/http/controllers/Currencies.js | 20 ++- server/src/http/controllers/ExchangeRates.js | 27 ++-- server/src/http/controllers/Expenses.js | 17 +-- .../http/controllers/FinancialStatements.js | 46 +++---- server/src/http/controllers/Items.js | 56 ++++----- server/src/http/controllers/Options.js | 6 +- server/src/http/controllers/Resources.js | 4 +- server/src/http/controllers/Views.js | 17 ++- server/src/http/index.js | 36 +++--- .../src/http/middleware/TenancyMiddleware.js | 52 ++++++++ server/src/http/middleware/jwtAuth.js | 4 +- server/src/models/Account.js | 13 +- server/src/models/AccountBalance.js | 8 +- server/src/models/AccountTransaction.js | 6 +- server/src/models/AccountType.js | 6 +- server/src/models/Budget.js | 4 +- server/src/models/BudgetEntry.js | 4 +- server/src/models/Currency.js | 4 +- server/src/models/ExchangeRate.js | 52 +++++++- server/src/models/Expense.js | 17 +-- server/src/models/Item.js | 25 +--- server/src/models/ItemCategory.js | 6 +- server/src/models/ItemMetadata.js | 9 +- server/src/models/JournalEntry.js | 4 +- server/src/models/ManualJournal.js | 4 +- server/src/models/Metable.js | 4 +- server/src/models/Model.js | 18 ++- server/src/models/OAuthClient.js | 16 --- server/src/models/OAuthServerModel.js | 81 ------------ server/src/models/OAuthToken.js | 16 --- server/src/models/PasswordReset.js | 4 +- server/src/models/Permission.js | 22 ++-- server/src/models/Resource.js | 11 +- server/src/models/ResourceField.js | 10 +- server/src/models/ResourceFieldMetadata.js | 4 +- server/src/models/Role.js | 16 +-- server/src/models/Setting.js | 4 +- server/src/models/TenantModel.js | 4 + server/src/models/{User.js => TenantUser.js} | 6 +- server/src/models/View.js | 11 +- server/src/models/ViewColumn.js | 8 +- server/src/models/ViewRole.js | 8 +- server/src/services/Logger/index.js | 13 ++ server/src/system/TenantsManager.js | 58 +++++++++ .../20190822214242_create_users_table.js | 25 ++++ .../20200420134631_create_tenants_table.js | 12 ++ server/src/system/models/SystemModel.js | 4 + server/src/system/models/SystemOption.js | 29 +++++ server/src/system/models/SystemUser.js | 39 ++++++ server/src/system/models/Tenant.js | 10 ++ 64 files changed, 812 insertions(+), 447 deletions(-) create mode 100644 server/config/config.js create mode 100644 server/config/systemKnexfile.js delete mode 100644 server/knexfile.js rename server/src/{models/index.js => database/objection.js} (100%) create mode 100644 server/src/http/middleware/TenancyMiddleware.js delete mode 100644 server/src/models/OAuthClient.js delete mode 100644 server/src/models/OAuthServerModel.js delete mode 100644 server/src/models/OAuthToken.js create mode 100644 server/src/models/TenantModel.js rename server/src/models/{User.js => TenantUser.js} (90%) create mode 100644 server/src/services/Logger/index.js create mode 100644 server/src/system/TenantsManager.js create mode 100644 server/src/system/migrations/20190822214242_create_users_table.js create mode 100644 server/src/system/migrations/20200420134631_create_tenants_table.js create mode 100644 server/src/system/models/SystemModel.js create mode 100644 server/src/system/models/SystemOption.js create mode 100644 server/src/system/models/SystemUser.js create mode 100644 server/src/system/models/Tenant.js diff --git a/server/config/config.js b/server/config/config.js new file mode 100644 index 000000000..dc91a1953 --- /dev/null +++ b/server/config/config.js @@ -0,0 +1,25 @@ + +module.exports = { + system: { + db_client: 'mysql', + db_host: '127.0.0.1', + db_user: 'root', + db_password: '123123123', + db_name: 'bigcapital_system', + migrations_dir: '../src/system/migrations', + }, + tenant: { + db_client: 'mysql', + db_name_prefix: 'bigcapital_tenant_', + db_host: '127.0.0.1', + db_user: 'root', + db_password: '123123123', + charset: 'utf8', + migrations_dir: 'src/database/migrations', + seeds_dir: 'src/database/seeds', + }, + manager: { + superUser: 'root', + superPassword: '123123123', + }, +}; diff --git a/server/config/systemKnexfile.js b/server/config/systemKnexfile.js new file mode 100644 index 000000000..373cce76d --- /dev/null +++ b/server/config/systemKnexfile.js @@ -0,0 +1,20 @@ +const config = require('./config'); + +const configEnv = { + client: config.system.db_client, + connection: { + host: config.system.db_host, + user: config.system.db_user, + password: config.system.db_password, + database: config.system.db_name, + charset: 'utf8', + }, + migrations: { + directory: config.system.migrations_dir, + }, +}; + +module.exports = { + development: configEnv, + production: configEnv, +}; diff --git a/server/knexfile.js b/server/knexfile.js deleted file mode 100644 index acbaeebca..000000000 --- a/server/knexfile.js +++ /dev/null @@ -1,52 +0,0 @@ -require('dotenv').config(); - -const MIGRATIONS_DIR = './src/database/migrations'; -const SEEDS_DIR = './src/database/seeds'; - -module.exports = { - test: { - client: process.env.DB_CLIENT, - migrations: { - directory: MIGRATIONS_DIR, - }, - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - charset: 'utf8', - }, - }, - development: { - client: process.env.DB_CLIENT, - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - charset: 'utf8', - }, - migrations: { - directory: MIGRATIONS_DIR, - }, - seeds: { - directory: SEEDS_DIR, - }, - }, - production: { - client: process.env.DB_CLIENT, - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - charset: 'utf8', - }, - migrations: { - directory: MIGRATIONS_DIR, - }, - seeds: { - directory: SEEDS_DIR, - }, - }, -}; diff --git a/server/package.json b/server/package.json index 6f919349c..cca066143 100644 --- a/server/package.json +++ b/server/package.json @@ -32,6 +32,7 @@ "i18n": "^0.8.5", "jsonwebtoken": "^8.5.1", "knex": "^0.20.3", + "knex-cleaner": "^1.3.0", "knex-db-manager": "^0.6.1", "lodash": "^4.17.15", "memory-cache": "^0.2.0", @@ -43,7 +44,9 @@ "node-cache": "^4.2.1", "nodemailer": "^6.3.0", "nodemon": "^1.19.1", - "objection": "^2.0.10" + "objection": "^2.0.10", + "uniqid": "^5.2.0", + "winston": "^3.2.1" }, "devDependencies": { "@babel/core": "^7.5.5", @@ -56,6 +59,7 @@ "chai": "^4.2.0", "chai-http": "^4.3.0", "chai-things": "^0.2.0", + "commander": "^5.0.0", "cross-env": "^5.2.0", "eslint": "^6.2.1", "eslint-config-airbnb-base": "^14.0.0", diff --git a/server/src/app.js b/server/src/app.js index 55403acfc..27244fb56 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -3,8 +3,9 @@ import helmet from 'helmet'; import boom from 'express-boom'; import i18n from 'i18n'; import '../config'; +import '@/database/objection'; import routes from '@/http'; -import '@/models'; + const app = express(); diff --git a/server/src/database/knex.js b/server/src/database/knex.js index f1111d936..7ef83dbab 100644 --- a/server/src/database/knex.js +++ b/server/src/database/knex.js @@ -1,6 +1,6 @@ import Knex from 'knex'; import { knexSnakeCaseMappers } from 'objection'; -import knexfile from '@/../knexfile'; +import knexfile from '@/../config/systemKnexfile'; const config = knexfile[process.env.NODE_ENV]; const knex = Knex({ diff --git a/server/src/database/manager.js b/server/src/database/manager.js index b42a3a3f0..5b173aaf4 100644 --- a/server/src/database/manager.js +++ b/server/src/database/manager.js @@ -1,16 +1,19 @@ import knexManager from 'knex-db-manager'; -import knexfile from '@/../knexfile'; +import knexfile from '@/../config/systemKnexfile'; +import config from '@/../config/config'; -const config = knexfile[process.env.NODE_ENV]; +const knexConfig = knexfile[process.env.NODE_ENV]; +console.log({ + superUser: config.manager.superUser, + superPassword: config.manager.superPassword, +}); const dbManager = knexManager.databaseManagerFactory({ - knex: config, + knex: knexConfig, dbManager: { - // db manager related configuration collate: [], - superUser: 'root', - superPassword: 'root', - // populatePathPattern: 'data/**/*.js', // glob format for searching seeds + superUser: config.manager.superUser, + superPassword: config.manager.superPassword, }, }); diff --git a/server/src/database/migrations/20190822214242_create_users_table.js b/server/src/database/migrations/20190822214242_create_users_table.js index 09f200924..dd4260bcc 100644 --- a/server/src/database/migrations/20190822214242_create_users_table.js +++ b/server/src/database/migrations/20190822214242_create_users_table.js @@ -12,10 +12,6 @@ exports.up = function (knex) { table.string('language'); table.date('last_login_at'); table.timestamps(); - }).then(() => { - knex.seed.run({ - specific: 'seed_users.js', - }) }); }; diff --git a/server/src/models/index.js b/server/src/database/objection.js similarity index 100% rename from server/src/models/index.js rename to server/src/database/objection.js diff --git a/server/src/http/controllers/AccountOpeningBalance.js b/server/src/http/controllers/AccountOpeningBalance.js index ec3fb3c7f..8f19c32a3 100644 --- a/server/src/http/controllers/AccountOpeningBalance.js +++ b/server/src/http/controllers/AccountOpeningBalance.js @@ -2,12 +2,10 @@ import express from 'express'; import { check, validationResult, oneOf } from 'express-validator'; import { difference } from 'lodash'; import moment from 'moment'; -import asyncMiddleware from '../middleware/asyncMiddleware'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import jwtAuth from '@/http/middleware/jwtAuth'; -import Account from '@/models/Account'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; -import ManualJournal from '@/models/ManualJournal'; export default { /** @@ -57,11 +55,13 @@ export default { const date = moment(form.date).format('YYYY-MM-DD'); const accountsIds = accounts.map((account) => account.id); + + const { Account, ManualJournal } = req.models; const storedAccounts = await Account.query() .select(['id']).whereIn('id', accountsIds) .withGraphFetched('type'); - const accountsCollection = new Map(storedAccounts.map(i => [i.id, i])); + const accountsCollection = new Map(storedAccounts.map((i) => [i.id, i])); // Get the stored accounts Ids and difference with submit accounts. const accountsStoredIds = storedAccounts.map((account) => account.id); diff --git a/server/src/http/controllers/AccountTypes.js b/server/src/http/controllers/AccountTypes.js index 9833a8d48..58e978131 100644 --- a/server/src/http/controllers/AccountTypes.js +++ b/server/src/http/controllers/AccountTypes.js @@ -1,7 +1,6 @@ import express from 'express'; import JWTAuth from '@/http/middleware/jwtAuth'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import AccountType from '@/models/AccountType'; export default { /** @@ -24,6 +23,7 @@ export default { getAccountTypesList: { validation: [], async handler(req, res) { + const { AccountType } = req.models; const accountTypes = await AccountType.query(); return res.status(200).send({ diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js index 7f7706fdd..94e097ede 100644 --- a/server/src/http/controllers/Accounting.js +++ b/server/src/http/controllers/Accounting.js @@ -1,16 +1,11 @@ -import { check, query, oneOf, validationResult, param } from 'express-validator'; +import { check, query, validationResult, param } from 'express-validator'; import express from 'express'; import { difference } from 'lodash'; import moment from 'moment'; -import Account from '@/models/Account'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import JWTAuth from '@/http/middleware/jwtAuth'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; -import ManualJournal from '@/models/JournalEntry'; -import AccountTransaction from '@/models/AccountTransaction'; -import Resource from '@/models/Resource'; -import View from '@/models/View'; import { mapViewRolesToConditionals, mapFilterRolesToDynamicFilter, @@ -55,6 +50,10 @@ export default { this.deleteManualJournal.validation, asyncMiddleware(this.deleteManualJournal.handler)); + router.delete('/manual-journals', + this.deleteBulkManualJournals.validation, + asyncMiddleware(this.deleteBulkManualJournals.handler)); + router.post('/recurring-journal-entries', this.recurringJournalEntries.validation, asyncMiddleware(this.recurringJournalEntries.handler)); @@ -91,6 +90,7 @@ export default { if (filter.stringified_filter_roles) { filter.filter_roles = JSON.parse(filter.stringified_filter_roles); } + const { Resource, View, ManualJournal } = req.models; const errorReasons = []; const manualJournalsResource = await Resource.query() @@ -200,6 +200,8 @@ export default { reference: '', ...req.body, }; + const { ManualJournal, Account } = req.models; + let totalCredit = 0; let totalDebit = 0; @@ -341,6 +343,10 @@ export default { ...req.body, }; const { id } = req.params; + const { + ManualJournal, AccountTransaction, Account, + } = req.models; + const manualJournal = await ManualJournal.query().where('id', id).first(); if (!manualJournal) { @@ -456,6 +462,11 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { + ManualJournal, + AccountTransaction, + } = req.models; + const { id } = req.params; const manualJournal = await ManualJournal.query() .where('id', id).first(); @@ -508,6 +519,10 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { + ManualJournal, AccountTransaction, + } = req.models; + const { id } = req.params; const manualJournal = await ManualJournal.query() .where('id', id).first(); @@ -549,6 +564,9 @@ export default { }); } const { id } = req.params; + const { + ManualJournal, AccountTransaction, + } = req.models; const manualJournal = await ManualJournal.query() .where('id', id).first(); @@ -614,6 +632,7 @@ export default { } const errorReasons = []; const form = { ...req.body }; + const { Account } = req.models; const foundAccounts = await Account.query() .where('id', form.credit_account_id) @@ -642,4 +661,54 @@ export default { return res.status(200).send(); }, }, + + + /** + * Deletes bulk manual journals. + */ + deleteBulkManualJournals: { + validation: [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const filter = { ...req.query }; + const { ManualJournal, AccountTransaction } = req.models; + + const manualJournals = await ManualJournal.query() + .whereIn('id', filter.ids); + + const notFoundManualJournals = difference(filter.ids, manualJournals.map(m => m.id)); + + if (notFoundManualJournals.length > 0) { + return res.status(404).send({ + errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 200 }], + }); + } + const transactions = await AccountTransaction.query() + .whereIn('reference_type', ['Journal', 'ManualJournal']) + .whereIn('reference_id', filter.ids); + + const journal = new JournalPoster(); + + journal.loadEntries(transactions); + journal.removeEntries(); + + await ManualJournal.query() + .whereIn('id', filter.ids).delete(); + + await Promise.all([ + journal.deleteEntries(), + journal.saveBalance(), + ]); + return res.status(200).send({ ids: filter.ids }); + }, + } }; diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js index 289fc1842..b777483d5 100644 --- a/server/src/http/controllers/Accounts.js +++ b/server/src/http/controllers/Accounts.js @@ -7,15 +7,10 @@ import { } from 'express-validator'; import { difference } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Account from '@/models/Account'; -import AccountType from '@/models/AccountType'; -import AccountTransaction from '@/models/AccountTransaction'; import JournalPoster from '@/services/Accounting/JournalPoster'; -import AccountBalance from '@/models/AccountBalance'; import NestedSet from '@/collection/NestedSet'; -import Resource from '@/models/Resource'; -import View from '@/models/View'; import JWTAuth from '@/http/middleware/jwtAuth'; +import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; import { mapViewRolesToConditionals, mapFilterRolesToDynamicFilter, @@ -36,6 +31,8 @@ export default { const router = express.Router(); router.use(JWTAuth); + router.use(TenancyMiddleware); + router.post('/', this.newAccount.validation, asyncMiddleware(this.newAccount.handler)); @@ -84,8 +81,12 @@ export default { */ newAccount: { validation: [ - check('name').exists().isLength({ min: 3 }).trim().escape(), - check('code').optional().isLength({ max: 10 }).trim().escape(), + check('name').exists().isLength({ min: 3 }) + .trim() + .escape(), + check('code').optional().isLength({ max: 10 }) + .trim() + .escape(), check('account_type_id').exists().isNumeric().toInt(), check('description').optional().trim().escape(), ], @@ -98,6 +99,7 @@ export default { }); } const form = { ...req.body }; + const { AccountType, Account } = req.models; const foundAccountCodePromise = form.code ? Account.query().where('code', form.code) : null; @@ -131,8 +133,12 @@ export default { editAccount: { validation: [ param('id').exists().toInt(), - check('name').exists().isLength({ min: 3 }).trim().escape(), - check('code').exists().isLength({ max: 10 }).trim().escape(), + check('name').exists().isLength({ min: 3 }) + .trim() + .escape(), + check('code').exists().isLength({ max: 10 }) + .trim() + .escape(), check('account_type_id').exists().isNumeric().toInt(), check('description').optional().trim().escape(), ], @@ -145,6 +151,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { Account, AccountType } = req.models; const form = { ...req.body }; const account = await Account.query().findById(id); @@ -185,6 +192,7 @@ export default { ], async handler(req, res) { const { id } = req.params; + const { Account } = req.models; const account = await Account.query().where('id', id).first(); if (!account) { @@ -203,6 +211,7 @@ export default { ], async handler(req, res) { const { id } = req.params; + const { Account, AccountTransaction } = req.models; const account = await Account.query().findById(id); if (!account) { @@ -255,6 +264,8 @@ export default { if (filter.stringified_filter_roles) { filter.filter_roles = JSON.parse(filter.stringified_filter_roles); } + + const { Resource, Account, View } = req.models; const errorReasons = []; const accountsResource = await Resource.query() @@ -294,7 +305,7 @@ export default { } // View roles. - if (view && view.roles.length > 0) { + if (view && view.roles.length > 0) { const viewFilter = new DynamicFilterViews( mapViewRolesToConditionals(view.roles), view.rolesLogicExpression, @@ -349,6 +360,11 @@ export default { ], async handler(req, res) { const { id } = req.params; + const { + Account, + AccountTransaction, + AccountBalance, + } = req.models; const account = await Account.findById(id); if (!account) { @@ -381,6 +397,7 @@ export default { ], async handler(req, res) { const { id } = req.params; + const { Account } = req.models; const account = await Account.findById(id); if (!account) { @@ -403,6 +420,7 @@ export default { ], async handler(req, res) { const { id } = req.params; + const { Account } = req.models; const account = await Account.findById(id); if (!account) { @@ -461,12 +479,14 @@ export default { }); } const filter = { ids: [], ...req.query }; + const { Account, AccountTransaction } = req.models; + const accounts = await Account.query().onBuild((builder) => { if (filter.ids.length) { builder.whereIn('id', filter.ids); } }); - const accountsIds = accounts.map(a => a.id); + const accountsIds = accounts.map((a) => a.id); const notFoundAccounts = difference(filter.ids, accountsIds); if (notFoundAccounts.length > 0) { diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js index db6fdf616..926ea2ea8 100644 --- a/server/src/http/controllers/Authentication.js +++ b/server/src/http/controllers/Authentication.js @@ -5,11 +5,18 @@ import path from 'path'; import fs from 'fs'; import Mustache from 'mustache'; import jwt from 'jsonwebtoken'; -import asyncMiddleware from '../middleware/asyncMiddleware'; -import User from '@/models/User'; -import PasswordReset from '@/models/PasswordReset'; +import { pick } from 'lodash'; +import uniqid from 'uniqid'; +import Logger from '@/services/Logger'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import SystemUser from '@/system/models/SystemUser'; import mail from '@/services/mail'; import { hashPassword } from '@/utils'; +import dbManager from '@/database/manager'; +import Tenant from '@/system/models/Tenant'; +import TenantUser from '@/models/TenantUser'; +import TenantsManager from '@/system/TenantsManager'; +import TenantModel from '@/models/TenantModel'; export default { /** @@ -22,6 +29,10 @@ export default { this.login.validation, asyncMiddleware(this.login.handler)); + router.post('/register', + this.register.validation, + asyncMiddleware(this.register.handler)); + router.post('/send_reset_password', this.sendResetPassword.validation, asyncMiddleware(this.sendResetPassword.handler)); @@ -49,12 +60,15 @@ export default { code: 'validation_error', ...validationErrors, }); } - const { crediential, password } = req.body; + const form = { ...req.body }; const { JWT_SECRET_KEY } = process.env; - const user = await User.query() - .where('email', crediential) - .orWhere('phone_number', crediential) + Logger.log('info', 'Someone trying to login.', { form }); + + const user = await SystemUser.query() + .withGraphFetched('tenant') + .where('email', form.crediential) + .orWhere('phone_number', form.crediential) .first(); if (!user) { @@ -62,7 +76,7 @@ export default { errors: [{ type: 'INVALID_DETAILS', code: 100 }], }); } - if (!user.verifyPassword(password)) { + if (!user.verifyPassword(form.password)) { return res.boom.badRequest(null, { errors: [{ type: 'INVALID_DETAILS', code: 100 }], }); @@ -74,16 +88,89 @@ export default { } // user.update({ last_login_at: new Date() }); - const token = jwt.sign({ - email: user.email, - _id: user.id, - }, JWT_SECRET_KEY, { - expiresIn: '1d', - }); + const token = jwt.sign( + { email: user.email, _id: user.id }, + JWT_SECRET_KEY, + { expiresIn: '1d' }, + ); + Logger.log('info', 'Logging success.', { form }); + return res.status(200).send({ token, user }); }, }, + /** + * Registers a new organization. + */ + register: { + validation: [ + check('organization_name').exists().trim().escape(), + check('first_name').exists().trim().escape(), + check('last_name').exists().trim().escape(), + check('email').exists().trim().escape(), + check('phone_number').exists().trim().escape(), + check('password').exists().trim().escape(), + check('country').exists().trim().escape(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const form = { ...req.body }; + Logger.log('info', 'Someone trying to register.', { form }); + + const user = await SystemUser.query() + .where('email', form.email) + .orWhere('phone_number', form.phone_number) + .first(); + + if (user && user.phoneNumber === form.phone_number) { + return res.boom.badRequest(null, { + errors: [{ type: 'PHONE_NUMBER_EXISTS', code: 100 }], + }); + } + if (user && user.email === form.email) { + return res.boom.badRequest(null, { + errors: [{ type: 'EMAIL_EXISTS', code: 200 }], + }); + } + const organizationId = uniqid(); + const tenantOrganization = await Tenant.query().insert({ + organization_id: organizationId, + }); + + const hashedPassword = await hashPassword(form.password); + const userInsert = { + ...pick(form, ['first_name', 'last_name', 'email', 'phone_number']), + password: hashedPassword, + active: true, + }; + const registeredUser = await SystemUser.query().insert({ + ...userInsert, + tenant_id: tenantOrganization.id, + }); + await dbManager.createDb(`bigcapital_tenant_${organizationId}`); + + const tenantDb = TenantsManager.knexInstance(organizationId); + await tenantDb.migrate.latest(); + + TenantModel.knexBinded = tenantDb; + + await TenantUser.bindKnex(tenantDb).query().insert({ + ...userInsert, + }); + Logger.log('info', 'New tenant has been created.', { organizationId }); + + return res.status(200).send({ + organization_id: organizationId, + }); + }, + }, + /** * Send reset password link via email or SMS. */ diff --git a/server/src/http/controllers/Currencies.js b/server/src/http/controllers/Currencies.js index 190919304..3e2ff7680 100644 --- a/server/src/http/controllers/Currencies.js +++ b/server/src/http/controllers/Currencies.js @@ -1,7 +1,6 @@ import express from 'express'; import { check, param, validationResult } from 'express-validator'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Currency from '@/models/Currency'; import jwtAuth from '@/http/middleware/jwtAuth'; export default { @@ -17,7 +16,7 @@ export default { router.post('/', this.newCurrency.validation, asyncMiddleware(this.newCurrency.handler)); - + router.post('/:id', this.editCurrency.validation, asyncMiddleware(this.editCurrency.handler)); @@ -35,6 +34,7 @@ export default { all: { validation: [], async handler(req, res) { + const { Currency } = req.models; const currencies = await Currency.query(); return res.status(200).send({ @@ -59,6 +59,7 @@ export default { }); } const form = { ...req.body }; + const { Currency } = req.models; const foundCurrency = await Currency.query() .where('currency_code', form.currency_code); @@ -89,6 +90,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { Currency } = req.models; const { currency_code: currencyCode } = req.params; await Currency.query() @@ -115,23 +117,19 @@ export default { } const form = { ...req.body }; const { id } = req.params; + const { Currency } = req.models; const foundCurrency = await Currency.query() - .where('currency_code', form.currency_code) - .whereNot('id', id); + .where('currency_code', form.currency_code).whereNot('id', id); if (foundCurrency.length > 0) { return res.status(400).send({ errors: [{ type: 'CURRENCY.CODE.ALREADY.EXISTS', code: 100 }], }); } - await Currency.query() - .where('id', id) - .update({ ...form }); + await Currency.query().where('id', id).update({ ...form }); - return res.status(200).send({ - currency: { ...form }, - }); + return res.status(200).send({ currency: { ...form } }); }, }, -}; \ No newline at end of file +}; diff --git a/server/src/http/controllers/ExchangeRates.js b/server/src/http/controllers/ExchangeRates.js index 5fddd2163..0d82d68b4 100644 --- a/server/src/http/controllers/ExchangeRates.js +++ b/server/src/http/controllers/ExchangeRates.js @@ -1,9 +1,13 @@ import express from 'express'; -import { check, param, query, validationResult } from 'express-validator'; +import { + check, + param, + query, + validationResult, +} from 'express-validator'; import moment from 'moment'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import jwtAuth from '@/http/middleware/jwtAuth'; -import ExchangeRate from '@/models/ExchangeRate'; export default { /** @@ -53,11 +57,12 @@ export default { page_size: 10, ...req.query, }; + const { ExchangeRate } = req.models; const exchangeRates = await ExchangeRate.query() .pagination(filter.page - 1, filter.page_size); return res.status(200).send({ exchange_rates: exchangeRates }); - } + }, }, /** @@ -77,7 +82,7 @@ export default { code: 'validation_error', ...validationErrors, }); } - + const { ExchangeRate } = req.models; const form = { ...req.body }; const foundExchangeRate = await ExchangeRate.query() .where('currency_code', form.currency_code) @@ -87,7 +92,7 @@ export default { return res.status(400).send({ errors: [{ type: 'EXCHANGE.RATE.DATE.PERIOD.DEFINED', code: 200 }], }); - } + } await ExchangeRate.query().insert({ ...form, date: moment(form.date).format('YYYY-MM-DD'), @@ -116,6 +121,7 @@ export default { } const { id } = req.params; const form = { ...req.body }; + const { ExchangeRate } = req.models; const foundExchangeRate = await ExchangeRate.query() .where('id', id); @@ -148,19 +154,18 @@ export default { code: 'validation_error', ...validationErrors, }); } - const { id } = req.params; - const foundExchangeRate = await ExchangeRate.query() - .where('id', id); + const { id } = req.params; + const { ExchangeRate } = req.models; + const foundExchangeRate = await ExchangeRate.query().where('id', id); if (!foundExchangeRate.length) { return res.status(404).send({ errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }], }); } - await ExchangeRate.query() - .where('id', id).delete(); + await ExchangeRate.query().where('id', id).delete(); return res.status(200).send({ id }); - } + }, }, } \ No newline at end of file diff --git a/server/src/http/controllers/Expenses.js b/server/src/http/controllers/Expenses.js index 9a55775b8..ae86c0475 100644 --- a/server/src/http/controllers/Expenses.js +++ b/server/src/http/controllers/Expenses.js @@ -8,14 +8,9 @@ import { import moment from 'moment'; import { difference, chain, omit } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Expense from '@/models/Expense'; -import Account from '@/models/Account'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; import JWTAuth from '@/http/middleware/jwtAuth'; -import AccountTransaction from '@/models/AccountTransaction'; -import View from '@/models/View'; -import Resource from '../../models/Resource'; import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository'; import { validateViewRoles, @@ -92,6 +87,7 @@ export default { custom_fields: [], ...req.body, }; + const { Account, Expense } = req.models; // Convert the date to the general format. form.date = moment(form.date).format('YYYY-MM-DD'); @@ -174,6 +170,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { Account, Expense } = req.models; const form = { ...req.body }; const errorReasons = []; @@ -268,6 +265,7 @@ export default { } const { id } = req.params; + const { Expense, AccountTransaction } = req.models; const errorReasons = []; const expense = await Expense.query().findById(id); @@ -277,7 +275,6 @@ export default { if (errorReasons.length > 0) { return res.status(400).send({ errors: errorReasons }); } - if (expense.published) { errorReasons.push({ type: 'EXPENSE.ALREADY.PUBLISHED', code: 200 }); } @@ -337,6 +334,7 @@ export default { page: 1, ...req.query, }; + const { Resource, View, Expense } = req.models; const errorReasons = []; const expenseResource = await Resource.query().where('name', 'expenses').first(); @@ -364,7 +362,7 @@ export default { viewConditionals = mapViewRolesToConditionals(view.viewRoles); if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) { - errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }) + errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); } } if (!view && filter.custom_view_id) { @@ -391,7 +389,7 @@ export default { return res.status(200).send({ ...(view) ? { - customViewId: view.id, + customViewId: view.id, viewColumns: view.columns, viewConditionals, } : {}, @@ -416,6 +414,7 @@ export default { }); } const { id } = req.params; + const { Expense, AccountTransaction } = req.models; const expenseTransaction = await Expense.query().findById(id); if (!expenseTransaction) { @@ -463,6 +462,7 @@ export default { }); } const { id } = req.params; + const { Expense } = req.models; const expenseTransaction = await Expense.query().findById(id); if (!expenseTransaction) { @@ -489,6 +489,7 @@ export default { }); } const { id } = req.params; + const { Expense } = req.models; const expenseTransaction = await Expense.query().findById(id); if (!expenseTransaction) { diff --git a/server/src/http/controllers/FinancialStatements.js b/server/src/http/controllers/FinancialStatements.js index dd3c2e522..6975e8b77 100644 --- a/server/src/http/controllers/FinancialStatements.js +++ b/server/src/http/controllers/FinancialStatements.js @@ -3,10 +3,7 @@ import { query, oneOf, validationResult } from 'express-validator'; import moment from 'moment'; import { pick, difference, groupBy } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import AccountTransaction from '@/models/AccountTransaction'; import jwtAuth from '@/http/middleware/jwtAuth'; -import AccountType from '@/models/AccountType'; -import Account from '@/models/Account'; import JournalPoster from '@/services/Accounting/JournalPoster'; import { dateRangeCollection } from '@/utils'; @@ -89,6 +86,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { AccountTransaction } = req.models; const filter = { from_date: moment().startOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'), @@ -119,9 +117,9 @@ export default { const formatNumber = formatNumberClosure(filter.number_format); - const journalGrouped = groupBy(accountsJournalEntries, (entry) => { - return `${entry.referenceId}-${entry.referenceType}`; - }); + const journalGrouped = groupBy(accountsJournalEntries, + (entry) => `${entry.referenceId}-${entry.referenceType}`); + const journal = Object.keys(journalGrouped).map((key) => { const transactionsGroup = journalGrouped[key]; @@ -169,6 +167,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { AccountTransaction, Account } = req.models; const filter = { from_date: moment().startOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'), @@ -289,6 +288,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { Account, AccountType } = req.models; const filter = { display_columns_type: 'total', display_columns_by: '', @@ -327,22 +327,20 @@ export default { filter.from_date, filter.to_date, comparatorDateType, ) : []; - const totalPeriods = (account) => { + const totalPeriods = (account) => ({ // Gets the date range set from start to end date. - return { - total_periods: dateRangeSet.map((date) => { - const balance = journalEntries.getClosingBalance(account.id, date, comparatorDateType); - return { - date, - formatted_amount: balanceFormatter(balance), - amount: balance, - }; - }), - }; - }; + total_periods: dateRangeSet.map((date) => { + const balance = journalEntries.getClosingBalance(account.id, date, comparatorDateType); + return { + date, + formatted_amount: balanceFormatter(balance), + amount: balance, + }; + }), + }); - const accountsMapper = (balanceSheetAccounts) => { - return balanceSheetAccounts.map((account) => { + const accountsMapper = (balanceSheetAccounts) => [ + ...balanceSheetAccounts.map((account) => { // Calculates the closing balance to the given date. const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date); @@ -358,8 +356,8 @@ export default { date: filter.to_date, }, }; - }); - }; + }), + ]; // Retrieve all assets accounts. const assetsAccounts = accounts.filter((account) => ( account.type.normal === 'debit' @@ -414,6 +412,9 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { + Account, + } = req.models; const filter = { from_date: moment().startOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'), @@ -490,6 +491,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { Account, AccountType } = req.models; const filter = { from_date: moment().startOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'), diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js index 4e6c23c0e..f2a5bfdb4 100644 --- a/server/src/http/controllers/Items.js +++ b/server/src/http/controllers/Items.js @@ -3,13 +3,6 @@ import { check, query, validationResult } from 'express-validator'; import { difference } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import jwtAuth from '@/http/middleware/jwtAuth'; -import Item from '@/models/Item'; -import Account from '@/models/Account'; -import ItemCategory from '@/models/ItemCategory'; -import Resource from '@/models/Resource'; -import ResourceField from '@/models/ResourceField'; -import Authorization from '@/http/middleware/authorization'; -import View from '@/models/View'; import { mapViewRolesToConditionals, mapFilterRolesToDynamicFilter, @@ -23,10 +16,11 @@ import { export default { - + /** + * Router constructor. + */ router() { const router = express.Router(); - const permit = Authorization('items'); router.use(jwtAuth); @@ -35,7 +29,6 @@ export default { asyncMiddleware(this.editItem.handler)); router.post('/', - // permit('create'), this.newItem.validation, asyncMiddleware(this.newItem.handler)); @@ -43,10 +36,6 @@ export default { this.deleteItem.validation, asyncMiddleware(this.deleteItem.handler)); - // router.get('/:id', - // this.getCategory.validation, - // asyncMiddleware(this.getCategory.handler)); - router.get('/', this.listItems.validation, asyncMiddleware(this.listItems.handler)); @@ -92,12 +81,19 @@ export default { custom_fields: [], ...req.body, }; + const { + Account, + Resource, + ResourceField, + ItemCategory, + Item, + } = req.models; const errorReasons = []; const costAccountPromise = Account.query().findById(form.cost_account_id); const sellAccountPromise = Account.query().findById(form.sell_account_id); - const inventoryAccountPromise = (form.type === 'inventory') ? - Account.query().findByid(form.inventory_account_id) : null; + const inventoryAccountPromise = (form.type === 'inventory') + ? Account.query().findByid(form.inventory_account_id) : null; const itemCategoryPromise = (form.category_id) ? ItemCategory.query().findById(form.category_id) : null; @@ -108,9 +104,9 @@ export default { // Get resource id than get all resource fields. const resource = await Resource.where('name', 'items').fetch(); - const fields = await ResourceField.query((query) => { - query.where('resource_id', resource.id); - query.whereIn('key', customFieldsKeys); + const fields = await ResourceField.query((builder) => { + builder.where('resource_id', resource.id); + builder.whereIn('key', customFieldsKeys); }).fetchAll(); const storedFieldsKey = fields.map((f) => f.attributes.key); @@ -186,18 +182,19 @@ export default { code: 'validation_error', ...validationErrors, }); } - + const { Account, Item, ItemCategory } = req.models; const { id } = req.params; + const form = { custom_fields: [], ...req.body, }; const item = await Item.query().findById(id); - + if (!item) { - return res.boom.notFound(null, { errors: [ - { type: 'ITEM.NOT.FOUND', code: 100 }, - ]}); + return res.boom.notFound(null, { + errors: [{ type: 'ITEM.NOT.FOUND', code: 100 }], + }); } const errorReasons = []; @@ -244,6 +241,7 @@ export default { validation: [], async handler(req, res) { const { id } = req.params; + const { Item } = req.models; const item = await Item.query().findById(id); if (!item) { @@ -251,7 +249,6 @@ export default { errors: [{ type: 'ITEM_NOT_FOUND', code: 100 }], }); } - // Delete the fucking the given item id. await Item.query().findById(item.id).delete(); @@ -281,15 +278,16 @@ export default { } const errorReasons = []; const viewConditions = []; + const { Resource, Item, View } = req.models; const itemsResource = await Resource.query() .where('name', 'items') .withGraphFetched('fields') .first(); if (!itemsResource) { - return res.status(400).send({ errors: [ - {type: 'ITEMS_RESOURCE_NOT_FOUND', code: 200}, - ]}); + return res.status(400).send({ + errors: [{ type: 'ITEMS_RESOURCE_NOT_FOUND', code: 200 }], + }); } const filter = { column_sort_order: '', @@ -377,4 +375,4 @@ export default { }); }, }, -}; \ No newline at end of file +}; diff --git a/server/src/http/controllers/Options.js b/server/src/http/controllers/Options.js index 5792a83d5..37a49bce0 100644 --- a/server/src/http/controllers/Options.js +++ b/server/src/http/controllers/Options.js @@ -2,7 +2,6 @@ import express from 'express'; import { body, query, validationResult } from 'express-validator'; import { pick } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Option from '@/models/Option'; import jwtAuth from '@/http/middleware/jwtAuth'; export default { @@ -43,6 +42,7 @@ export default { code: 'VALIDATION_ERROR', ...validationErrors, }); } + const { Option } = req.models; const form = { ...req.body }; const optionsCollections = await Option.query(); @@ -53,7 +53,7 @@ export default { errorReasons.push({ type: 'OPTIONS.KEY.NOT.DEFINED', code: 200, - keys: notDefinedOptions.map(o => ({ ...pick(o, ['key', 'group']) })), + keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })), }); } if (errorReasons.length) { @@ -84,6 +84,7 @@ export default { code: 'VALIDATION_ERROR', ...validationErrors, }); } + const { Option } = req.models; const filter = { ...req.query }; const options = await Option.query().onBuild((builder) => { if (filter.key) { @@ -93,7 +94,6 @@ export default { builder.where('group', filter.group); } }); - return res.status(200).send({ options: options.metadata }); }, }, diff --git a/server/src/http/controllers/Resources.js b/server/src/http/controllers/Resources.js index 9b2c6a4b3..6555c10ed 100644 --- a/server/src/http/controllers/Resources.js +++ b/server/src/http/controllers/Resources.js @@ -2,11 +2,9 @@ import express from 'express'; import { param, query, - validationResult, } from 'express-validator'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import jwtAuth from '@/http/middleware/jwtAuth'; -import Resource from '@/models/Resource'; export default { /** @@ -37,6 +35,7 @@ export default { ], async handler(req, res) { const { resource_slug: resourceSlug } = req.params; + const { Resource } = req.models; const resource = await Resource.query() .where('name', resourceSlug) @@ -74,6 +73,7 @@ export default { ], async handler(req, res) { const { resource_slug: resourceSlug } = req.params; + const { Resource } = req.models; const resource = await Resource.query() .where('name', resourceSlug) diff --git a/server/src/http/controllers/Views.js b/server/src/http/controllers/Views.js index 4f0d1f5cb..72aade61b 100644 --- a/server/src/http/controllers/Views.js +++ b/server/src/http/controllers/Views.js @@ -1,4 +1,4 @@ -import { difference, intersection, pick } from 'lodash'; +import { difference, pick } from 'lodash'; import express from 'express'; import { check, @@ -9,10 +9,6 @@ import { } from 'express-validator'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import jwtAuth from '@/http/middleware/jwtAuth'; -import Resource from '@/models/Resource'; -import View from '@/models/View'; -import ViewRole from '@/models/ViewRole'; -import ViewColumn from '@/models/ViewColumn'; import { validateViewRoles, } from '@/lib/ViewRolesBuilder'; @@ -62,6 +58,7 @@ export default { ]), ], async handler(req, res) { + const { Resource, View } = req.models; const filter = { ...req.query }; const resource = await Resource.query().onBuild((builder) => { @@ -113,6 +110,7 @@ export default { param('view_id').exists().isNumeric().toInt(), ], async handler(req, res) { + const { View } = req.models; const { view_id: viewId } = req.params; const view = await View.query().findById(viewId); @@ -161,6 +159,12 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { + Resource, + View, + ViewColumn, + ViewRole, + } = req.models; const form = { roles: [], ...req.body }; const resource = await Resource.query().where('name', form.resource_name).first(); @@ -265,6 +269,9 @@ export default { code: 'validation_error', ...validationErrors, }); } + const { + View, ViewRole, ViewColumn, Resource, + } = req.models; const view = await View.query().where('id', viewId) .withGraphFetched('roles.field') .withGraphFetched('columns') diff --git a/server/src/http/index.js b/server/src/http/index.js index 6ac3d22c0..9ea7cbc38 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -1,25 +1,25 @@ // import OAuth2 from '@/http/controllers/OAuth2'; import Authentication from '@/http/controllers/Authentication'; -import Users from '@/http/controllers/Users'; -import Roles from '@/http/controllers/Roles'; +// import Users from '@/http/controllers/Users'; +// import Roles from '@/http/controllers/Roles'; import Items from '@/http/controllers/Items'; import ItemCategories from '@/http/controllers/ItemCategories'; import Accounts from '@/http/controllers/Accounts'; import AccountTypes from '@/http/controllers/AccountTypes'; -import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance'; +// import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance'; import Views from '@/http/controllers/Views'; -import CustomFields from '@/http/controllers/Fields'; +// import CustomFields from '@/http/controllers/Fields'; import Accounting from '@/http/controllers/Accounting'; import FinancialStatements from '@/http/controllers/FinancialStatements'; -import Expenses from '@/http/controllers/Expenses'; +// import Expenses from '@/http/controllers/Expenses'; import Options from '@/http/controllers/Options'; -import Budget from '@/http/controllers/Budget'; -import BudgetReports from '@/http/controllers/BudgetReports'; +// import Budget from '@/http/controllers/Budget'; +// import BudgetReports from '@/http/controllers/BudgetReports'; import Currencies from '@/http/controllers/Currencies'; -import Customers from '@/http/controllers/Customers'; -import Suppliers from '@/http/controllers/Suppliers'; -import Bills from '@/http/controllers/Bills'; -import CurrencyAdjustment from './controllers/CurrencyAdjustment'; +// import Customers from '@/http/controllers/Customers'; +// import Suppliers from '@/http/controllers/Suppliers'; +// import Bills from '@/http/controllers/Bills'; +// import CurrencyAdjustment from './controllers/CurrencyAdjustment'; import Resources from './controllers/Resources'; import ExchangeRates from '@/http/controllers/ExchangeRates'; // import SalesReports from '@/http/controllers/SalesReports'; @@ -29,24 +29,24 @@ export default (app) => { // app.use('/api/oauth2', OAuth2.router()); app.use('/api/auth', Authentication.router()); app.use('/api/currencies', Currencies.router()); - app.use('/api/users', Users.router()); - app.use('/api/roles', Roles.router()); + // app.use('/api/users', Users.router()); + // app.use('/api/roles', Roles.router()); app.use('/api/accounts', Accounts.router()); app.use('/api/account_types', AccountTypes.router()); app.use('/api/accounting', Accounting.router()); - app.use('/api/accounts_opening_balances', AccountOpeningBalance.router()); + // app.use('/api/accounts_opening_balances', AccountOpeningBalance.router()); app.use('/api/views', Views.router()); - app.use('/api/fields', CustomFields.router()); + // app.use('/api/fields', CustomFields.router()); app.use('/api/items', Items.router()); app.use('/api/item_categories', ItemCategories.router()); - app.use('/api/expenses', Expenses.router()); + // app.use('/api/expenses', Expenses.router()); app.use('/api/financial_statements', FinancialStatements.router()); app.use('/api/options', Options.router()); - app.use('/api/budget_reports', BudgetReports.router()); + // app.use('/api/budget_reports', BudgetReports.router()); // app.use('/api/customers', Customers.router()); // app.use('/api/suppliers', Suppliers.router()); // app.use('/api/bills', Bills.router()); - app.use('/api/budget', Budget.router()); + // app.use('/api/budget', Budget.router()); app.use('/api/resources', Resources.router()); app.use('/api/exchange_rates', ExchangeRates.router()); // app.use('/api/currency_adjustment', CurrencyAdjustment.router()); diff --git a/server/src/http/middleware/TenancyMiddleware.js b/server/src/http/middleware/TenancyMiddleware.js new file mode 100644 index 000000000..fef5fc49e --- /dev/null +++ b/server/src/http/middleware/TenancyMiddleware.js @@ -0,0 +1,52 @@ +import fs from 'fs'; +import path from 'path'; +import TenantsManager from '@/system/TenantsManager'; +import Model from '@/models/Model'; + +function loadModelsFromDirectory() { + const models = {}; + fs.readdirSync('src/models/').forEach((filename) => { + const model = { + path: path.join(__dirname, 'src/models/', filename), + name: filename.replace(/\.[^/.]+$/, ''), + }; + // eslint-disable-next-line global-require + model.resource = require(`@/models/${model.name}`); + models[model.name] = model; + }); + return models; +} + +export default async (req, res, next) => { + const { organization: organizationId } = req.query; + const notFoundOrganization = () => res.status(400).send({ + errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND' }], + }); + + if (!organizationId) { + return notFoundOrganization(); + } + const tenant = await TenantsManager.getTenant(organizationId); + + if (!tenant) { + return notFoundOrganization(); + } + const knex = TenantsManager.knexInstance(organizationId); + const models = loadModelsFromDirectory(); + + Model.knexBinded = knex; + + req.knex = knex; + req.organizationId = organizationId; + req.models = { + ...Object.values(models).reduce((acc, model) => { + if (model.resource + && model.resource.default + && Object.getPrototypeOf(model.resource.default) === Model) { + acc[model.name] = model.resource.default.bindKnex(knex); + } + return acc; + }, {}), + }; + next(); +}; \ No newline at end of file diff --git a/server/src/http/middleware/jwtAuth.js b/server/src/http/middleware/jwtAuth.js index 15f877186..4ab780419 100644 --- a/server/src/http/middleware/jwtAuth.js +++ b/server/src/http/middleware/jwtAuth.js @@ -1,6 +1,6 @@ /* eslint-disable consistent-return */ import jwt from 'jsonwebtoken'; -import User from '@/models/User'; +import SystemUser from '@/system/models/SystemUser'; // import Auth from '@/models/Auth'; const authMiddleware = (req, res, next) => { @@ -25,7 +25,7 @@ const authMiddleware = (req, res, next) => { reject(error); } else { // eslint-disable-next-line no-underscore-dangle - req.user = await User.query().findById(decoded._id); + req.user = await SystemUser.query().findById(decoded._id); // Auth.setAuthenticatedUser(req.user); if (!req.user) { diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 52f5c1681..b02c31520 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -1,12 +1,13 @@ /* eslint-disable global-require */ import { Model } from 'objection'; import { flatten } from 'lodash'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; import { buildFilterQuery, buildSortColumnQuery, } from '@/lib/ViewRolesBuilder'; -export default class Account extends BaseModel { + +export default class Account extends TenantModel { /** * Table name */ @@ -36,7 +37,7 @@ export default class Account extends BaseModel { }, sortColumnBuilder(query, columnKey, direction) { buildSortColumnQuery(Account.tableName, columnKey, direction)(query); - } + }, }; } @@ -54,7 +55,7 @@ export default class Account extends BaseModel { */ type: { relation: Model.BelongsToOneRelation, - modelClass: AccountType.default, + modelClass: this.relationBindKnex(AccountType.default), join: { from: 'accounts.accountTypeId', to: 'account_types.id', @@ -66,7 +67,7 @@ export default class Account extends BaseModel { */ balance: { relation: Model.HasOneRelation, - modelClass: AccountBalance.default, + modelClass: this.relationBindKnex(AccountBalance.default), join: { from: 'accounts.id', to: 'account_balances.accountId', @@ -78,7 +79,7 @@ export default class Account extends BaseModel { */ transactions: { relation: Model.HasManyRelation, - modelClass: AccountTransaction.default, + modelClass: this.relationBindKnex(AccountTransaction.default), join: { from: 'accounts.id', to: 'accounts_transactions.accountId', diff --git a/server/src/models/AccountBalance.js b/server/src/models/AccountBalance.js index 996daaae3..e1ca55ad6 100644 --- a/server/src/models/AccountBalance.js +++ b/server/src/models/AccountBalance.js @@ -1,7 +1,7 @@ import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class AccountBalance extends BaseModel { +export default class AccountBalance extends TenantModel { /** * Table name */ @@ -18,9 +18,9 @@ export default class AccountBalance extends BaseModel { return { account: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { - from: 'account_balance.account_id', + from: 'account_balances.account_id', to: 'accounts.id', }, }, diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index e46243db0..fa9b29689 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -1,8 +1,8 @@ import { Model } from 'objection'; import moment from 'moment'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class AccountTransaction extends BaseModel { +export default class AccountTransaction extends TenantModel { /** * Table name */ @@ -70,7 +70,7 @@ export default class AccountTransaction extends BaseModel { return { account: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'accounts_transactions.accountId', to: 'accounts.id', diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js index f95564afe..a4a0eca42 100644 --- a/server/src/models/AccountType.js +++ b/server/src/models/AccountType.js @@ -1,8 +1,8 @@ // import path from 'path'; import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class AccountType extends BaseModel { +export default class AccountType extends TenantModel { /** * Table name */ @@ -22,7 +22,7 @@ export default class AccountType extends BaseModel { */ accounts: { relation: Model.HasManyRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'account_types.id', to: 'accounts.accountTypeId', diff --git a/server/src/models/Budget.js b/server/src/models/Budget.js index 5a2b70f9f..a4486a3ff 100644 --- a/server/src/models/Budget.js +++ b/server/src/models/Budget.js @@ -1,6 +1,6 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/Model'; -export default class Budget extends BaseModel { +export default class Budget extends TenantModel { /** * Table name */ diff --git a/server/src/models/BudgetEntry.js b/server/src/models/BudgetEntry.js index dec3472d1..6774666c2 100644 --- a/server/src/models/BudgetEntry.js +++ b/server/src/models/BudgetEntry.js @@ -1,6 +1,6 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class Budget extends BaseModel { +export default class Budget extends TenantModel { /** * Table name */ diff --git a/server/src/models/Currency.js b/server/src/models/Currency.js index 7f8b37daa..33f534216 100644 --- a/server/src/models/Currency.js +++ b/server/src/models/Currency.js @@ -1,6 +1,6 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class Currency extends BaseModel { +export default class Currency extends TenantModel { /** * Table name */ diff --git a/server/src/models/ExchangeRate.js b/server/src/models/ExchangeRate.js index 57343da17..783d7a0f0 100644 --- a/server/src/models/ExchangeRate.js +++ b/server/src/models/ExchangeRate.js @@ -1,10 +1,54 @@ -import BaseModel from '@/models/Model'; +import bcrypt from 'bcryptjs'; +import { Model } from 'objection'; +import TenantModel from '@/models/TenantModel'; +// import PermissionsService from '@/services/PermissionsService'; + +export default class TenantUser extends TenantModel { + // ...PermissionsService + + static get virtualAttributes() { + return ['fullName']; + } -export default class ExchangeRate extends BaseModel { /** * Table name */ static get tableName() { - return 'exchange_rates'; + return 'users'; } -} + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Role = require('@/models/Role'); + + return { + roles: { + relation: Model.ManyToManyRelation, + modelClass: this.relationBindKnex(Role.default), + join: { + from: 'users.id', + through: { + from: 'user_has_roles.userId', + to: 'user_has_roles.roleId', + }, + to: 'roles.id', + }, + }, + }; + } + + /** + * Verify the password of the user. + * @param {String} password - The given password. + * @return {Boolean} + */ + verifyPassword(password) { + return bcrypt.compareSync(password, this.password); + } + + fullName() { + return `${this.firstName} ${this.lastName}`; + } +} \ No newline at end of file diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index 7425fe990..41262b4b5 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -1,7 +1,8 @@ import { Model } from 'objection'; -import BaseModel from '@/models/Model'; -import {viewRolesBuilder} from '@/lib/ViewRolesBuilder'; -export default class Expense extends BaseModel { +import TenantModel from '@/models/TenantModel'; +import { viewRolesBuilder } from '@/lib/ViewRolesBuilder'; + +export default class Expense extends TenantModel { /** * Table name */ @@ -60,12 +61,12 @@ export default class Expense extends BaseModel { */ static get relationMappings() { const Account = require('@/models/Account'); - const User = require('@/models/User'); - + const User = require('@/models/TenantUser'); + return { paymentAccount: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'expenses.paymentAccountId', to: 'accounts.id', @@ -74,7 +75,7 @@ export default class Expense extends BaseModel { expenseAccount: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'expenses.expenseAccountId', to: 'accounts.id', @@ -83,7 +84,7 @@ export default class Expense extends BaseModel { user: { relation: Model.BelongsToOneRelation, - modelClass: User.default, + modelClass: this.relationBindKnex(User.default), join: { from: 'expenses.userId', to: 'users.id', diff --git a/server/src/models/Item.js b/server/src/models/Item.js index 6173560da..f047a67a1 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -1,11 +1,10 @@ import { Model } from 'objection'; -import path from 'path'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; import { buildFilterQuery, } from '@/lib/ViewRolesBuilder'; -export default class Item extends BaseModel { +export default class Item extends TenantModel { /** * Table name */ @@ -34,24 +33,12 @@ export default class Item extends BaseModel { const ItemCategory = require('@/models/ItemCategory'); return { - /** - * Item may has many meta data. - */ - metadata: { - relation: Model.HasManyRelation, - modelBase: path.join(__dirname, 'ItemMetadata'), - join: { - from: 'items.id', - to: 'items_metadata.item_id', - }, - }, - /** * Item may belongs to cateogory model. */ category: { relation: Model.BelongsToOneRelation, - modelClass: ItemCategory.default, + modelClass: this.relationBindKnex(ItemCategory.default), join: { from: 'items.categoryId', to: 'items_categories.id', @@ -60,7 +47,7 @@ export default class Item extends BaseModel { costAccount: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'items.costAccountId', to: 'accounts.id', @@ -69,7 +56,7 @@ export default class Item extends BaseModel { sellAccount: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'items.sellAccountId', to: 'accounts.id', @@ -78,7 +65,7 @@ export default class Item extends BaseModel { inventoryAccount: { relation: Model.BelongsToOneRelation, - modelClass: Account.default, + modelClass: this.relationBindKnex(Account.default), join: { from: 'items.inventoryAccountId', to: 'accounts.id', diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js index 89af2b72a..1ca3ed806 100644 --- a/server/src/models/ItemCategory.js +++ b/server/src/models/ItemCategory.js @@ -1,8 +1,8 @@ import path from 'path'; import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class ItemCategory extends BaseModel { +export default class ItemCategory extends TenantModel { /** * Table name. */ @@ -22,7 +22,7 @@ export default class ItemCategory extends BaseModel { */ items: { relation: Model.HasManyRelation, - modelClass: Item.default, + modelClass: this.relationBindKnex(Item.default), join: { from: 'items_categories.id', to: 'items.categoryId', diff --git a/server/src/models/ItemMetadata.js b/server/src/models/ItemMetadata.js index 43c2e4736..7b5e5cc72 100644 --- a/server/src/models/ItemMetadata.js +++ b/server/src/models/ItemMetadata.js @@ -1,8 +1,7 @@ -import path from 'path'; import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class ItemMetadata extends BaseModel { +export default class ItemMetadata extends TenantModel { /** * Table name */ @@ -21,13 +20,15 @@ export default class ItemMetadata extends BaseModel { * Relationship mapping. */ static get relationMappings() { + const Item = require('@/models/Item'); + return { /** * Item category may has many items. */ items: { relation: Model.BelongsToOneRelation, - modelBase: path.join(__dirname, 'Item'), + modelBase: this.relationBindKnex(Item.default), join: { from: 'items_metadata.item_id', to: 'items.id', diff --git a/server/src/models/JournalEntry.js b/server/src/models/JournalEntry.js index 479904900..d659471b9 100644 --- a/server/src/models/JournalEntry.js +++ b/server/src/models/JournalEntry.js @@ -1,6 +1,6 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class JournalEntry extends BaseModel { +export default class JournalEntry extends TenantModel { /** * Table name. */ diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 5361e18d2..9f8a288c0 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -1,6 +1,6 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/Model'; -export default class ManualJournal extends BaseModel { +export default class ManualJournal extends TenantModel { /** * Table name. */ diff --git a/server/src/models/Metable.js b/server/src/models/Metable.js index b16d61682..8522120fe 100644 --- a/server/src/models/Metable.js +++ b/server/src/models/Metable.js @@ -36,7 +36,7 @@ export default { setExtraColumns(columns) { this.extraColumns = columns; }, - + /** * Metadata database query. * @param {Object} query - @@ -117,7 +117,7 @@ export default { metadata.markAsDeleted = true; } this.shouldReload = true; - + }, /** * Remove all meta data of the given group. diff --git a/server/src/models/Model.js b/server/src/models/Model.js index f4d7980bc..482d97b73 100644 --- a/server/src/models/Model.js +++ b/server/src/models/Model.js @@ -1,9 +1,18 @@ import { Model } from 'objection'; -import {transform, snakeCase} from 'lodash'; -import {mapKeysDeep} from '@/utils'; +import { snakeCase } from 'lodash'; +import { mapKeysDeep } from '@/utils'; import PaginationQueryBuilder from '@/models/Pagination'; export default class ModelBase extends Model { + + static get knexBinded() { + return this.knexBindInstance; + } + + static set knexBinded(knex) { + this.knexBindInstance = knex; + } + static get collection() { return Array; } @@ -22,11 +31,14 @@ export default class ModelBase extends Model { return snakeCase(key); }); const parsedJson = super.$formatJson(transformed, opt); - return parsedJson; } static get QueryBuilder() { return PaginationQueryBuilder; } + + static relationBindKnex(model) { + return this.knexBinded ? model.bindKnex(this.knexBinded) : model; + } } diff --git a/server/src/models/OAuthClient.js b/server/src/models/OAuthClient.js deleted file mode 100644 index 7e59cb728..000000000 --- a/server/src/models/OAuthClient.js +++ /dev/null @@ -1,16 +0,0 @@ -import bookshelf from './bookshelf'; - -const OAuthClient = bookshelf.Model.extend({ - - /** - * Table name - */ - tableName: 'oauth_clients', - - /** - * Timestamp columns. - */ - hasTimestamps: false, -}); - -export default bookshelf.model('OAuthClient', OAuthClient); diff --git a/server/src/models/OAuthServerModel.js b/server/src/models/OAuthServerModel.js deleted file mode 100644 index 08862b6e0..000000000 --- a/server/src/models/OAuthServerModel.js +++ /dev/null @@ -1,81 +0,0 @@ -import OAuthClient from '@/models/OAuthClient'; -import OAuthToken from '@/models/OAuthToken'; -import User from '@/models/User'; - -export default { - /** - * Retrieve the access token. - * @param {String} bearerToken - - */ - async getAccessToken(bearerToken) { - const token = await OAuthClient.where({ - access_token: bearerToken, - }).fetch(); - - return { - accessToken: token.attributes.access_token, - client: { - id: token.attributes.client_id, - }, - expires: token.attributes.access_token_expires_on, - }; - }, - - /** - * Retrieve the client from client id and secret. - * @param {Number} clientId - - * @param {String} clientSecret - - */ - async getClient(clientId, clientSecret) { - const token = await OAuthClient.where({ - client_id: clientId, - client_secret: clientSecret, - }); - - if (!token) { return {}; } - - return { - clientId: token.attributes.client_id, - clientSecret: token.attributes.client_secret, - grants: ['password'], - }; - }, - - /** - * Get specific user with given username and password. - */ - async getUser(username, password) { - const user = await User.query((query) => { - query.where('username', username); - query.where('password', password); - }).fetch(); - - return { - ...user.attributes, - }; - }, - - /** - * Saves the access token. - * @param {Object} token - - * @param {Object} client - - * @param {Object} user - - */ - async saveAccessToken(token, client, user) { - const oauthToken = OAuthToken.forge({ - access_token: token.accessToken, - access_token_expires_on: token.accessTokenExpiresOn, - client_id: client.id, - refresh_token: token.refreshToken, - refresh_token_expires_on: token.refreshTokenExpiresOn, - user_id: user.id, - }); - - await oauthToken.save(); - - return { - client: { id: client.id }, - user: { id: user.id }, - }; - }, -}; diff --git a/server/src/models/OAuthToken.js b/server/src/models/OAuthToken.js deleted file mode 100644 index ecad417e9..000000000 --- a/server/src/models/OAuthToken.js +++ /dev/null @@ -1,16 +0,0 @@ -import bookshelf from './bookshelf'; - -const OAuthToken = bookshelf.Model.extend({ - - /** - * Table name - */ - tableName: 'oauth_tokens', - - /** - * Timestamp columns. - */ - hasTimestamps: false, -}); - -export default bookshelf.model('OAuthToken', OAuthToken); diff --git a/server/src/models/PasswordReset.js b/server/src/models/PasswordReset.js index ec9734080..58b45eeae 100644 --- a/server/src/models/PasswordReset.js +++ b/server/src/models/PasswordReset.js @@ -1,6 +1,6 @@ -import Model from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class PasswordResets extends Model { +export default class PasswordResets extends TenantModel { /** * Table name */ diff --git a/server/src/models/Permission.js b/server/src/models/Permission.js index c2c2e6968..c8d8dfd4c 100644 --- a/server/src/models/Permission.js +++ b/server/src/models/Permission.js @@ -1,8 +1,8 @@ import { Model } from 'objection'; import path from 'path'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class Permission extends BaseModel { +export default class Permission extends TenantModel { /** * Table name of Role model. * @type {String} @@ -15,18 +15,20 @@ export default class Permission extends BaseModel { * Relationship mapping. */ static get relationMappings() { + const Role = require('@/models/Role'); + return { /** * Permission model may belongs to role model. */ - role: { - relation: Model.BelongsToOneRelation, - modelBase: path.join(__dirname, 'Role'), - join: { - from: 'permissions.role_id', - to: 'roles.id', - }, - }, + // role: { + // relation: Model.BelongsToOneRelation, + // modelBase: path.join(__dirname, 'Role').bindKnex(this.knexBinded), + // join: { + // from: 'permissions.role_id', + // to: 'roles.id', + // }, + // }, // resource: { // relation: Model.BelongsToOneRelation, diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js index c0a06b198..68928a9cf 100644 --- a/server/src/models/Resource.js +++ b/server/src/models/Resource.js @@ -1,8 +1,7 @@ -import path from 'path'; import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class Resource extends BaseModel { +export default class Resource extends TenantModel { /** * Table name. */ @@ -31,7 +30,7 @@ export default class Resource extends BaseModel { */ views: { relation: Model.HasManyRelation, - modelClass: View.default, + modelClass: this.relationBindKnex(View.default), join: { from: 'resources.id', to: 'views.resourceId', @@ -43,7 +42,7 @@ export default class Resource extends BaseModel { */ fields: { relation: Model.HasManyRelation, - modelClass: ResourceField.default, + modelClass: this.relationBindKnex(ResourceField.default), join: { from: 'resources.id', to: 'resource_fields.resourceId', @@ -55,7 +54,7 @@ export default class Resource extends BaseModel { */ permissions: { relation: Model.ManyToManyRelation, - modelClass: Permission.default, + modelClass: this.relationBindKnex(Permission.default), join: { from: 'resources.id', through: { diff --git a/server/src/models/ResourceField.js b/server/src/models/ResourceField.js index 5d470b1f6..32083dfef 100644 --- a/server/src/models/ResourceField.js +++ b/server/src/models/ResourceField.js @@ -1,9 +1,9 @@ import { snakeCase } from 'lodash'; import { Model } from 'objection'; import path from 'path'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class ResourceField extends BaseModel { +export default class ResourceField extends TenantModel { /** * Table name. */ @@ -51,15 +51,17 @@ export default class ResourceField extends BaseModel { * Relationship mapping. */ static get relationMappings() { + const Resource = require('@/models/Resource'); + return { /** * Resource field may belongs to resource model. */ resource: { relation: Model.BelongsToOneRelation, - modelBase: path.join(__dirname, 'Resource'), + modelClass: this.relationBindKnex(Resource.default), join: { - from: 'resource_fields.resource_id', + from: 'resource_fields.resourceId', to: 'resources.id', }, }, diff --git a/server/src/models/ResourceFieldMetadata.js b/server/src/models/ResourceFieldMetadata.js index 30ea3cb1f..7957417d0 100644 --- a/server/src/models/ResourceFieldMetadata.js +++ b/server/src/models/ResourceFieldMetadata.js @@ -1,9 +1,9 @@ import { Model } from 'objection'; import path from 'path'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection'; -export default class ResourceFieldMetadata extends BaseModel { +export default class ResourceFieldMetadata extends TenantModel { /** * Table name. */ diff --git a/server/src/models/Role.js b/server/src/models/Role.js index 4fe6ee135..9f0f9f401 100644 --- a/server/src/models/Role.js +++ b/server/src/models/Role.js @@ -1,7 +1,7 @@ import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class Role extends BaseModel { +export default class Role extends TenantModel { /** * Table name of Role model. * @type {String} @@ -23,7 +23,7 @@ export default class Role extends BaseModel { static get relationMappings() { const Permission = require('@/models/Permission'); const Resource = require('@/models/Resource'); - const User = require('@/models/User'); + const User = require('@/models/TenantUser'); const ResourceField = require('@/models/ResourceField'); return { @@ -32,7 +32,7 @@ export default class Role extends BaseModel { */ permissions: { relation: Model.ManyToManyRelation, - modelClass: Permission.default, + modelClass: Permission.default.bindKnex(this.knexBinded), join: { from: 'roles.id', through: { @@ -48,7 +48,7 @@ export default class Role extends BaseModel { */ resources: { relation: Model.ManyToManyRelation, - modelClass: Resource.default, + modelClass: Resource.default.bindKnex(this.knexBinded), join: { from: 'roles.id', through: { @@ -64,11 +64,11 @@ export default class Role extends BaseModel { */ field: { relation: Model.BelongsToOneRelation, - modelClass: ResourceField.default, + modelClass: ResourceField.default.bindKnex(this.knexBinded), join: { from: 'roles.fieldId', to: 'resource_fields.id', - } + }, }, /** @@ -76,7 +76,7 @@ export default class Role extends BaseModel { */ users: { relation: Model.ManyToManyRelation, - modelClass: User.default, + modelClass: User.default.bindKnex(this.knexBinded), join: { from: 'roles.id', through: { diff --git a/server/src/models/Setting.js b/server/src/models/Setting.js index 3b542d088..92cf7d625 100644 --- a/server/src/models/Setting.js +++ b/server/src/models/Setting.js @@ -1,7 +1,7 @@ -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; import Auth from './Auth'; -export default class Setting extends BaseModel { +export default class Setting extends TenantModel { /** * Table name */ diff --git a/server/src/models/TenantModel.js b/server/src/models/TenantModel.js new file mode 100644 index 000000000..fb5a0af46 --- /dev/null +++ b/server/src/models/TenantModel.js @@ -0,0 +1,4 @@ +import BaseModel from '@/models/Model'; + +export default class TenantModel extends BaseModel{ +} \ No newline at end of file diff --git a/server/src/models/User.js b/server/src/models/TenantUser.js similarity index 90% rename from server/src/models/User.js rename to server/src/models/TenantUser.js index 1cf0fd7f0..886372aba 100644 --- a/server/src/models/User.js +++ b/server/src/models/TenantUser.js @@ -3,7 +3,7 @@ import { Model } from 'objection'; import BaseModel from '@/models/Model'; // import PermissionsService from '@/services/PermissionsService'; -export default class User extends BaseModel { +export default class TenantUser extends BaseModel { // ...PermissionsService static get virtualAttributes() { @@ -26,7 +26,7 @@ export default class User extends BaseModel { return { roles: { relation: Model.ManyToManyRelation, - modelClass: Role.default, + modelClass: this.relationBindKnex(Role.default), join: { from: 'users.id', through: { @@ -51,4 +51,4 @@ export default class User extends BaseModel { fullName() { return `${this.firstName} ${this.lastName}`; } -} +} \ No newline at end of file diff --git a/server/src/models/View.js b/server/src/models/View.js index 0f2d87c65..c0e006dcc 100644 --- a/server/src/models/View.js +++ b/server/src/models/View.js @@ -1,8 +1,7 @@ -import path from 'path'; import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class View extends BaseModel { +export default class View extends TenantModel { /** * Table name. */ @@ -24,7 +23,7 @@ export default class View extends BaseModel { */ resource: { relation: Model.BelongsToOneRelation, - modelClass: Resource.default, + modelClass: this.relationBindKnex(Resource.default), join: { from: 'views.resourceId', to: 'resources.id', @@ -36,7 +35,7 @@ export default class View extends BaseModel { */ columns: { relation: Model.HasManyRelation, - modelClass: ViewColumn.default, + modelClass: this.relationBindKnex(ViewColumn.default), join: { from: 'views.id', to: 'view_has_columns.viewId', @@ -48,7 +47,7 @@ export default class View extends BaseModel { */ roles: { relation: Model.HasManyRelation, - modelClass: ViewRole.default, + modelClass: this.relationBindKnex(ViewRole.default), join: { from: 'views.id', to: 'view_roles.viewId', diff --git a/server/src/models/ViewColumn.js b/server/src/models/ViewColumn.js index d5db7efd3..a96979fe1 100644 --- a/server/src/models/ViewColumn.js +++ b/server/src/models/ViewColumn.js @@ -1,7 +1,7 @@ import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class ViewColumn extends BaseModel { +export default class ViewColumn extends TenantModel { /** * Table name. */ @@ -29,9 +29,9 @@ export default class ViewColumn extends BaseModel { */ field: { relation: Model.BelongsToOneRelation, - modelClass: ResourceField.default, + modelClass: this.relationBindKnex(ResourceField.default), join: { - from: 'view_columns.fieldId', + from: 'view_has_columns.fieldId', to: 'resource_fields.id', }, }, diff --git a/server/src/models/ViewRole.js b/server/src/models/ViewRole.js index 1db794aec..0a89e7217 100644 --- a/server/src/models/ViewRole.js +++ b/server/src/models/ViewRole.js @@ -1,7 +1,7 @@ import { Model } from 'objection'; -import BaseModel from '@/models/Model'; +import TenantModel from '@/models/TenantModel'; -export default class ViewRole extends BaseModel { +export default class ViewRole extends TenantModel { /** * Virtual attributes. @@ -43,7 +43,7 @@ export default class ViewRole extends BaseModel { */ view: { relation: Model.BelongsToOneRelation, - modelClass: View.default, + modelClass: this.relationBindKnex(View.default), join: { from: 'view_roles.viewId', to: 'views.id', @@ -55,7 +55,7 @@ export default class ViewRole extends BaseModel { */ field: { relation: Model.BelongsToOneRelation, - modelClass: ResourceField.default, + modelClass: this.relationBindKnex(ResourceField.default), join: { from: 'view_roles.fieldId', to: 'resource_fields.id', diff --git a/server/src/services/Logger/index.js b/server/src/services/Logger/index.js new file mode 100644 index 000000000..cbdf1a3fa --- /dev/null +++ b/server/src/services/Logger/index.js @@ -0,0 +1,13 @@ +import winston from 'winston'; + +const transports = { + console: new winston.transports.Console({ level: 'warn' }), + file: new winston.transports.File({ filename: 'stdout.log' }), +}; + +export default winston.createLogger({ + transports: [ + transports.console, + transports.file, + ], +}); diff --git a/server/src/system/TenantsManager.js b/server/src/system/TenantsManager.js new file mode 100644 index 000000000..dd16f81dc --- /dev/null +++ b/server/src/system/TenantsManager.js @@ -0,0 +1,58 @@ +import Knex from 'knex'; +import Tenant from '@/system/models/Tenant'; +import config from '@/../config/config'; + +export default class TenantsManager { + + constructor() { + this.knexCache = new Map(); + } + + static async getTenant(organizationId) { + const tenant = await Tenant.query() + .where('organization_id', organizationId).first(); + + return tenant; + } + + /** + * Retrieve all tenants metadata from system storage. + */ + static getAllTenants() { + return Tenant.query(); + } + + /** + * Retrieve the given organization id knex configuration. + * @param {String} organizationId - + */ + static getTenantKnexConfig(organizationId) { + return { + client: config.tenant.db_client, + connection: { + host: config.tenant.db_host, + user: config.tenant.db_user, + password: config.tenant.db_password, + database: `${config.tenant.db_name_prefix}${organizationId}`, + charset: config.tenant.charset, + }, + migrations: { + directory: config.tenant.migrations_dir, + }, + seeds: { + directory: config.tenant.seeds_dir, + }, + }; + } + + static knexInstance(organizationId) { + const knexCache = new Map(); + let knex = knexCache.get(organizationId); + + if (!knex) { + knex = Knex(this.getTenantKnexConfig(organizationId)); + knexCache.set(organizationId, knex); + } + return knex; + } +} \ No newline at end of file diff --git a/server/src/system/migrations/20190822214242_create_users_table.js b/server/src/system/migrations/20190822214242_create_users_table.js new file mode 100644 index 000000000..6c040feb6 --- /dev/null +++ b/server/src/system/migrations/20190822214242_create_users_table.js @@ -0,0 +1,25 @@ + +exports.up = function (knex) { + return knex.schema.createTable('users', (table) => { + table.increments(); + table.string('first_name'); + table.string('last_name'); + table.string('email').unique(); + table.string('phone_number').unique(); + table.string('password'); + table.boolean('active'); + table.integer('role_id').unique(); + table.string('language'); + table.date('last_login_at'); + table.integer('tenant_id').unsigned(); + table.timestamps(); + }).then(() => { + // knex.seed.run({ + // specific: 'seed_users.js', + // }) + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTableIfExists('users'); +}; diff --git a/server/src/system/migrations/20200420134631_create_tenants_table.js b/server/src/system/migrations/20200420134631_create_tenants_table.js new file mode 100644 index 000000000..5121509f3 --- /dev/null +++ b/server/src/system/migrations/20200420134631_create_tenants_table.js @@ -0,0 +1,12 @@ + +exports.up = function(knex) { + return knex.schema.createTable('tenants', (table) => { + table.bigIncrements(); + table.string('organization_id'); + table.timestamps(); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('tenants'); +}; diff --git a/server/src/system/models/SystemModel.js b/server/src/system/models/SystemModel.js new file mode 100644 index 000000000..6dfc00449 --- /dev/null +++ b/server/src/system/models/SystemModel.js @@ -0,0 +1,4 @@ +import BaseModel from '@/models/Model'; + +export default class SystemModel extends BaseModel{ +} \ No newline at end of file diff --git a/server/src/system/models/SystemOption.js b/server/src/system/models/SystemOption.js new file mode 100644 index 000000000..9f28e192d --- /dev/null +++ b/server/src/system/models/SystemOption.js @@ -0,0 +1,29 @@ +import { mixin } from 'objection'; +import SystemModel from '@/system/models/SystemModel'; +import MetableCollection from '@/lib/Metable/MetableCollection'; + +export default class Option extends mixin(SystemModel, [mixin]) { + /** + * Table name. + */ + static get tableName() { + return 'options'; + } + + /** + * Override the model query. + * @param {...any} args - + */ + static query(...args) { + return super.query(...args).runAfter((result) => { + if (result instanceof MetableCollection) { + result.setModel(Option); + } + return result; + }); + } + + static get collection() { + return MetableCollection; + } +} diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js new file mode 100644 index 000000000..dd1c820cd --- /dev/null +++ b/server/src/system/models/SystemUser.js @@ -0,0 +1,39 @@ +import { Model } from 'objection'; +import bcrypt from 'bcryptjs'; +import SystemModel from '@/system/models/SystemModel'; + +export default class SystemUser extends SystemModel { + /** + * Table name. + */ + static get tableName() { + return 'users'; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Tenant = require('@/system/models/Tenant'); + + return { + tenant: { + relation: Model.BelongsToOneRelation, + modelClass: Tenant.default, + join: { + from: 'users.tenant_id', + to: 'tenants.id', + }, + }, + }; + } + + /** + * Verify the password of the user. + * @param {String} password - The given password. + * @return {Boolean} + */ + verifyPassword(password) { + return bcrypt.compareSync(password, this.password); + } +} diff --git a/server/src/system/models/Tenant.js b/server/src/system/models/Tenant.js new file mode 100644 index 000000000..1c8b3fe58 --- /dev/null +++ b/server/src/system/models/Tenant.js @@ -0,0 +1,10 @@ +import BaseModel from '@/models/Model'; + +export default class Tenant extends BaseModel { + /** + * Table name. + */ + static get tableName() { + return 'tenants'; + } +}