From 9ee7ed89ec573804c3cd907104e4a8d7c84fc638 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 3 Sep 2020 16:51:48 +0200 Subject: [PATCH] - feat: remove unnecessary migrations, controllers and models files. - feat: metable store - feat: metable store with settings store. - feat: settings middleware to auto-save and load. - feat: DI db manager to master container. - feat: write some logs to sale invoices. --- server/bin/bigcapital.js | 4 - server/bin/license.js | 55 --- server/config/config.js | 5 + server/src/database/manager.js | 6 +- ...20190423085238_create_permissions_table.js | 11 - .../20190423085247_create_roles_table.js | 12 - ...90822214244_create_user_has_roles_table.js | 12 - ...20190822214905_create_role_has_accounts.js | 10 - ...90822214905_create_role_has_permissions.js | 11 - ...20190822214905_create_views_roles_table.js | 17 - ...23827_create_currency_adjustments_table.js | 14 - ...108204331_make_recurring_journals_table.js | 12 - .../20200120145228_create_budgets_table.js | 14 - ...00120145342_create_budget_entries_table.js | 14 - server/src/decorators/eventDispatcher.ts | 5 - server/src/exceptions/HttpException.ts | 9 + .../http/controllers/AccountOpeningBalance.js | 147 -------- server/src/http/controllers/Authentication.ts | 25 +- server/src/http/controllers/Banking.js | 33 -- server/src/http/controllers/BaseController.ts | 11 + server/src/http/controllers/Expenses.js | 1 - server/src/http/controllers/InviteUsers.js | 251 ------------- server/src/http/controllers/InviteUsers.ts | 140 +++++++ server/src/http/controllers/ItemCategories.ts | 28 +- server/src/http/controllers/Media.js | 13 +- server/src/http/controllers/Options.js | 97 ----- server/src/http/controllers/Organization.ts | 65 ++++ server/src/http/controllers/Roles.js | 346 ------------------ server/src/http/controllers/Settings.ts | 89 +++++ .../controllers/Subscription/PaymentMethod.ts | 5 +- .../Subscription/PaymentViaVoucher.ts | 18 +- .../http/controllers/Subscription/Vouchers.ts | 25 +- .../http/controllers/Subscription/index.ts | 2 + server/src/http/index.js | 31 +- .../middleware/AttachCurrentTenantUser.ts | 29 ++ .../middleware/EnsureTenantIsInitialized.ts | 17 + .../src/http/middleware/LoggerMiddleware.ts | 8 + .../src/http/middleware/SettingsMiddleware.ts | 29 ++ .../src/http/middleware/TenancyMiddleware.js | 77 ++-- .../middleware/TenantDependencyInjection.ts | 13 - server/src/http/middleware/authorization.js | 16 - server/src/http/middleware/jwtAuth.js | 28 +- .../src/http/middleware/prettierMiddleware.ts | 34 -- server/src/interfaces/Metable.ts | 28 ++ server/src/interfaces/Options.ts | 11 + server/src/interfaces/User.ts | 4 + server/src/interfaces/index.ts | 20 + server/src/jobs/ComputeItemCost.ts | 9 +- .../src/jobs/MailNotificationSubscribeEnd.ts | 8 +- server/src/jobs/MailNotificationTrialEnd.ts | 6 +- server/src/jobs/ResetPasswordMail.ts | 60 ++- .../src/jobs/SMSNotificationSubscribeEnd.ts | 6 +- server/src/jobs/SMSNotificationTrialEnd.ts | 6 +- server/src/jobs/SendVoucherPhone.ts | 3 +- server/src/jobs/UserInviteMail.ts | 28 +- server/src/jobs/WelcomeSMS.ts | 28 ++ server/src/jobs/welcomeEmail.ts | 42 +-- server/src/jobs/writeInvoicesJEntries.ts | 6 +- server/src/lib/Metable/MetableCollection.js | 266 -------------- server/src/lib/Metable/MetableStore.ts | 204 +++++++++++ server/src/lib/Metable/MetableStoreDB.ts | 213 +++++++++++ server/src/loaders/dbManager.ts | 14 + server/src/loaders/dependencyInjector.ts | 7 +- server/src/loaders/index.ts | 2 +- server/src/loaders/jobs.ts | 15 +- .../Logger/index.js => loaders/logger.ts} | 0 server/src/loaders/tenantModels.ts | 70 ++++ server/src/models/Budget.js | 60 --- server/src/models/BudgetEntry.js | 10 - server/src/models/Option.js | 22 -- server/src/models/Permission.js | 43 --- server/src/models/Resource.js | 17 - server/src/models/ResourceFieldMetadata.js | 8 - server/src/models/Role.js | 91 ----- server/src/models/TenantUser.js | 26 +- .../AuthenticationMailMessages.ts | 35 ++ .../AuthenticationSMSMessages.ts | 13 + server/src/services/Authentication/index.ts | 25 +- .../InviteUsers/InviteUsersMailMessages.ts | 31 ++ .../InviteUsers/InviteUsersSMSMessages.ts | 0 server/src/services/InviteUsers/index.ts | 172 +++++++++ server/src/services/Moment/index.js | 6 - server/src/services/Organization/index.ts | 56 +++ .../Permissions/PermissionsService.js | 77 ---- server/src/services/Sales/PaymentsReceives.ts | 10 + server/src/services/Sales/SalesEstimate.ts | 22 +- server/src/services/Sales/SalesInvoices.ts | 35 +- server/src/services/Settings/SettingsStore.ts | 15 + server/src/services/Tenancy/TenancyService.ts | 1 - server/src/subscribers/events.ts | 8 +- server/src/subscribers/inviteUser.ts | 29 ++ server/src/system/TenantEnvironment.js | 12 - .../20200420134631_create_tenants_table.js | 1 + server/src/system/models/SystemOption.js | 18 - server/tests/lib/MetableStore.test.ts | 39 ++ server/tests/models/Option.test.js | 18 - server/tests/routes/bills.test.js | 2 +- server/tsconfig.json | 2 + 98 files changed, 1697 insertions(+), 2052 deletions(-) delete mode 100644 server/bin/license.js delete mode 100644 server/src/database/migrations/20190423085238_create_permissions_table.js delete mode 100644 server/src/database/migrations/20190423085247_create_roles_table.js delete mode 100644 server/src/database/migrations/20190822214244_create_user_has_roles_table.js delete mode 100644 server/src/database/migrations/20190822214905_create_role_has_accounts.js delete mode 100644 server/src/database/migrations/20190822214905_create_role_has_permissions.js delete mode 100644 server/src/database/migrations/20190822214905_create_views_roles_table.js delete mode 100644 server/src/database/migrations/20200105023827_create_currency_adjustments_table.js delete mode 100644 server/src/database/migrations/20200108204331_make_recurring_journals_table.js delete mode 100644 server/src/database/migrations/20200120145228_create_budgets_table.js delete mode 100644 server/src/database/migrations/20200120145342_create_budget_entries_table.js create mode 100644 server/src/exceptions/HttpException.ts delete mode 100644 server/src/http/controllers/AccountOpeningBalance.js delete mode 100644 server/src/http/controllers/Banking.js delete mode 100644 server/src/http/controllers/InviteUsers.js create mode 100644 server/src/http/controllers/InviteUsers.ts delete mode 100644 server/src/http/controllers/Options.js create mode 100644 server/src/http/controllers/Organization.ts delete mode 100644 server/src/http/controllers/Roles.js create mode 100644 server/src/http/controllers/Settings.ts create mode 100644 server/src/http/middleware/AttachCurrentTenantUser.ts create mode 100644 server/src/http/middleware/EnsureTenantIsInitialized.ts create mode 100644 server/src/http/middleware/LoggerMiddleware.ts create mode 100644 server/src/http/middleware/SettingsMiddleware.ts delete mode 100644 server/src/http/middleware/TenantDependencyInjection.ts delete mode 100644 server/src/http/middleware/authorization.js delete mode 100644 server/src/http/middleware/prettierMiddleware.ts create mode 100644 server/src/interfaces/Metable.ts create mode 100644 server/src/interfaces/Options.ts create mode 100644 server/src/jobs/WelcomeSMS.ts delete mode 100644 server/src/lib/Metable/MetableCollection.js create mode 100644 server/src/lib/Metable/MetableStore.ts create mode 100644 server/src/lib/Metable/MetableStoreDB.ts create mode 100644 server/src/loaders/dbManager.ts rename server/src/{services/Logger/index.js => loaders/logger.ts} (100%) create mode 100644 server/src/loaders/tenantModels.ts delete mode 100644 server/src/models/Budget.js delete mode 100644 server/src/models/BudgetEntry.js delete mode 100644 server/src/models/Permission.js delete mode 100644 server/src/models/Role.js create mode 100644 server/src/services/Authentication/AuthenticationMailMessages.ts create mode 100644 server/src/services/Authentication/AuthenticationSMSMessages.ts create mode 100644 server/src/services/InviteUsers/InviteUsersMailMessages.ts create mode 100644 server/src/services/InviteUsers/InviteUsersSMSMessages.ts create mode 100644 server/src/services/InviteUsers/index.ts delete mode 100644 server/src/services/Moment/index.js create mode 100644 server/src/services/Organization/index.ts delete mode 100644 server/src/services/Permissions/PermissionsService.js create mode 100644 server/src/services/Settings/SettingsStore.ts create mode 100644 server/src/subscribers/inviteUser.ts delete mode 100644 server/src/system/TenantEnvironment.js create mode 100644 server/tests/lib/MetableStore.test.ts delete mode 100644 server/tests/models/Option.test.js diff --git a/server/bin/bigcapital.js b/server/bin/bigcapital.js index 8169a41f7..0bc3d39a3 100644 --- a/server/bin/bigcapital.js +++ b/server/bin/bigcapital.js @@ -10,7 +10,6 @@ const { success, log, } = require('./utils'); -const lincenseCommander = require('./license'); // - bigcapital system:migrate:latest // - bigcapital system:migrate:rollback @@ -21,9 +20,6 @@ const lincenseCommander = require('./license'); // - bigcapital tenants:migrate:make // - bigcapital system:migrate:make // - bigcapital tenants:list -// -// - bigcapital license:generate -// - bigcapital licenses:list commander .command('system:migrate:rollback') diff --git a/server/bin/license.js b/server/bin/license.js deleted file mode 100644 index b921015d9..000000000 --- a/server/bin/license.js +++ /dev/null @@ -1,55 +0,0 @@ -const commander = require('commander'); -const color = require('colorette'); -const argv = require('getopts')(process.argv.slice(2)); -const cryptoRandomString = require('crypto-random-string'); -const { - initSystemKnex, - getAllSystemTenants, - initTenantKnex, - exit, - success, - log, -} = require('./utils'); - -// License generate key. -commander - .command('license:generate ') - .description('Generates a new license key.') - .action(async (interval) => { - try { - const sysDb = initSystemKnex(); - let repeat = true; - - while(repeat) { - key = cryptoRandomString(16).toUpperCase(); - const license = await sysDb('subscription_licenses').where('key', key); - - if (license.length === 0) { - repeat = false; - } - } - const licenseIds = await sysDb('subscription_licenses').insert({ - key, - license_period: interval ? parseInt(interval, 10) : 1, - license_interval: 'month', - }); - const license = await sysDb('subscription_licenses').where('id', licenseIds[0]).first(); - success(`ID: ${license.id} | License: ${license.key} | Interval: ${license.licenseInterval} | Period: ${license.licensePeriod}`); - } catch(error) { - exit(error); - } - }); - -// Retrieve licenses list. -commander - .command('licenses:list') - .description('Retrieve a list of subscription licenses.') - .action(async () => { - const sysDb = initSystemKnex(); - const licenses = await sysDb('subscription_licenses'); - - licenses.forEach((license) => { - log(`ID: ${license.id} | Key: ${license.key} | Interval: ${license.licenseInterval} | Period: ${license.licensePeriod}`); - }); - exit(); - }); \ No newline at end of file diff --git a/server/config/config.js b/server/config/config.js index 23d1c8d44..a3efaf32d 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -1,5 +1,10 @@ module.exports = { + /** + * Your favorite port + */ + port: parseInt(process.env.PORT, 10), + system: { db_client: 'mysql', db_host: '127.0.0.1', diff --git a/server/src/database/manager.js b/server/src/database/manager.js index 675954d19..9a002432e 100644 --- a/server/src/database/manager.js +++ b/server/src/database/manager.js @@ -4,13 +4,11 @@ import config from '@/../config/config'; const knexConfig = knexfile[process.env.NODE_ENV]; -const dbManager = knexManager.databaseManagerFactory({ +export default () => knexManager.databaseManagerFactory({ knex: knexConfig, dbManager: { collate: [], superUser: config.manager.superUser, superPassword: config.manager.superPassword, }, -}); - -export default dbManager; \ No newline at end of file +}); \ No newline at end of file diff --git a/server/src/database/migrations/20190423085238_create_permissions_table.js b/server/src/database/migrations/20190423085238_create_permissions_table.js deleted file mode 100644 index 21826ece5..000000000 --- a/server/src/database/migrations/20190423085238_create_permissions_table.js +++ /dev/null @@ -1,11 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('permissions', (table) => { - table.increments(); - table.string('name'); - }); -}; - -exports.down = function (knex) { - return knex.schema.dropTable('permissions'); -}; diff --git a/server/src/database/migrations/20190423085247_create_roles_table.js b/server/src/database/migrations/20190423085247_create_roles_table.js deleted file mode 100644 index 4f1c6c435..000000000 --- a/server/src/database/migrations/20190423085247_create_roles_table.js +++ /dev/null @@ -1,12 +0,0 @@ - -exports.up = (knex) => knex.schema.createTable('roles', (table) => { - table.increments(); - table.string('name'); - table.string('description'); - table.boolean('predefined').default(false); - table.timestamps(); -}).raw('ALTER TABLE `ROLES` AUTO_INCREMENT = 1000'); - - - -exports.down = (knex) => knex.schema.dropTable('roles'); diff --git a/server/src/database/migrations/20190822214244_create_user_has_roles_table.js b/server/src/database/migrations/20190822214244_create_user_has_roles_table.js deleted file mode 100644 index c35b826c4..000000000 --- a/server/src/database/migrations/20190822214244_create_user_has_roles_table.js +++ /dev/null @@ -1,12 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('user_has_roles', (table) => { - table.increments(); - table.integer('user_id').unsigned(); - table.integer('role_id').unsigned(); - }); -}; - -exports.down = function (knex) { - return knex.schema.dropTableIfExists('user_has_roles'); -}; diff --git a/server/src/database/migrations/20190822214905_create_role_has_accounts.js b/server/src/database/migrations/20190822214905_create_role_has_accounts.js deleted file mode 100644 index 6bc9c0d15..000000000 --- a/server/src/database/migrations/20190822214905_create_role_has_accounts.js +++ /dev/null @@ -1,10 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('role_has_accounts', (table) => { - table.increments(); - table.integer('role_id').unsigned(); - table.integer('account_id').unsigned(); - }); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('role_has_accounts'); diff --git a/server/src/database/migrations/20190822214905_create_role_has_permissions.js b/server/src/database/migrations/20190822214905_create_role_has_permissions.js deleted file mode 100644 index d25020eab..000000000 --- a/server/src/database/migrations/20190822214905_create_role_has_permissions.js +++ /dev/null @@ -1,11 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('role_has_permissions', (table) => { - table.increments(); - table.integer('role_id').unsigned(); - table.integer('permission_id').unsigned(); - table.integer('resource_id').unsigned(); - }); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('role_has_permissions'); diff --git a/server/src/database/migrations/20190822214905_create_views_roles_table.js b/server/src/database/migrations/20190822214905_create_views_roles_table.js deleted file mode 100644 index 59f9ccf1f..000000000 --- a/server/src/database/migrations/20190822214905_create_views_roles_table.js +++ /dev/null @@ -1,17 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('view_roles', (table) => { - table.increments(); - table.integer('index'); - table.integer('field_id').unsigned(); - table.string('comparator'); - table.string('value'); - table.integer('view_id').unsigned(); - }).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000').then(() => { - return knex.seed.run({ - specific: 'seed_views_roles.js', - }); - }); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('view_roles'); diff --git a/server/src/database/migrations/20200105023827_create_currency_adjustments_table.js b/server/src/database/migrations/20200105023827_create_currency_adjustments_table.js deleted file mode 100644 index c4a72a743..000000000 --- a/server/src/database/migrations/20200105023827_create_currency_adjustments_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('currency_adjustments', (table) => { - table.increments(); - table.date('date'); - table.string('currency_code'); - table.decimal('exchange_rate', 8, 5); - table.string('note'); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('currency_adjustments'); -}; diff --git a/server/src/database/migrations/20200108204331_make_recurring_journals_table.js b/server/src/database/migrations/20200108204331_make_recurring_journals_table.js deleted file mode 100644 index 3b74e959d..000000000 --- a/server/src/database/migrations/20200108204331_make_recurring_journals_table.js +++ /dev/null @@ -1,12 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('recurring_journals', (table) => { - table.increments(); - table.string('template_name'); - table.timestamps(); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('recurring_journals'); -}; diff --git a/server/src/database/migrations/20200120145228_create_budgets_table.js b/server/src/database/migrations/20200120145228_create_budgets_table.js deleted file mode 100644 index ac27a72b9..000000000 --- a/server/src/database/migrations/20200120145228_create_budgets_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('budgets', (table) => { - table.increments(); - table.string('name'); - table.string('fiscal_year'); - table.string('period'); - table.string('account_types'); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('budgets'); -}; diff --git a/server/src/database/migrations/20200120145342_create_budget_entries_table.js b/server/src/database/migrations/20200120145342_create_budget_entries_table.js deleted file mode 100644 index bd7795ef5..000000000 --- a/server/src/database/migrations/20200120145342_create_budget_entries_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('budget_entries', (table) => { - table.increments(); - table.integer('budget_id').unsigned(); - table.integer('account_id').unsigned(); - table.decimal('amount', 15, 5); - table.integer('order'); - }) -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('budget_entries'); -}; diff --git a/server/src/decorators/eventDispatcher.ts b/server/src/decorators/eventDispatcher.ts index 219a583aa..4ac9a6d50 100644 --- a/server/src/decorators/eventDispatcher.ts +++ b/server/src/decorators/eventDispatcher.ts @@ -1,8 +1,3 @@ -/** - * Originally taken from 'w3tecch/express-typescript-boilerplate' - * Credits to the author - */ - import { EventDispatcher as EventDispatcherClass } from 'event-dispatch'; import { Container } from 'typedi'; diff --git a/server/src/exceptions/HttpException.ts b/server/src/exceptions/HttpException.ts new file mode 100644 index 000000000..217b082ae --- /dev/null +++ b/server/src/exceptions/HttpException.ts @@ -0,0 +1,9 @@ +class HttpException extends Error { + public status: number; + public message: string; + constructor(status: number, message: string) { + super(message); + this.status = status; + this.message = message; + } +} \ No newline at end of file diff --git a/server/src/http/controllers/AccountOpeningBalance.js b/server/src/http/controllers/AccountOpeningBalance.js deleted file mode 100644 index 8f19c32a3..000000000 --- a/server/src/http/controllers/AccountOpeningBalance.js +++ /dev/null @@ -1,147 +0,0 @@ -import express from 'express'; -import { check, validationResult, oneOf } from 'express-validator'; -import { difference } from 'lodash'; -import moment from 'moment'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import jwtAuth from '@/http/middleware/jwtAuth'; -import JournalPoster from '@/services/Accounting/JournalPoster'; -import JournalEntry from '@/services/Accounting/JournalEntry'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.use(jwtAuth); - - router.post('/', - this.openingBalnace.validation, - asyncMiddleware(this.openingBalnace.handler)); - - return router; - }, - - /** - * Opening balance to the given account. - * @param {Request} req - - * @param {Response} res - - */ - openingBalnace: { - validation: [ - check('date').optional(), - check('note').optional().trim().escape(), - check('balance_adjustment_account').exists().isNumeric().toInt(), - check('accounts').isArray({ min: 1 }), - check('accounts.*.id').exists().isInt(), - oneOf([ - check('accounts.*.debit').exists().isNumeric().toFloat(), - check('accounts.*.credit').exists().isNumeric().toFloat(), - ]), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - - const { accounts } = req.body; - const { user } = req; - const form = { ...req.body }; - 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])); - - // Get the stored accounts Ids and difference with submit accounts. - const accountsStoredIds = storedAccounts.map((account) => account.id); - const notFoundAccountsIds = difference(accountsIds, accountsStoredIds); - const errorReasons = []; - - if (notFoundAccountsIds.length > 0) { - const ids = notFoundAccountsIds.map((a) => parseInt(a, 10)); - errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids }); - } - if (form.balance_adjustment_account) { - const account = await Account.query().findById(form.balance_adjustment_account); - - if (!account) { - errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 }); - } - } - if (errorReasons.length > 0) { - return res.boom.badData(null, { errors: errorReasons }); - } - - const journalEntries = new JournalPoster(); - - accounts.forEach((account) => { - const storedAccount = accountsCollection.get(account.id); - - // Can't continue in case the stored account was not found. - if (!storedAccount) { return; } - - const entryModel = new JournalEntry({ - referenceType: 'OpeningBalance', - account: account.id, - accountNormal: storedAccount.type.normal, - userId: user.id, - }); - if (account.credit) { - entryModel.entry.credit = account.credit; - journalEntries.credit(entryModel); - } else if (account.debit) { - entryModel.entry.debit = account.debit; - journalEntries.debit(entryModel); - } - }); - // Calculates the credit and debit balance of stacked entries. - const trial = journalEntries.getTrialBalance(); - - if (trial.credit !== trial.debit) { - const entryModel = new JournalEntry({ - referenceType: 'OpeningBalance', - account: form.balance_adjustment_account, - accountNormal: 'credit', - userId: user.id, - }); - - if (trial.credit > trial.debit) { - entryModel.entry.credit = Math.abs(trial.credit); - journalEntries.credit(entryModel); - - } else if (trial.credit < trial.debit) { - entryModel.entry.debit = Math.abs(trial.debit); - journalEntries.debit(entryModel); - } - } - const manualJournal = await ManualJournal.query().insert({ - amount: Math.max(trial.credit, trial.debit), - transaction_type: 'OpeningBalance', - date, - note: form.note, - user_id: user.id, - }); - - journalEntries.entries = journalEntries.entries.map((entry) => ({ - ...entry, - referenceId: manualJournal.id, - })); - await Promise.all([ - journalEntries.saveEntries(), - journalEntries.saveBalance(), - ]); - return res.status(200).send({ id: manualJournal.id }); - }, - }, -}; diff --git a/server/src/http/controllers/Authentication.ts b/server/src/http/controllers/Authentication.ts index 5fc3d36f3..9b0d1537c 100644 --- a/server/src/http/controllers/Authentication.ts +++ b/server/src/http/controllers/Authentication.ts @@ -1,17 +1,15 @@ import { Request, Response, Router } from 'express'; -import { check, validationResult, matchedData, ValidationChain } from 'express-validator'; +import { check, ValidationChain } from 'express-validator'; import { Service, Inject } from 'typedi'; -import { camelCase, mapKeys } from 'lodash'; +import BaseController from '@/http/controllers/BaseController'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import prettierMiddleware from '@/http/middleware/prettierMiddleware'; import AuthenticationService from '@/services/Authentication'; import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces'; import { ServiceError, ServiceErrors } from "@/exceptions"; -import { IRegisterDTO } from 'src/interfaces'; @Service() -export default class AuthenticationController { +export default class AuthenticationController extends BaseController{ @Inject() authService: AuthenticationService; @@ -88,6 +86,9 @@ export default class AuthenticationController { ] } + /** + * Send reset password validation schema. + */ get sendResetPasswordSchema(): ValidationChain[] { return [ check('email').exists().isEmail().trim().escape(), @@ -100,10 +101,7 @@ export default class AuthenticationController { * @param {Response} res */ async login(req: Request, res: Response, next: Function): Response { - const userDTO: IUserOTD = mapKeys(matchedData(req, { - locations: ['body'], - includeOptionals: true, - }), (v, k) => camelCase(k)); + const userDTO: IUserOTD = this.matchedBodyData(req); try { const { token, user } = await this.authService.signIn( @@ -134,13 +132,10 @@ export default class AuthenticationController { * @param {Response} res */ async register(req: Request, res: Response, next: Function) { - const registerDTO: IRegisterDTO = mapKeys(matchedData(req, { - locations: ['body'], - includeOptionals: true, - }), (v, k) => camelCase(k)); + const registerDTO: IRegisterOTD = this.matchedBodyData(req); try { - const registeredUser = await this.authService.register(registerDTO); + const registeredUser: ISystemUser = await this.authService.register(registerDTO); return res.status(200).send({ code: 'REGISTER.SUCCESS', @@ -170,7 +165,7 @@ export default class AuthenticationController { * @param {Response} res */ async sendResetPassword(req: Request, res: Response, next: Function) { - const { email } = req.body; + const { email } = this.matchedBodyData(req); try { await this.authService.sendResetPassword(email); diff --git a/server/src/http/controllers/Banking.js b/server/src/http/controllers/Banking.js deleted file mode 100644 index c643f6bca..000000000 --- a/server/src/http/controllers/Banking.js +++ /dev/null @@ -1,33 +0,0 @@ - -import express from 'express'; - -export default { - - - router() { - const router = express.Router(); - - return router; - }, - - reconciliations: { - validation: [ - - ], - async handler(req, res) { - - }, - }, - - reconciliation: { - validation: [ - body('from_date'), - body('to_date'), - body('closing_balance'), - - ], - async handler(req, res) { - - }, - }, -} \ No newline at end of file diff --git a/server/src/http/controllers/BaseController.ts b/server/src/http/controllers/BaseController.ts index a1aea3170..ec7de3fb8 100644 --- a/server/src/http/controllers/BaseController.ts +++ b/server/src/http/controllers/BaseController.ts @@ -1,3 +1,14 @@ +import { matchedData } from "express-validator"; +import { mapKeys, camelCase, omit } from "lodash"; export default class BaseController { + + matchedBodyData(req: Request, options: any) { + const data = matchedData(req, { + locations: ['body'], + includeOptionals: true, + ...omit(options, ['locations']), // override any propery except locations. + }); + return mapKeys(data, (v, k) => camelCase(k)); + } } \ No newline at end of file diff --git a/server/src/http/controllers/Expenses.js b/server/src/http/controllers/Expenses.js index d17e5d4b9..70a05022b 100644 --- a/server/src/http/controllers/Expenses.js +++ b/server/src/http/controllers/Expenses.js @@ -20,7 +20,6 @@ export default { */ router() { const router = express.Router(); - router.use(JWTAuth); router.post( '/', diff --git a/server/src/http/controllers/InviteUsers.js b/server/src/http/controllers/InviteUsers.js deleted file mode 100644 index 9f385fa46..000000000 --- a/server/src/http/controllers/InviteUsers.js +++ /dev/null @@ -1,251 +0,0 @@ -import express from 'express'; -import uniqid from 'uniqid'; -import { - check, - body, - param, - validationResult, -} from 'express-validator'; -import path from 'path'; -import fs from 'fs'; -import Mustache from 'mustache'; -import moment from 'moment'; -import { hashPassword } from '@/utils'; -import SystemUser from '@/system/models/SystemUser'; -import Invite from '@/system/models/Invite'; -import TenantUser from '@/models/TenantUser'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Tenant from '@/system/models/Tenant'; -import TenantsManager from '@/system/TenantsManager'; -import jwtAuth from '@/http/middleware/jwtAuth'; -import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; -import TenantModel from '@/models/TenantModel'; -import Logger from '@/services/Logger'; -import Option from '@/models/Option'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.use('/send', jwtAuth); - router.use('/send', TenancyMiddleware); - - router.post('/send', - this.invite.validation, - asyncMiddleware(this.invite.handler)); - - router.post('/accept/:token', - this.accept.validation, - asyncMiddleware(this.accept.handler)); - - router.get('/invited/:token', - this.invited.validation, - asyncMiddleware(this.invited.handler)); - - return router; - }, - - /** - * Invite a user to the authorized user organization. - */ - invite: { - validation: [ - body('email').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 }; - const { user } = req; - const { TenantUser } = req.models; - const foundUser = await SystemUser.query() - .where('email', form.email).first(); - - if (foundUser) { - return res.status(400).send({ - errors: [{ type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100 }], - }); - } - const token = uniqid(); - const invite = await Invite.query().insert({ - email: form.email, - tenant_id: user.tenantId, - token, - }); - const tenantUser = await TenantUser.query().insert({ - first_name: form.email, - email: form.email, - }); - const { Option } = req.models; - const organizationOptions = await Option.query() - .where('key', 'organization_name'); - - const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html'); - const template = fs.readFileSync(filePath, 'utf8'); - - const rendered = Mustache.render(template, { - acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`, - fullName: `${user.firstName} ${user.lastName}`, - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - organizationName: organizationOptions.getMeta('organization_name'), - }); - const mailOptions = { - to: user.email, - from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, - subject: `${user.fullName} has invited you to join a Bigcapital`, - html: rendered, - }; - mail.sendMail(mailOptions, (error) => { - if (error) { - Logger.log('error', 'Failed send user invite mail', { error, form }); - } - Logger.log('info', 'User has been sent invite user email successfuly.', { form }); - }); - return res.status(200).send(); - } - }, - - /** - * Acceprt the inviation. - */ - accept: { - validation: [ - check('first_name').exists().trim().escape(), - check('last_name').exists().trim().escape(), - check('phone_number').exists().trim().escape(), - check('password').exists().trim().escape(), - param('token').exists().trim().escape(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - - const { token } = req.params; - const inviteToken = await Invite.query() - .where('token', token).first(); - - if (!inviteToken) { - return res.status(404).send({ - errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }], - }); - } - const form = { - language: 'en', - ...req.body, - }; - const systemUser = await SystemUser.query() - .where('phone_number', form.phone_number) - .first(); - - const errorReasons = []; - - // Validate there is already registered phone number. - if (systemUser && systemUser.phoneNumber === form.phone_number) { - errorReasons.push({ - type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400, - }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Find the tenant that associated to the given token. - const tenant = await Tenant.query() - .where('id', inviteToken.tenantId).first(); - - const tenantDb = TenantsManager.knexInstance(tenant.organizationId); - const hashedPassword = await hashPassword(form.password); - - const userForm = { - first_name: form.first_name, - last_name: form.last_name, - email: inviteToken.email, - phone_number: form.phone_number, - language: form.language, - active: 1, - }; - TenantModel.knexBinded = tenantDb; - - const foundTenantUser = await TenantUser.query() - .where('phone_number', form.phone_number).first(); - - if (foundTenantUser) { - return res.status(400).send({ - errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }], - }); - } - - const insertUserOper = TenantUser.bindKnex(tenantDb) - .query() - .where('email', userForm.email) - .patch({ - ...userForm, - invite_accepted_at: moment().format('YYYY/MM/DD'), - }); - - const insertSysUserOper = SystemUser.query().insert({ - ...userForm, - password: hashedPassword, - tenant_id: inviteToken.tenantId, - }); - - const deleteInviteTokenOper = Invite.query() - .where('token', inviteToken.token).delete(); - - await Promise.all([ - insertUserOper, - insertSysUserOper, - deleteInviteTokenOper, - ]); - return res.status(200).send(); - }, - }, - - /** - * Get - */ - invited: { - validation: [ - param('token').exists().trim().escape(), - ], - async handler(req, res) { - const { token } = req.params; - const inviteToken = await Invite.query() - .where('token', token).first(); - - if (!inviteToken) { - return res.status(404).send({ - errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }], - }); - } - // Find the tenant that associated to the given token. - const tenant = await Tenant.query() - .where('id', inviteToken.tenantId).first(); - - const tenantDb = TenantsManager.knexInstance(tenant.organizationId); - const organizationOptions = await Option.bindKnex(tenantDb).query() - .where('key', 'organization_name'); - - return res.status(200).send({ - data: { - organization_name: organizationOptions.getMeta('organization_name', '') , - invited_email: inviteToken.email, - }, - }); - }, - }, -} \ No newline at end of file diff --git a/server/src/http/controllers/InviteUsers.ts b/server/src/http/controllers/InviteUsers.ts new file mode 100644 index 000000000..016230977 --- /dev/null +++ b/server/src/http/controllers/InviteUsers.ts @@ -0,0 +1,140 @@ +import { Service, Inject } from 'typedi'; +import { Router, Request, Response } from 'express'; +import { + check, + body, + param, + matchedData, +} from 'express-validator'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; +import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import InviteUserService from '@/services/InviteUsers'; +import { ServiceErrors, ServiceError } from '@/exceptions'; + +@Service() +export default class InviteUsersController { + @Inject() + inviteUsersService: InviteUserService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.use('/send', jwtAuth); + router.use('/send', TenancyMiddleware); + + router.post('/send', [ + body('email').exists().trim().escape(), + ], + asyncMiddleware(this.sendInvite), + ); + + router.post('/accept/:token', [ + check('first_name').exists().trim().escape(), + check('last_name').exists().trim().escape(), + check('phone_number').exists().trim().escape(), + check('password').exists().trim().escape(), + param('token').exists().trim().escape(), + ], + asyncMiddleware(this.accept) + ); + + router.get('/invited/:token', [ + param('token').exists().trim().escape(), + ], + asyncMiddleware(this.invited) + ); + return router; + } + + /** + * Invite a user to the authorized user organization. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async sendInvite(req: Request, res: Response, next: Function) { + const { email } = req.body; + const { tenantId } = req; + const { user } = req; + + try { + await this.inviteUsersService.sendInvite(tenantId, email, user); + } catch (error) { + console.log(error); + if (error instanceof ServiceError) { + if (error.errorType === 'email_already_invited') { + return res.status(400).send({ + errors: [{ type: 'EMAIL.ALREADY.INVITED' }], + }); + } + } + next(error); + } + return res.status(200).send(); + } + + /** + * Accept the inviation. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async accept(req: Request, res: Response, next: Function) { + const inviteUserInput: IInviteUserInput = matchedData(req, { + locations: ['body'], + includeOptionals: true, + }); + const { token } = req.params; + + try { + await this.inviteUsersService.acceptInvite(token, inviteUserInput); + return res.status(200).send(); + } catch (error) { + + if (error instanceof ServiceErrors) { + const errorReasons = []; + + if (error.hasType('email_exists')) { + errorReasons.push({ type: 'EMAIL.EXISTS' }); + } + if (error.hasType('phone_number_exists')) { + errorReasons.push({ type: 'PHONE_NUMBER.EXISTS' }); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + } + next(error); + } + } + + /** + * Check if the invite token is valid. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async invited(req: Request, res: Response, next: Function) { + const { token } = req.params; + + try { + await this.inviteUsersService.checkInvite(token); + + return res.status(200).send(); + } catch (error) { + + if (error instanceof ServiceError) { + if (error.errorType === 'invite_token_invalid') { + return res.status(400).send({ + errors: [{ type: 'INVITE.TOKEN.INVALID' }], + }); + } + } + next(error); + } + } +} \ No newline at end of file diff --git a/server/src/http/controllers/ItemCategories.ts b/server/src/http/controllers/ItemCategories.ts index 0c080e64c..2542fe68f 100644 --- a/server/src/http/controllers/ItemCategories.ts +++ b/server/src/http/controllers/ItemCategories.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import { Router, Request, Response } from 'express'; import { check, param, @@ -17,22 +17,21 @@ import { mapFilterRolesToDynamicFilter, } from '@/lib/ViewRolesBuilder'; import { IItemCategory, IItemCategoryOTD } from '@/interfaces'; -import PrettierMiddleware from '@/http/middleware/PrettierMiddleware'; +import BaseController from '@/http/controllers/BaseController'; @Service() -export default class ItemsCategoriesController { +export default class ItemsCategoriesController extends BaseController { /** * Router constructor method. */ - constructor() { - const router = express.Router(); + router() { + const router = Router(); router.post('/:id', [ ...this.categoryValidationSchema, ...this.specificCategoryValidationSchema, ], validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateParentCategoryExistance), asyncMiddleware(this.validateSellAccountExistance), asyncMiddleware(this.validateCostAccountExistance), @@ -42,7 +41,6 @@ export default class ItemsCategoriesController { router.post('/', this.categoryValidationSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateParentCategoryExistance), asyncMiddleware(this.validateSellAccountExistance), asyncMiddleware(this.validateCostAccountExistance), @@ -52,28 +50,24 @@ export default class ItemsCategoriesController { router.delete('/bulk', this.categoriesBulkValidationSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateCategoriesIdsExistance), asyncMiddleware(this.bulkDeleteCategories), ); router.delete('/:id', this.specificCategoryValidationSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateItemCategoryExistance), asyncMiddleware(this.deleteItem), ); router.get('/:id', this.specificCategoryValidationSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateItemCategoryExistance), asyncMiddleware(this.getCategory) ); router.get('/', this.categoriesListValidationSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.getList) ); return router; @@ -164,7 +158,7 @@ export default class ItemsCategoriesController { */ async validateCostAccountExistance(req: Request, res: Response, next: Function) { const { Account, AccountType } = req.models; - const category: IItemCategoryOTD = { ...req.body }; + const category: IItemCategoryOTD = this.matchedBodyData(req); if (category.costAccountId) { const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold'); @@ -191,7 +185,7 @@ export default class ItemsCategoriesController { */ async validateSellAccountExistance(req: Request, res: Response, next: Function) { const { Account, AccountType } = req.models; - const category: IItemCategoryOTD = { ...req.body }; + const category: IItemCategoryOTD = this.matchedBodyData(req); if (category.sellAccountId) { const incomeType = await AccountType.query().findOne('key', 'income'); @@ -218,7 +212,7 @@ export default class ItemsCategoriesController { */ async validateInventoryAccountExistance(req: Request, res: Response, next: Function) { const { Account, AccountType } = req.models; - const category: IItemCategoryOTD = { ...req.body }; + const category: IItemCategoryOTD = this.matchedBodyData(req); if (category.inventoryAccountId) { const otherAsset = await AccountType.query().findOne('key', 'other_asset'); @@ -244,7 +238,7 @@ export default class ItemsCategoriesController { * @param {Function} next */ async validateParentCategoryExistance(req: Request, res: Response, next: Function) { - const category: IItemCategory = { ...req.body }; + const category: IItemCategory = this.matchedBodyData(req); const { ItemCategory } = req.models; if (category.parentCategoryId) { @@ -290,7 +284,7 @@ export default class ItemsCategoriesController { */ async newCategory(req: Request, res: Response) { const { user } = req; - const category: IItemCategory = { ...req.body }; + const category: IItemCategory = this.matchedBodyData(req); const { ItemCategory } = req.models; const storedCategory = await ItemCategory.query().insert({ @@ -308,7 +302,7 @@ export default class ItemsCategoriesController { */ async editCategory(req: Request, res: Response) { const { id } = req.params; - const category: IItemCategory = { ...req.body }; + const category: IItemCategory = this.matchedBodyData(req); const { ItemCategory } = req.models; const updateItemCategory = await ItemCategory.query() diff --git a/server/src/http/controllers/Media.js b/server/src/http/controllers/Media.js index 91ea063c8..bddf125b8 100644 --- a/server/src/http/controllers/Media.js +++ b/server/src/http/controllers/Media.js @@ -5,10 +5,10 @@ import { query, validationResult, } from 'express-validator'; +import Container from 'typedi'; import fs from 'fs'; import { difference } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Logger from '@/services/Logger'; const fsPromises = fs.promises; @@ -70,6 +70,8 @@ export default { // check('attachment').exists(), ], async handler(req, res) { + const Logger = Container.get('logger'); + if (!req.files.attachment) { return res.status(400).send({ errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }], @@ -93,9 +95,9 @@ export default { try { await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`); - Logger.log('info', 'Attachment uploaded successfully'); + Logger.info('[attachment] uploaded successfully'); } catch (error) { - Logger.log('info', 'Attachment uploading failed.', { error }); + Logger.info('[attachment] uploading failed.', { error }); } const media = await Media.query().insert({ @@ -114,6 +116,7 @@ export default { query('ids.*').exists().isNumeric().toInt(), ], async handler(req, res) { + const Logger = Container.get('logger'); const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { @@ -142,12 +145,12 @@ export default { }); await Promise.all(unlinkOpers).then((resolved) => { resolved.forEach(() => { - Logger.log('error', 'Attachment file has been deleted.'); + Logger.info('[attachment] file has been deleted.'); }); }) .catch((errors) => { errors.forEach((error) => { - Logger.log('error', 'Delete item attachment file delete failed.', { error }); + Logger.info('[attachment] Delete item attachment file delete failed.', { error }); }) }); diff --git a/server/src/http/controllers/Options.js b/server/src/http/controllers/Options.js deleted file mode 100644 index 151270da9..000000000 --- a/server/src/http/controllers/Options.js +++ /dev/null @@ -1,97 +0,0 @@ -import express from 'express'; -import { body, query, validationResult } from 'express-validator'; -import { pick } from 'lodash'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.post('/', - this.saveOptions.validation, - asyncMiddleware(this.saveOptions.handler)); - - router.get('/', - this.getOptions.validation, - asyncMiddleware(this.getOptions.handler)); - - return router; - }, - - /** - * Saves the given options to the storage. - */ - saveOptions: { - validation: [ - body('options').isArray({ min: 1 }), - body('options.*.key').exists(), - body('options.*.value').exists(), - body('options.*.group').exists(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'VALIDATION_ERROR', ...validationErrors, - }); - } - const { Option } = req.models; - const form = { ...req.body }; - const optionsCollections = await Option.query(); - - const errorReasons = []; - const notDefinedOptions = Option.validateDefined(form.options); - - if (notDefinedOptions.length) { - errorReasons.push({ - type: 'OPTIONS.KEY.NOT.DEFINED', - code: 200, - keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })), - }); - } - if (errorReasons.length) { - return res.status(400).send({ errors: errorReasons }); - } - form.options.forEach((option) => { - optionsCollections.setMeta({ ...option }); - }); - await optionsCollections.saveMeta(); - - return res.status(200).send({ options: form }); - }, - }, - - /** - * Retrieve the application options from the storage. - */ - getOptions: { - validation: [ - query('key').optional(), - query('group').optional(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'VALIDATION_ERROR', ...validationErrors, - }); - } - const { Option } = req.models; - const filter = { ...req.query }; - const options = await Option.query().onBuild((builder) => { - if (filter.key) { - builder.where('key', filter.key); - } - if (filter.group) { - builder.where('group', filter.group); - } - }); - return res.status(200).send({ options: options.metadata }); - }, - }, -}; diff --git a/server/src/http/controllers/Organization.ts b/server/src/http/controllers/Organization.ts new file mode 100644 index 000000000..f23e68312 --- /dev/null +++ b/server/src/http/controllers/Organization.ts @@ -0,0 +1,65 @@ +import { Inject, Service } from 'typedi'; +import { Router, Request, Response } from 'express'; +import { check, matchedData } from 'express-validator'; +import { mapKeys, camelCase } from 'lodash'; +import asyncMiddleware from "@/http/middleware/asyncMiddleware"; +import validateMiddleware from '@/http/middleware/validateMiddleware'; +import OrganizationService from '@/services/Organization'; +import { ServiceError } from '@/exceptions'; + +@Service() +export default class OrganizationController { + @Inject() + organizationService: OrganizationService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.post( + '/build', [ + check('organization_id').exists().trim().escape(), + ], + validateMiddleware, + asyncMiddleware(this.build.bind(this)) + ); + return router; + } + + /** + * Builds tenant database and seed initial data. + * @param {Request} req - Express request. + * @param {Response} res - Express response. + * @param {NextFunction} next + */ + async build(req: Request, res: Response, next: Function) { + const buildOTD = mapKeys(matchedData(req, { + locations: ['body'], + includeOptionals: true, + }), (v, k) => camelCase(k)); + + try { + await this.organizationService.build(buildOTD.organizationId); + + return res.status(200).send({ + type: 'ORGANIZATION.DATABASE.INITIALIZED', + }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'tenant_not_found') { + return res.status(400).send({ + errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'tenant_initialized') { + return res.status(400).send({ + errors: [{ type: 'TENANT.DATABASE.ALREADY.BUILT', code: 200 }], + }); + } + } + next(error); + } + } +} \ No newline at end of file diff --git a/server/src/http/controllers/Roles.js b/server/src/http/controllers/Roles.js deleted file mode 100644 index 9d7fe70b9..000000000 --- a/server/src/http/controllers/Roles.js +++ /dev/null @@ -1,346 +0,0 @@ -/* eslint-disable no-unused-vars */ -import express from 'express'; -import { check, validationResult } from 'express-validator'; -import { difference } from 'lodash'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import Role from '@/models/Role'; -import Permission from '@/models/Permission'; -import Resource from '@/models/Resource'; -import knex from '@/database/knex'; - -const AccessControllSchema = [ - { - resource: 'items', - label: 'products_services', - permissions: ['create', 'edit', 'delete', 'view'], - fullAccess: true, - ownControl: true, - }, -]; - -const getResourceSchema = (resource) => AccessControllSchema - .find((schema) => schema.resource === resource); - -const getResourcePermissions = (resource) => { - const foundResource = getResourceSchema(resource); - return foundResource ? foundResource.permissions : []; -}; - -const findNotFoundResources = (resourcesSlugs) => { - const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource); - return difference(resourcesSlugs, schemaResourcesSlugs); -}; - -const findNotFoundPermissions = (permissions, resourceSlug) => { - const schemaPermissions = getResourcePermissions(resourceSlug); - return difference(permissions, schemaPermissions); -}; - -export default { - /** - * Router constructor method. - */ - router() { - const router = express.Router(); - - router.post('/', - this.newRole.validation, - asyncMiddleware(this.newRole.handler)); - - router.post('/:id', - this.editRole.validation, - asyncMiddleware(this.editRole.handler.bind(this))); - - router.delete('/:id', - this.deleteRole.validation, - asyncMiddleware(this.deleteRole.handler)); - - return router; - }, - - /** - * Creates a new role. - */ - newRole: { - validation: [ - check('name').exists().trim().escape(), - check('description').optional().trim().escape(), - check('permissions').isArray({ min: 0 }), - check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'), - check('permissions.*.permissions').isArray({ min: 1 }), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { name, description, permissions } = req.body; - - const resourcesSlugs = permissions.map((perm) => perm.resource_slug); - const permissionsSlugs = []; - const resourcesNotFound = findNotFoundResources(resourcesSlugs); - - const errorReasons = []; - const notFoundPermissions = []; - - if (resourcesNotFound.length > 0) { - errorReasons.push({ - type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound, - }); - } - permissions.forEach((perm) => { - const abilities = perm.permissions.map((ability) => ability); - - // Gets the not found permissions in the schema. - const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug); - - if (notFoundAbilities.length > 0) { - notFoundPermissions.push({ - resource_slug: perm.resource_slug, - permissions: notFoundAbilities, - }); - } else { - const perms = perm.permissions || []; - perms.forEach((permission) => { - if (perms.indexOf(permission) !== -1) { - permissionsSlugs.push(permission); - } - }); - } - }); - if (notFoundPermissions.length > 0) { - errorReasons.push({ - type: 'PERMISSIONS_SLUG_NOT_FOUND', - code: 200, - permissions: notFoundPermissions, - }); - } - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - // Permissions. - const [resourcesCollection, permsCollection] = await Promise.all([ - Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(), - Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(), - ]); - - const notStoredResources = difference( - resourcesSlugs, resourcesCollection.map((s) => s.name), - ); - const notStoredPermissions = difference( - permissionsSlugs, permsCollection.map((perm) => perm.slug), - ); - - const insertThread = []; - - if (notStoredResources.length > 0) { - insertThread.push(knex('resources').insert([ - ...notStoredResources.map((resource) => ({ name: resource })), - ])); - } - if (notStoredPermissions.length > 0) { - insertThread.push(knex('permissions').insert([ - ...notStoredPermissions.map((permission) => ({ name: permission })), - ])); - } - - await Promise.all(insertThread); - - const [storedPermissions, storedResources] = await Promise.all([ - Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(), - Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(), - ]); - - const storedResourcesSet = new Map(storedResources.map((resource) => [ - resource.attributes.name, resource.attributes.id, - ])); - const storedPermissionsSet = new Map(storedPermissions.map((perm) => [ - perm.attributes.name, perm.attributes.id, - ])); - const role = Role.forge({ name, description }); - - await role.save(); - - const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({ - role_id: role.id, - resource_id: storedResourcesSet.get(resource.resource_slug), - permission_id: storedPermissionsSet.get(perm), - }))); - - if (roleHasPerms.length > 0) { - await knex('role_has_permissions').insert(roleHasPerms[0]); - } - return res.status(200).send({ id: role.get('id') }); - }, - }, - - /** - * Edit the give role. - */ - editRole: { - validation: [ - check('name').exists().trim().escape(), - check('description').optional().trim().escape(), - check('permissions').isArray({ min: 0 }), - check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'), - check('permissions.*.permissions').isArray({ min: 1 }), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - - const { id } = req.params; - const role = await Role.where('id', id).fetch(); - - if (!role) { - return res.boom.notFound(null, { - errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }], - }); - } - - const { permissions } = req.body; - const errorReasons = []; - const permissionsSlugs = []; - const notFoundPermissions = []; - - const resourcesSlugs = permissions.map((perm) => perm.resource_slug); - const resourcesNotFound = findNotFoundResources(resourcesSlugs); - - if (resourcesNotFound.length > 0) { - errorReasons.push({ - type: 'RESOURCE_SLUG_NOT_FOUND', - code: 100, - resources: resourcesNotFound, - }); - } - - permissions.forEach((perm) => { - const abilities = perm.permissions.map((ability) => ability); - // Gets the not found permissions in the schema. - const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug); - - if (notFoundAbilities.length > 0) { - notFoundPermissions.push({ - resource_slug: perm.resource_slug, permissions: notFoundAbilities, - }); - } else { - const perms = perm.permissions || []; - perms.forEach((permission) => { - if (perms.indexOf(permission) !== -1) { - permissionsSlugs.push(permission); - } - }); - } - }); - - if (notFoundPermissions.length > 0) { - errorReasons.push({ - type: 'PERMISSIONS_SLUG_NOT_FOUND', - code: 200, - permissions: notFoundPermissions, - }); - } - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - - // Permissions. - const [resourcesCollection, permsCollection] = await Promise.all([ - Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(), - Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(), - ]); - - const notStoredResources = difference( - resourcesSlugs, resourcesCollection.map((s) => s.name), - ); - const notStoredPermissions = difference( - permissionsSlugs, permsCollection.map((perm) => perm.slug), - ); - const insertThread = []; - - if (notStoredResources.length > 0) { - insertThread.push(knex('resources').insert([ - ...notStoredResources.map((resource) => ({ name: resource })), - ])); - } - if (notStoredPermissions.length > 0) { - insertThread.push(knex('permissions').insert([ - ...notStoredPermissions.map((permission) => ({ name: permission })), - ])); - } - - await Promise.all(insertThread); - - const [storedPermissions, storedResources] = await Promise.all([ - Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(), - Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(), - ]); - - const storedResourcesSet = new Map(storedResources.map((resource) => [ - resource.attributes.name, resource.attributes.id, - ])); - const storedPermissionsSet = new Map(storedPermissions.map((perm) => [ - perm.attributes.name, perm.attributes.id, - ])); - - await role.save(); - - - const savedRoleHasPerms = await knex('role_has_permissions').where({ - role_id: role.id, - }); - - console.log(savedRoleHasPerms); - - // const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({ - // role_id: role.id, - // resource_id: storedResourcesSet.get(resource.resource_slug), - // permission_id: storedPermissionsSet.get(perm), - // }))); - - // if (roleHasPerms.length > 0) { - // await knex('role_has_permissions').insert(roleHasPerms[0]); - // } - return res.status(200).send({ id: role.get('id') }); - }, - }, - - deleteRole: { - validation: [], - async handler(req, res) { - const { id } = req.params; - const role = await Role.where('id', id).fetch(); - - if (!role) { - return res.boom.notFound(); - } - if (role.attributes.predefined) { - return res.boom.badRequest(null, { - errors: [{ type: 'ROLE_PREDEFINED', code: 100 }], - }); - } - - await knex('role_has_permissions') - .where('role_id', role.id).delete({ require: false }); - - await role.destroy(); - - return res.status(200).send(); - }, - }, - - getRole: { - validation: [], - handler(req, res) { - return res.status(200).send(); - }, - }, -}; diff --git a/server/src/http/controllers/Settings.ts b/server/src/http/controllers/Settings.ts new file mode 100644 index 000000000..e7b968e88 --- /dev/null +++ b/server/src/http/controllers/Settings.ts @@ -0,0 +1,89 @@ +import { Router, Request, Response } from 'express'; +import { body, query, validationResult } from 'express-validator'; +import { pick } from 'lodash'; +import { IOptionDTO, IOptionsDTO } from '@/interfaces'; +import BaseController from '@/http/controllers/BaseController'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; + +export default class SettingsController extends BaseController{ + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.post('/', + this.saveSettingsValidationSchema, + asyncMiddleware(this.saveSettings.bind(this))); + + router.get('/', + this.getSettingsSchema, + asyncMiddleware(this.getSettings.bind(this))); + + return router; + } + + /** + * Save settings validation schema. + */ + get saveSettingsValidationSchema() { + return [ + body('options').isArray({ min: 1 }), + body('options.*.key').exists(), + body('options.*.value').exists(), + body('options.*.group').exists(), + ]; + } + + /** + * Retrieve the application options from the storage. + */ + get getSettingsSchema() { + return [ + query('key').optional(), + query('group').optional(), + ]; + } + + /** + * Saves the given options to the storage. + */ + saveSettings(req: Request, res: Response) { + const { Option } = req.models; + const optionsDTO: IOptionsDTO = this.matchedBodyData(req); + const { settings } = req; + + const errorReasons: { type: string, code: number, keys: [] }[] = []; + const notDefinedOptions = Option.validateDefined(optionsDTO.options); + + if (notDefinedOptions.length) { + errorReasons.push({ + type: 'OPTIONS.KEY.NOT.DEFINED', + code: 200, + keys: notDefinedOptions.map((o) => ({ + ...pick(o, ['key', 'group']) + })), + }); + } + if (errorReasons.length) { + return res.status(400).send({ errors: errorReasons }); + } + optionsDTO.options.forEach((option: IOptionDTO) => { + settings.set({ ...option }); + }); + + return res.status(200).send({ }); + } + + /** + * Retrieve settings. + * @param {Request} req + * @param {Response} res + */ + getSettings(req: Request, res: Response) { + const { settings } = req; + const allSettings = settings.all(); + + return res.status(200).send({ settings: allSettings }); + } +}; diff --git a/server/src/http/controllers/Subscription/PaymentMethod.ts b/server/src/http/controllers/Subscription/PaymentMethod.ts index 810fe73fe..fb01e7fc3 100644 --- a/server/src/http/controllers/Subscription/PaymentMethod.ts +++ b/server/src/http/controllers/Subscription/PaymentMethod.ts @@ -1,8 +1,9 @@ import { Inject } from 'typedi'; import { Plan } from '@/system/models'; +import BaseController from '@/http/controllers/BaseController'; import SubscriptionService from '@/services/Subscription/SubscriptionService'; -export default class PaymentMethodController { +export default class PaymentMethodController extends BaseController { @Inject() subscriptionService: SubscriptionService; @@ -16,7 +17,7 @@ export default class PaymentMethodController { * @return {Response|void} */ async validatePlanSlugExistance(req: Request, res: Response, next: Function) { - const { planSlug } = req.body; + const { planSlug } = this.matchedBodyData(req); const foundPlan = await Plan.query().where('slug', planSlug).first(); if (!foundPlan) { diff --git a/server/src/http/controllers/Subscription/PaymentViaVoucher.ts b/server/src/http/controllers/Subscription/PaymentViaVoucher.ts index 106b392a2..ea758b869 100644 --- a/server/src/http/controllers/Subscription/PaymentViaVoucher.ts +++ b/server/src/http/controllers/Subscription/PaymentViaVoucher.ts @@ -1,17 +1,19 @@ -import { Container, Service } from 'typedi'; +import { Inject, Service } from 'typedi'; import { Router, Request, Response } from 'express'; import { check, param, query, ValidationSchema } from 'express-validator'; import { Voucher, Plan } from '@/system/models'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod'; -import PrettierMiddleware from '@/http/middleware/PrettierMiddleware'; import { NotAllowedChangeSubscriptionPlan } from '@/exceptions'; @Service() export default class PaymentViaVoucherController extends PaymentMethodController { + @Inject('logger') + logger: any; + /** * Router constructor. */ @@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController '/payment', this.paymentViaVoucherSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateVoucherCodeExistance.bind(this)), asyncMiddleware(this.validatePlanSlugExistance.bind(this)), asyncMiddleware(this.validateVoucherAndPlan.bind(this)), @@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController * @param {Response} res */ async validateVoucherCodeExistance(req: Request, res: Response, next: Function) { - const { voucherCode } = req.body; + const { voucherCode } = this.matchedBodyData(req); + this.logger.info('[voucher_payment] trying to validate voucher code.', { voucherCode }); const foundVoucher = await Voucher.query() .modify('filterActiveVoucher') @@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController * @param {Function} next */ async validateVoucherAndPlan(req: Request, res: Response, next: Function) { - const { planSlug, voucherCode } = req.body; + const { planSlug, voucherCode } = this.matchedBodyData(req); + this.logger.info('[voucher_payment] trying to validate voucher with the plan.', { voucherCode }); const voucher = await Voucher.query().findOne('voucher_code', voucherCode); const plan = await Plan.query().findOne('slug', planSlug); @@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController * @return {Response} */ async paymentViaVoucher(req: Request, res: Response, next: Function) { - const { planSlug, voucherCode } = req.body; + const { planSlug, voucherCode } = this.matchedBodyData(req); const { tenant } = req; try { - await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode); + await this.subscriptionService + .subscriptionViaVoucher(tenant.id, planSlug, voucherCode); return res.status(200).send({ type: 'PAYMENT.SUCCESSFULLY.MADE', diff --git a/server/src/http/controllers/Subscription/Vouchers.ts b/server/src/http/controllers/Subscription/Vouchers.ts index 37174114a..3780527cd 100644 --- a/server/src/http/controllers/Subscription/Vouchers.ts +++ b/server/src/http/controllers/Subscription/Vouchers.ts @@ -1,16 +1,15 @@ import { Router, Request, Response } from 'express' -import { repeat, times, orderBy } from 'lodash'; -import { check, oneOf, param, query, ValidationChain } from 'express-validator'; -import { Container, Service, Inject } from 'typedi'; +import { check, oneOf, ValidationChain } from 'express-validator'; +import { Service, Inject } from 'typedi'; import { Voucher, Plan } from '@/system/models'; +import BaseController from '@/http/controllers/BaseController'; import VoucherService from '@/services/Payment/Voucher'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import PrettierMiddleware from '@/http/middleware/prettierMiddleware'; import { IVouchersFilter } from '@/interfaces'; @Service() -export default class VouchersController { +export default class VouchersController extends BaseController { @Inject() voucherService: VoucherService; @@ -24,14 +23,12 @@ export default class VouchersController { '/generate', this.generateVoucherSchema, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validatePlanExistance.bind(this)), asyncMiddleware(this.generateVoucher.bind(this)), ); router.post( '/disable/:voucherId', validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.validateVoucherExistance.bind(this)), asyncMiddleware(this.validateNotDisabledVoucher.bind(this)), asyncMiddleware(this.disableVoucher.bind(this)), @@ -40,18 +37,15 @@ export default class VouchersController { '/send', this.sendVoucherSchemaValidation, validateMiddleware, - PrettierMiddleware, asyncMiddleware(this.sendVoucher.bind(this)), ); router.delete( '/:voucherId', - PrettierMiddleware, asyncMiddleware(this.validateVoucherExistance.bind(this)), asyncMiddleware(this.deleteVoucher.bind(this)), ); router.get( '/', - PrettierMiddleware, asyncMiddleware(this.listVouchers.bind(this)), ); return router; @@ -106,7 +100,8 @@ export default class VouchersController { * @param {Function} next */ async validatePlanExistance(req: Request, res: Response, next: Function) { - const planId: number = req.body.planId || req.params.planId; + const body = this.matchedBodyData(req); + const planId: number = body.planId || req.params.planId; const foundPlan = await Plan.query().findById(planId); if (!foundPlan) { @@ -124,7 +119,9 @@ export default class VouchersController { * @param {Function} */ async validateVoucherExistance(req: Request, res: Response, next: Function) { - const voucherId = req.body.voucherId || req.params.voucherId; + const body = this.matchedBodyData(req); + + const voucherId = body.voucherId || req.params.voucherId; const foundVoucher = await Voucher.query().findById(voucherId); if (!foundVoucher) { @@ -160,7 +157,7 @@ export default class VouchersController { * @return {Response} */ async generateVoucher(req: Request, res: Response, next: Function) { - const { loop = 10, period, periodInterval, planId } = req.body; + const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req); try { await this.voucherService.generateVouchers( @@ -211,7 +208,7 @@ export default class VouchersController { * @return {Response} */ async sendVoucher(req: Request, res: Response) { - const { phoneNumber, email, period, periodInterval, planId } = req.body; + const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req); const voucher = await Voucher.query() .modify('filterActiveVoucher') diff --git a/server/src/http/controllers/Subscription/index.ts b/server/src/http/controllers/Subscription/index.ts index f744b5749..7e3b8941f 100644 --- a/server/src/http/controllers/Subscription/index.ts +++ b/server/src/http/controllers/Subscription/index.ts @@ -2,6 +2,7 @@ import { Router } from 'express' import { Container, Service } from 'typedi'; import JWTAuth from '@/http/middleware/jwtAuth'; import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser'; import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher'; @Service() @@ -13,6 +14,7 @@ export default class SubscriptionController { const router = Router(); router.use(JWTAuth); + router.use(AttachCurrentTenantUser); router.use(TenancyMiddleware); router.use('/voucher', Container.get(PaymentViaVoucherController).router()); diff --git a/server/src/http/index.js b/server/src/http/index.js index d8f6852bc..b3ea77c38 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -1,7 +1,18 @@ import express from 'express'; import { Container } from 'typedi'; + +// Middlewares +import JWTAuth from '@/http/middleware/jwtAuth'; +import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser'; +import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware'; +import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import EnsureTenantIsInitialized from '@/http/middleware/EnsureTenantIsInitialized'; +import SettingsMiddleware from '@/http/middleware/SettingsMiddleware'; + +// Routes import Authentication from '@/http/controllers/Authentication'; import InviteUsers from '@/http/controllers/InviteUsers'; +import Organization from '@/http/controllers/Organization'; import Users from '@/http/controllers/Users'; import Items from '@/http/controllers/Items'; import ItemCategories from '@/http/controllers/ItemCategories'; @@ -11,7 +22,7 @@ import Views from '@/http/controllers/Views'; import Accounting from '@/http/controllers/Accounting'; import FinancialStatements from '@/http/controllers/FinancialStatements'; import Expenses from '@/http/controllers/Expenses'; -import Options from '@/http/controllers/Options'; +import Settings from '@/http/controllers/Settings'; import Currencies from '@/http/controllers/Currencies'; import Customers from '@/http/controllers/Customers'; import Vendors from '@/http/controllers/Vendors'; @@ -20,19 +31,16 @@ import Purchases from '@/http/controllers/Purchases'; import Resources from './controllers/Resources'; import ExchangeRates from '@/http/controllers/ExchangeRates'; import Media from '@/http/controllers/Media'; -import JWTAuth from '@/http/middleware/jwtAuth'; -import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; import Ping from '@/http/controllers/Ping'; import Agendash from '@/http/controllers/Agendash'; import Subscription from '@/http/controllers/Subscription'; import VouchersController from '@/http/controllers/Subscription/Vouchers'; -import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjection'; -import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware'; export default (app) => { app.use('/api/auth', Container.get(Authentication).router()); - app.use('/api/invite', InviteUsers.router()); + app.use('/api/invite', Container.get(InviteUsers).router()); + app.use('/api/organization', Container.get(Organization).router()); app.use('/api/vouchers', Container.get(VouchersController).router()); app.use('/api/subscription', Container.get(Subscription).router()); app.use('/api/ping', Container.get(Ping).router()); @@ -40,20 +48,23 @@ export default (app) => { const dashboard = express.Router(); dashboard.use(JWTAuth); + dashboard.use(AttachCurrentTenantUser) dashboard.use(TenancyMiddleware); dashboard.use(SubscriptionMiddleware('main')); - - dashboard.use('/api/currencies', Currencies.router()); + dashboard.use(EnsureTenantIsInitialized); + dashboard.use(SettingsMiddleware); + dashboard.use('/api/users', Users.router()); + dashboard.use('/api/currencies', Currencies.router()); dashboard.use('/api/accounts', Accounts.router()); dashboard.use('/api/account_types', AccountTypes.router()); dashboard.use('/api/accounting', Accounting.router()); dashboard.use('/api/views', Views.router()); dashboard.use('/api/items', Container.get(Items).router()); - dashboard.use('/api/item_categories', Container.get(ItemCategories)); + dashboard.use('/api/item_categories', Container.get(ItemCategories).router()); dashboard.use('/api/expenses', Expenses.router()); dashboard.use('/api/financial_statements', FinancialStatements.router()); - dashboard.use('/api/options', Options.router()); + dashboard.use('/api/settings', Container.get(Settings).router()); dashboard.use('/api/sales', Sales.router()); dashboard.use('/api/customers', Customers.router()); dashboard.use('/api/vendors', Vendors.router()); diff --git a/server/src/http/middleware/AttachCurrentTenantUser.ts b/server/src/http/middleware/AttachCurrentTenantUser.ts new file mode 100644 index 000000000..981bc2aab --- /dev/null +++ b/server/src/http/middleware/AttachCurrentTenantUser.ts @@ -0,0 +1,29 @@ +import { Container } from 'typedi'; +import { SystemUser } from '@/system/models'; + +/** + * Attach user to req.currentUser + * @param {Request} req Express req Object + * @param {Response} res Express res Object + * @param {NextFunction} next Express next Function + */ +const attachCurrentUser = async (req: Request, res: Response, next: Function) => { + const Logger = Container.get('logger'); + + try { + Logger.info('[attach_user_middleware] finding system user by id.'); + const user = await SystemUser.query().findById(req.token.id); + + if (!user) { + Logger.info('[attach_user_middleware] the system user not found.'); + return res.boom.unauthorized(); + } + req.user = user; + return next(); + } catch (e) { + Logger.error('[attach_user_middleware] error attaching user to req: %o', e); + return next(e); + } +}; + +export default attachCurrentUser; diff --git a/server/src/http/middleware/EnsureTenantIsInitialized.ts b/server/src/http/middleware/EnsureTenantIsInitialized.ts new file mode 100644 index 000000000..086bfd8fc --- /dev/null +++ b/server/src/http/middleware/EnsureTenantIsInitialized.ts @@ -0,0 +1,17 @@ +import { Container } from 'typedi'; + +export default (req: Request, res: Response, next: Function) => { + const Logger = Container.get('logger'); + + if (!req.tenant) { + Logger.info('[ensure_tenant_intialized_middleware] no tenant model.'); + throw new Error('Should load this middleware after `TenancyMiddleware`.'); + } + if (!req.tenant.initialized) { + Logger.info('[ensure_tenant_intialized_middleware] tenant database not initalized.'); + return res.status(400).send({ + errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }], + }); + } + next(); +}; \ No newline at end of file diff --git a/server/src/http/middleware/LoggerMiddleware.ts b/server/src/http/middleware/LoggerMiddleware.ts new file mode 100644 index 000000000..e9688379f --- /dev/null +++ b/server/src/http/middleware/LoggerMiddleware.ts @@ -0,0 +1,8 @@ +import { NextFunction, Request } from 'express'; + +function loggerMiddleware(request: Request, response: Response, next: NextFunction) { + console.log(`${request.method} ${request.path}`); + next(); +} + +export default loggerMiddleware; diff --git a/server/src/http/middleware/SettingsMiddleware.ts b/server/src/http/middleware/SettingsMiddleware.ts new file mode 100644 index 000000000..250761eb9 --- /dev/null +++ b/server/src/http/middleware/SettingsMiddleware.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from 'express'; +import { Container } from 'typedi'; +import SettingsStore from '@/services/Settings/SettingsStore'; + +export default async (req: Request, res: Response, next: NextFunction) => { + const { tenantId } = req.user; + const { knex } = req; + + const Logger = Container.get('logger'); + const tenantContainer = Container.of(`tenant-${tenantId}`); + + if (tenantContainer && !tenantContainer.has('settings')) { + Logger.info('[settings_middleware] initialize settings store.'); + const settings = new SettingsStore(knex); + + Logger.info('[settings_middleware] load settings from storage or cache.'); + await settings.load(); + + tenantContainer.set('settings', settings); + } + Logger.info('[settings_middleware] get settings instance from container.'); + const settings = tenantContainer.get('settings'); + req.settings = settings; + + res.on('finish', async () => { + await settings.save(); + }); + next(); +} \ No newline at end of file diff --git a/server/src/http/middleware/TenancyMiddleware.js b/server/src/http/middleware/TenancyMiddleware.js index b68b03578..9c635fa53 100644 --- a/server/src/http/middleware/TenancyMiddleware.js +++ b/server/src/http/middleware/TenancyMiddleware.js @@ -1,34 +1,29 @@ -import fs from 'fs'; -import path from 'path'; -import TenantsManager from '@/system/TenantsManager'; -import TenantModel from '@/models/TenantModel'; import { Container } from 'typedi'; - -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; -} +import TenantsManager from '@/system/TenantsManager'; +import tenantModelsLoader from '@/loaders/tenantModels'; export default async (req, res, next) => { + const Logger = Container.get('logger'); const organizationId = req.headers['organization-id'] || req.query.organization; - const notFoundOrganization = () => res.boom.unauthorized( - 'Organization identication not found.', - { errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] }, - ); + const notFoundOrganization = () => { + Logger.info('[tenancy_middleware] organization id not found.'); + return res.boom.unauthorized( + 'Organization identication not found.', + { errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] }, + ); + } + // In case the given organization not found. if (!organizationId) { return notFoundOrganization(); } - const tenant = await TenantsManager.getTenant(organizationId); + const tenantsManager = Container.get(TenantsManager); + + Logger.info('[tenancy_middleware] trying get tenant by org. id from storage.'); + const tenant = await tenantsManager.getTenant(organizationId); + + Logger.info('[tenancy_middleware] initializing tenant knex instance.'); + const tenantKnex = tenantsManager.knexInstance(organizationId); // When the given organization id not found on the system storage. if (!tenant) { @@ -36,30 +31,22 @@ export default async (req, res, next) => { } // When user tenant not match the given organization id. if (tenant.id !== req.user.tenantId) { + Logger.info('[tenancy_middleware] authorized user not match org. tenant.'); return res.boom.unauthorized(); } - const knex = TenantsManager.knexInstance(organizationId); - const models = loadModelsFromDirectory(); - - TenantModel.knexBinded = knex; - - req.knex = knex; + const models = tenantModelsLoader(tenantKnex); + + req.knex = tenantKnex; req.organizationId = organizationId; req.tenant = tenant; - req.tenantId = tenant.id; - req.models = { - ...Object.values(models).reduce((acc, model) => { - if (typeof model.resource.default !== 'undefined' && - typeof model.resource.default.requestModel === 'function' && - model.resource.default.requestModel() && - model.name !== 'TenantModel') { - acc[model.name] = model.resource.default.bindKnex(knex); - } - return acc; - }, {}), - }; - Container.of(`tenant-${tenant.id}`).set('models', { - ...req.models, - }); + req.models = models; + + const tenantContainer = Container.of(`tenant-${tenant.id}`); + + tenantContainer.set('models', models); + tenantContainer.set('knex', tenantKnex); + tenantContainer.set('tenant', tenant); + Logger.info('[tenancy_middleware] tenant dependencies injected to container.'); + next(); -}; \ No newline at end of file +} diff --git a/server/src/http/middleware/TenantDependencyInjection.ts b/server/src/http/middleware/TenantDependencyInjection.ts deleted file mode 100644 index 37a725919..000000000 --- a/server/src/http/middleware/TenantDependencyInjection.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express'; -import { Container } from 'typedi'; - -export default async (req: Request, res: Response, next: Function) => { - const { organizationId, knex } = req; - - if (!organizationId || !knex) { - throw new Error('Should load `TenancyMiddleware` before this middleware.'); - } - Container.of(`tenant-${organizationId}`).set('knex', knex); - - next(); -}; \ No newline at end of file diff --git a/server/src/http/middleware/authorization.js b/server/src/http/middleware/authorization.js deleted file mode 100644 index 6d1acb7bc..000000000 --- a/server/src/http/middleware/authorization.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable consistent-return */ -const authorization = (resourceName) => (...permissions) => (req, res, next) => { - const { user } = req; - const onError = () => { - res.boom.unauthorized(); - }; - user.hasPermissions(resourceName, permissions) - .then((authorized) => { - if (!authorized) { - return onError(); - } - next(); - }).catch(onError); -}; - -export default authorization; diff --git a/server/src/http/middleware/jwtAuth.js b/server/src/http/middleware/jwtAuth.js index eb879bcb8..23188e334 100644 --- a/server/src/http/middleware/jwtAuth.js +++ b/server/src/http/middleware/jwtAuth.js @@ -1,31 +1,31 @@ -/* eslint-disable consistent-return */ +import { Container } from 'typedi'; import jwt from 'jsonwebtoken'; -import SystemUser from '@/system/models/SystemUser'; +import config from '@/../config/config'; const authMiddleware = (req, res, next) => { - const { JWT_SECRET_KEY } = process.env; + const Logger = Container.get('logger'); const token = req.headers['x-access-token'] || req.query.token; - const onError = () => { res.boom.unauthorized(); }; - + const onError = () => { + Logger.info('[auth_middleware] jwt verify error.'); + res.boom.unauthorized(); + }; + const onSuccess = (decoded) => { + req.token = decoded; + Logger.info('[auth_middleware] jwt verify success.'); + next(); + }; if (!token) { return onError(); } const verify = new Promise((resolve, reject) => { - jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => { + jwt.verify(token, config.jwtSecret, async (error, decoded) => { if (error) { reject(error); } else { - // eslint-disable-next-line no-underscore-dangle - req.user = await SystemUser.query().findById(decoded._id); - - if (!req.user) { - return onError(); - } resolve(decoded); } }); }); - - verify.then(() => { next(); }).catch(onError); + verify.then(onSuccess).catch(onError); }; export default authMiddleware; diff --git a/server/src/http/middleware/prettierMiddleware.ts b/server/src/http/middleware/prettierMiddleware.ts deleted file mode 100644 index 3daf6ee2c..000000000 --- a/server/src/http/middleware/prettierMiddleware.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Request, Response } from 'express'; -import { camelCase, snakeCase, mapKeys } from 'lodash'; - -/** - * create a middleware to change json format from snake case to camelcase in request - * then change back to snake case in response - * - */ -export default (req: Request, res: Response, next: Function) => { - /** - * camelize `req.body` - */ - if (req.body && typeof req.body === 'object') { - req.body = mapKeys(req.body, (value: any, key: string) => camelCase(key)); - } - - /** - * camelize `req.query` - */ - if (req.query && typeof req.query === 'object') { - req.query = mapKeys(req.query, (value: any, key: string) => camelCase(key)); - } - - /** - * wrap `res.json()` - */ - const sendJson = res.json; - - res.json = (data: any) => { - const mapped = mapKeys(data, (value: any, key: string) => snakeCase(key)); - return sendJson.call(res, mapped); - }; - return next(); -}; \ No newline at end of file diff --git a/server/src/interfaces/Metable.ts b/server/src/interfaces/Metable.ts new file mode 100644 index 000000000..b319cb3c6 --- /dev/null +++ b/server/src/interfaces/Metable.ts @@ -0,0 +1,28 @@ + + +export interface IMetadata { + key: string, + value: string|boolean|number, + group: string, + _markAsDeleted?: boolean, + _markAsInserted?: boolean, + _markAsUpdated?: boolean, +}; + +export interface IMetaQuery { + key: string, + group: string, +}; + +export interface IMetableStore { + find(query: string|IMetaQuery): IMetadata; + all(): IMetadata[]; + get(query: string|IMetaQuery, defaultValue: any): string|number|boolean; + remove(query: string|IMetaQuery): void; + removeAll(): void; + toArray(): IMetadata[]; +}; + +export interface IMetableStoreStorage { + save(): Promise; +} \ No newline at end of file diff --git a/server/src/interfaces/Options.ts b/server/src/interfaces/Options.ts new file mode 100644 index 000000000..51a4e7834 --- /dev/null +++ b/server/src/interfaces/Options.ts @@ -0,0 +1,11 @@ + + +export interface IOptionDTO { + key: string, + value: string|number, + group: string, +}; + +export interface IOptionsDTO { + options: IOptionDTO[], +}; \ No newline at end of file diff --git a/server/src/interfaces/User.ts b/server/src/interfaces/User.ts index 652305cc7..8e693f6fc 100644 --- a/server/src/interfaces/User.ts +++ b/server/src/interfaces/User.ts @@ -6,4 +6,8 @@ export interface ISystemUser { export interface ISystemUserDTO { +} + +export interface IInviteUserInput { + } \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 6eef4afee..c838b61f7 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -34,7 +34,18 @@ import { import { ISystemUser, ISystemUserDTO, + IInviteUserInput, } from './User'; +import { + IMetadata, + IMetaQuery, + IMetableStore, + IMetableStoreStorage, +} from './Metable'; +import { + IOptionDTO, + IOptionsDTO, +} from './Options'; export { IBillPaymentEntry, @@ -69,4 +80,13 @@ export { IRegisterDTO, ISystemUser, ISystemUserDTO, + IInviteUserInput, + + IMetadata, + IMetaQuery, + IMetableStore, + IMetableStoreStorage, + + IOptionDTO, + IOptionsDTO, }; \ No newline at end of file diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index 1049a6986..f844b25bc 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -25,15 +25,15 @@ export default class ComputeItemCostJob { const Logger = Container.get('logger'); const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data; - Logger.debug(`Compute item cost - started: ${job.attrs.data}`); + Logger.info(`Compute item cost - started: ${job.attrs.data}`); try { await InventoryService.computeItemCost(startingDate, itemId, costMethod); - Logger.debug(`Compute item cost - completed: ${job.attrs.data}`); + Logger.info(`Compute item cost - completed: ${job.attrs.data}`); done(); } catch(e) { console.log(e); - Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`); + Logger.info(`Compute item cost: ${job.attrs.data}, error: ${e}`); done(e); } } @@ -58,9 +58,8 @@ export default class ComputeItemCostJob { async onJobFinished() { const agenda = Container.get('agenda'); const startingDate = this.startingDate; - this.depends = Math.max(this.depends - 1, 0); - console.log(startingDate); + this.depends = Math.max(this.depends - 1, 0); if (this.depends === 0) { this.startingDate = null; diff --git a/server/src/jobs/MailNotificationSubscribeEnd.ts b/server/src/jobs/MailNotificationSubscribeEnd.ts index 1f5d9451d..be79266cd 100644 --- a/server/src/jobs/MailNotificationSubscribeEnd.ts +++ b/server/src/jobs/MailNotificationSubscribeEnd.ts @@ -3,7 +3,7 @@ import SubscriptionService from '@/services/Subscription/Subscription'; export default class MailNotificationSubscribeEnd { /** - * + * Job handler. * @param {Job} job - */ handler(job) { @@ -12,15 +12,15 @@ export default class MailNotificationSubscribeEnd { const subscriptionService = Container.get(SubscriptionService); const Logger = Container.get('logger'); - Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`); + Logger.info(`Send mail notification subscription end soon - started: ${job.attrs.data}`); try { subscriptionService.mailMessages.sendRemainingTrialPeriod( phoneNumber, remainingDays, ); - Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`); + Logger.info(`Send mail notification subscription end soon - finished: ${job.attrs.data}`); } catch(error) { - Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); + Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); done(e); } } diff --git a/server/src/jobs/MailNotificationTrialEnd.ts b/server/src/jobs/MailNotificationTrialEnd.ts index c2b62cffa..89162db5d 100644 --- a/server/src/jobs/MailNotificationTrialEnd.ts +++ b/server/src/jobs/MailNotificationTrialEnd.ts @@ -12,15 +12,15 @@ export default class MailNotificationTrialEnd { const subscriptionService = Container.get(SubscriptionService); const Logger = Container.get('logger'); - Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`); + Logger.info(`Send mail notification subscription end soon - started: ${job.attrs.data}`); try { subscriptionService.mailMessages.sendRemainingTrialPeriod( phoneNumber, remainingDays, ); - Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`); + Logger.info(`Send mail notification subscription end soon - finished: ${job.attrs.data}`); } catch(error) { - Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); + Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); done(e); } } diff --git a/server/src/jobs/ResetPasswordMail.ts b/server/src/jobs/ResetPasswordMail.ts index 49e5176aa..baf7076a4 100644 --- a/server/src/jobs/ResetPasswordMail.ts +++ b/server/src/jobs/ResetPasswordMail.ts @@ -1,44 +1,28 @@ -import fs from 'fs'; -import path from 'path'; -import Mustache from 'mustache'; -import { Container } from 'typedi'; +import { Container, Inject } from 'typedi'; +import AuthenticationService from '@/services/Authentication'; + +export default class WelcomeEmailJob { + @Inject() + authService: AuthenticationService; -export default class ResetPasswordMailJob { /** - * - * @param job - * @param done + * Handle send welcome mail job. + * @param {Job} job + * @param {Function} done */ - handler(job, done) { - const { user, token } = job.attrs.data; - + public async handler(job, done: Function): Promise { + const { email, organizationName, firstName } = job.attrs.data; const Logger = Container.get('logger'); - const Mail = Container.get('mail'); - const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html'); - const template = fs.readFileSync(filePath, 'utf8'); - const rendered = Mustache.render(template, { - url: `https://google.com/reset/${token}`, - first_name: user.firstName, - last_name: user.lastName, - // contact_us_email: config.contactUsMail, - }); - - const mailOptions = { - to: user.email, - from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, - subject: 'Bigcapital - Password Reset', - html: rendered, - }; - Mail.sendMail(mailOptions, (error) => { - if (error) { - Logger.info('[send_reset_password] failed send reset password mail', { error, user }); - done(error); - return; - } - Logger.info('[send_reset_password] user has been sent reset password email successfuly.', { user }); - done(); - }); - res.status(200).send({ email: passwordReset.email }); + Logger.info(`Send reset password mail - started: ${job.attrs.data}`); + + try { + await this.authService.mailMessages.sendResetPasswordMessage(); + Logger.info(`Send reset password mail - finished: ${job.attrs.data}`); + done() + } catch (error) { + Logger.info(`Send reset password mail - error: ${job.attrs.data}, error: ${error}`); + done(error); + } } -} \ No newline at end of file +} diff --git a/server/src/jobs/SMSNotificationSubscribeEnd.ts b/server/src/jobs/SMSNotificationSubscribeEnd.ts index d9302a955..d203c1d6b 100644 --- a/server/src/jobs/SMSNotificationSubscribeEnd.ts +++ b/server/src/jobs/SMSNotificationSubscribeEnd.ts @@ -13,15 +13,15 @@ export default class SMSNotificationSubscribeEnd { const subscriptionService = Container.get(SubscriptionService); const Logger = Container.get('logger'); - Logger.debug(`Send SMS notification subscription end soon - started: ${job.attrs.data}`); + Logger.info(`Send SMS notification subscription end soon - started: ${job.attrs.data}`); try { subscriptionService.smsMessages.sendRemainingSubscriptionPeriod( phoneNumber, remainingDays, ); - Logger.debug(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`); + Logger.info(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`); } catch(error) { - Logger.error(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); + Logger.info(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); done(e); } } diff --git a/server/src/jobs/SMSNotificationTrialEnd.ts b/server/src/jobs/SMSNotificationTrialEnd.ts index 69ca54b39..a3e5c5420 100644 --- a/server/src/jobs/SMSNotificationTrialEnd.ts +++ b/server/src/jobs/SMSNotificationTrialEnd.ts @@ -13,15 +13,15 @@ export default class SMSNotificationTrialEnd { const subscriptionService = Container.get(SubscriptionService); const Logger = Container.get('logger'); - Logger.debug(`Send notification subscription end soon - started: ${job.attrs.data}`); + Logger.info(`Send notification subscription end soon - started: ${job.attrs.data}`); try { subscriptionService.smsMessages.sendRemainingTrialPeriod( phoneNumber, remainingDays, ); - Logger.debug(`Send notification subscription end soon - finished: ${job.attrs.data}`); + Logger.info(`Send notification subscription end soon - finished: ${job.attrs.data}`); } catch(error) { - Logger.error(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); + Logger.info(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`); done(e); } } diff --git a/server/src/jobs/SendVoucherPhone.ts b/server/src/jobs/SendVoucherPhone.ts index cebac228a..ec302c0ed 100644 --- a/server/src/jobs/SendVoucherPhone.ts +++ b/server/src/jobs/SendVoucherPhone.ts @@ -3,9 +3,10 @@ import VoucherService from '@/services/Payment/Voucher'; export default class SendVoucherViaPhoneJob { public async handler(job, done: Function): Promise { + const { phoneNumber, voucherCode } = job.attrs.data; + const Logger = Container.get('logger'); const voucherService = Container.get(VoucherService); - const { phoneNumber, voucherCode } = job.attrs.data; Logger.debug(`Send voucher via phone number - started: ${job.attrs.data}`); diff --git a/server/src/jobs/UserInviteMail.ts b/server/src/jobs/UserInviteMail.ts index e379a8097..f97703b78 100644 --- a/server/src/jobs/UserInviteMail.ts +++ b/server/src/jobs/UserInviteMail.ts @@ -1,8 +1,28 @@ - +import { Container, Inject } from 'typedi'; +import InviteUserService from '@/services/InviteUsers'; export default class UserInviteMailJob { + @Inject() + inviteUsersService: InviteUserService; - handler(job, done) { - + /** + * Handle invite user job. + * @param {Job} job + * @param {Function} done + */ + public async handler(job, done: Function): Promise { + const { email, organizationName, firstName } = job.attrs.data; + const Logger = Container.get('logger'); + + Logger.info(`Send invite user mail - started: ${job.attrs.data}`); + + try { + await this.inviteUsersService.mailMessages.sendInviteMail(); + Logger.info(`Send invite user mail - finished: ${job.attrs.data}`); + done() + } catch (error) { + Logger.info(`Send invite user mail - error: ${job.attrs.data}, error: ${error}`); + done(error); + } } -} \ No newline at end of file +} diff --git a/server/src/jobs/WelcomeSMS.ts b/server/src/jobs/WelcomeSMS.ts new file mode 100644 index 000000000..059af2fba --- /dev/null +++ b/server/src/jobs/WelcomeSMS.ts @@ -0,0 +1,28 @@ +import { Container, Inject } from 'typedi'; +import AuthenticationService from '@/services/Authentication'; + +export default class WelcomeSMSJob { + @Inject() + authService: AuthenticationService; + + /** + * Handle send welcome mail job. + * @param {Job} job + * @param {Function} done + */ + public async handler(job, done: Function): Promise { + const { email, organizationName, firstName } = job.attrs.data; + const Logger = Container.get('logger'); + + Logger.info(`Send welcome SMS message - started: ${job.attrs.data}`); + + try { + await this.authService.smsMessages.sendWelcomeMessage(); + Logger.info(`Send welcome SMS message - finished: ${job.attrs.data}`); + done() + } catch (error) { + Logger.info(`Send welcome SMS message - error: ${job.attrs.data}, error: ${error}`); + done(error); + } + } +} diff --git a/server/src/jobs/welcomeEmail.ts b/server/src/jobs/welcomeEmail.ts index 16d160e54..c257f6cae 100644 --- a/server/src/jobs/welcomeEmail.ts +++ b/server/src/jobs/welcomeEmail.ts @@ -1,38 +1,28 @@ -import fs from 'fs'; -import Mustache from 'mustache'; -import path from 'path'; -import { Container } from 'typedi'; +import { Container, Inject } from 'typedi'; +import AuthenticationService from '@/services/Authentication'; export default class WelcomeEmailJob { + @Inject() + authService: AuthenticationService; + /** - * + * Handle send welcome mail job. * @param {Job} job * @param {Function} done */ public async handler(job, done: Function): Promise { const { email, organizationName, firstName } = job.attrs.data; const Logger = Container.get('logger'); - const Mail = Container.get('mail'); - const filePath = path.join(global.rootPath, 'views/mail/Welcome.html'); - const template = fs.readFileSync(filePath, 'utf8'); - const rendered = Mustache.render(template, { - email, organizationName, firstName, - }); - const mailOptions = { - to: email, - from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, - subject: 'Welcome to Bigcapital', - html: rendered, - }; - Mail.sendMail(mailOptions, (error) => { - if (error) { - Logger.error('Failed send welcome mail', { error, form }); - done(error); - return; - } - Logger.info('User has been sent welcome email successfuly.', { form }); - done(); - }); + Logger.info(`Send welcome mail message - started: ${job.attrs.data}`); + + try { + await this.authService.mailMessages.sendWelcomeMessage(); + Logger.info(`Send welcome mail message - finished: ${job.attrs.data}`); + done() + } catch (error) { + Logger.info(`Send welcome mail message - error: ${job.attrs.data}, error: ${error}`); + done(error); + } } } diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts index 23606443b..ff91368b0 100644 --- a/server/src/jobs/writeInvoicesJEntries.ts +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -7,15 +7,15 @@ export default class WriteInvoicesJournalEntries { const Logger = Container.get('logger'); const { startingDate } = job.attrs.data; - Logger.debug(`Write sales invoices journal entries - started: ${job.attrs.data}`); + Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`); try { await SalesInvoicesCost.writeJournalEntries(startingDate, true); - Logger.debug(`Write sales invoices journal entries - completed: ${job.attrs.data}`); + Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`); done(); } catch(e) { console.log(e); - Logger.error(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`); + Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`); done(e); } } diff --git a/server/src/lib/Metable/MetableCollection.js b/server/src/lib/Metable/MetableCollection.js deleted file mode 100644 index 947cca6bd..000000000 --- a/server/src/lib/Metable/MetableCollection.js +++ /dev/null @@ -1,266 +0,0 @@ - -export default class MetableCollection { - /** - * Constructor method. - */ - constructor() { - this.metadata = []; - this.KEY_COLUMN = 'key'; - this.VALUE_COLUMN = 'value'; - this.TYPE_COLUMN = 'type'; - this.model = null; - this.extraColumns = []; - - this.extraQuery = (query, meta) => { - query.where('key', meta[this.KEY_COLUMN]); - }; - } - - /** - * Set model of this metadata collection. - * @param {Object} model - - */ - setModel(model) { - this.model = model; - } - - /** - * Sets a extra columns. - * @param {Array} columns - - */ - setExtraColumns(columns) { - this.extraColumns = columns; - } - - /** - * Find the given metadata key. - * @param {String} key - - * @return {object} - Metadata object. - */ - findMeta(payload) { - const { key, extraColumns } = this.parsePayload(payload); - - return this.allMetadata().find((meta) => { - const isSameKey = meta.key === key; - const sameExtraColumns = this.extraColumns.some((extraColumn) => { - return !extraColumns || (extraColumns[extraColumn] === meta[extraColumn]); - }); - return isSameKey && sameExtraColumns; - }); - } - - /** - * Retrieve all metadata. - */ - allMetadata() { - return this.metadata.filter((meta) => !meta.markAsDeleted); - } - - /** - * Retrieve metadata of the given key. - * @param {String} key - - * @param {Mixied} defaultValue - - */ - getMeta(payload, defaultValue) { - const metadata = this.findMeta(payload); - return metadata ? metadata.value : defaultValue || false; - } - - /** - * Markes the metadata to should be deleted. - * @param {String} key - - */ - removeMeta(key) { - const metadata = this.findMeta(key); - - if (metadata) { - metadata.markAsDeleted = true; - } - } - - /** - * Remove all meta data of the given group. - * @param {*} group - */ - removeAllMeta(group = 'default') { - this.metadata = this.metadata.map((meta) => ({ - ...meta, - markAsDeleted: true, - })); - } - - setExtraQuery(callback) { - this.extraQuery = callback; - } - - /** - * Set the meta data to the stack. - * @param {String} key - - * @param {String} value - - */ - setMeta(payload, ...args) { - if (Array.isArray(key)) { - const metadata = key; - - metadata.forEach((meta) => { - this.setMeta(meta.key, meta.value); - }); - return; - } - const { key, value, ...extraColumns } = this.parsePayload(payload, args[0]); - const metadata = this.findMeta(payload); - - if (metadata) { - metadata.value = value; - metadata.markAsUpdated = true; - } else { - this.metadata.push({ - value, key, ...extraColumns, markAsInserted: true, - }); - } - } - - parsePayload(payload, value) { - return typeof payload !== 'object' ? { key: payload, value } : { ...payload }; - } - - /** - * Saved the modified/deleted and inserted metadata. - */ - async saveMeta() { - const inserted = this.metadata.filter((m) => (m.markAsInserted === true)); - const updated = this.metadata.filter((m) => (m.markAsUpdated === true)); - const deleted = this.metadata.filter((m) => (m.markAsDeleted === true)); - const opers = []; - - if (deleted.length > 0) { - deleted.forEach((meta) => { - const deleteOper = this.model.query().onBuild((query, result) => { - this.extraQuery(query, meta); - return result; - }).delete(); - opers.push(deleteOper); - }); - } - inserted.forEach((meta) => { - const insertOper = this.model.query().insert({ - [this.KEY_COLUMN]: meta.key, - [this.VALUE_COLUMN]: meta.value, - ...this.extraColumns.reduce((obj, column) => { - if (typeof meta[column] !== 'undefined') { - obj[column] = meta[column]; - } - return obj; - }, {}), - }); - opers.push(insertOper); - }); - updated.forEach((meta) => { - const updateOper = this.model.query().onBuild((query) => { - this.extraQuery(query, meta); - }).patch({ - [this.VALUE_COLUMN]: meta.value, - }); - opers.push(updateOper); - }); - await Promise.all(opers); - } - - /** - * Loads the metadata from the storage. - * @param {String|Array} key - - * @param {Boolean} force - - */ - async load() { - const metadata = await this.query(); - - const metadataArray = this.mapMetadataCollection(metadata); - metadataArray.forEach((meta) => { - this.metadata.push(meta); - }); - } - - /** - * Format the metadata before saving to the database. - * @param {String|Number|Boolean} value - - * @param {String} valueType - - * @return {String|Number|Boolean} - - */ - static formatMetaValue(value, valueType) { - let parsedValue; - - switch (valueType) { - case 'number': - parsedValue = `${value}`; - break; - case 'boolean': - parsedValue = value ? '1' : '0'; - break; - case 'json': - parsedValue = JSON.stringify(parsedValue); - break; - default: - parsedValue = value; - break; - } - return parsedValue; - } - - /** - * Mapping and parse metadata to collection entries. - * @param {Meta} attr - - * @param {String} parseType - - */ - mapMetadata(attr, parseType = 'parse') { - return { - key: attr[this.KEY_COLUMN], - value: (parseType === 'parse') - ? MetableCollection.parseMetaValue( - attr[this.VALUE_COLUMN], - this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false, - ) - : MetableCollection.formatMetaValue( - attr[this.VALUE_COLUMN], - this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false, - ), - ...this.extraColumns.map((extraCol) => ({ - [extraCol]: attr[extraCol] || null, - })), - }; - } - - /** - * Parse the metadata to the collection. - * @param {Array} collection - - */ - mapMetadataToCollection(metadata, parseType = 'parse') { - return metadata.map((model) => this.mapMetadataToCollection(model, parseType)); - } - - /** - * Load metadata to the metable collection. - * @param {Array} meta - - */ - from(meta) { - if (Array.isArray(meta)) { - meta.forEach((m) => { this.from(m); }); - return; - } - this.metadata.push(meta); - } - - toArray() { - return this.metadata; - } - - /** - * Static method to load metadata to the collection. - * @param {Array} meta - */ - static from(meta) { - const collection = new MetableCollection(); - collection.from(meta); - - return collection; - } -} diff --git a/server/src/lib/Metable/MetableStore.ts b/server/src/lib/Metable/MetableStore.ts new file mode 100644 index 000000000..b79dda503 --- /dev/null +++ b/server/src/lib/Metable/MetableStore.ts @@ -0,0 +1,204 @@ +import { Model } from 'objection'; +import { omit, isEmpty } from 'lodash'; +import { + IMetadata, + IMetaQuery, + IMetableStore, +} from '@/interfaces'; +import { itemsStartWith } from '@/utils'; + +export default class MetableStore implements IMetableStore{ + metadata: IMetadata[]; + model: Model; + extraColumns: string[]; + + /** + * Constructor method. + */ + constructor() { + this.metadata = []; + this.model = null; + this.extraColumns = []; + } + + /** + * Sets a extra columns. + * @param {Array} columns - + */ + setExtraColumns(columns: string[]): void { + this.extraColumns = columns; + } + + /** + * Find the given metadata key. + * @param {string|IMetaQuery} query - + * @returns {IMetadata} - Metadata object. + */ + find(query: string|IMetaQuery): IMetadata { + const { key, value, ...extraColumns } = this.parseQuery(query); + + return this.metadata.find((meta: IMetadata) => { + const isSameKey = meta.key === key; + const sameExtraColumns = this.extraColumns + .some((extraColumn: string) => extraColumns[extraColumn] === meta[extraColumn]); + + const isSameExtraColumns = (sameExtraColumns || isEmpty(extraColumns)); + + return isSameKey && isSameExtraColumns; + }); + } + + /** + * Retrieve all metadata. + * @returns {IMetadata[]} + */ + all(): IMetadata[] { + return this.metadata + .filter((meta: IMetadata) => !meta._markAsDeleted) + .map((meta: IMetadata) => omit( + meta, + itemsStartWith(Object.keys(meta), '_') + )); + } + + /** + * Retrieve metadata of the given key. + * @param {String} key - + * @param {Mixied} defaultValue - + */ + get(query: string|IMetaQuery, defaultValue: any): any|false { + const metadata = this.find(query); + return metadata ? metadata.value : defaultValue || false; + } + + /** + * Markes the metadata to should be deleted. + * @param {String} key - + */ + remove(query: string|IMetaQuery): void { + const metadata: IMetadata = this.find(query); + + if (metadata) { + metadata._markAsDeleted = true; + } + } + + /** + * Remove all meta data of the given group. + * @param {string} group + */ + removeAll(group: string = 'default'): void { + this.metadata = this.metadata.map((meta) => ({ + ...meta, + _markAsDeleted: true, + })); + } + + /** + * Set the meta data to the stack. + * @param {String} key - + * @param {String} value - + */ + set(query: IMetaQuery|IMetadata[]|string, metaValue?: any): void { + if (Array.isArray(query)) { + const metadata = query; + + metadata.forEach((meta: IMetadata) => { + this.set(meta.key, meta.value); + }); + return; + } + const { key, value, ...extraColumns } = this.parseQuery(query); + const metadata = this.find(query); + const newValue = metaValue || value; + + if (metadata) { + metadata.value = newValue; + metadata._markAsUpdated = true; + } else { + this.metadata.push({ + value: newValue, + key, + ...extraColumns, + _markAsInserted: true, + }); + } + } + + /** + * Parses query query. + * @param query + * @param value + */ + parseQuery(query: string|IMetaQuery): IMetaQuery { + return typeof query !== 'object' ? { key: query } : { ...query }; + } + + /** + * Format the metadata before saving to the database. + * @param {string|number|boolean} value - + * @param {string} valueType - + * @return {string|number|boolean} - + */ + static formatMetaValue( + value: string|boolean|number, + valueType: string + ) : string|number|boolean { + let parsedValue; + + switch (valueType) { + case 'number': + parsedValue = `${value}`; + break; + case 'boolean': + parsedValue = value ? '1' : '0'; + break; + case 'json': + parsedValue = JSON.stringify(parsedValue); + break; + default: + parsedValue = value; + break; + } + return parsedValue; + } + + /** + * Parse the metadata to the collection. + * @param {Array} collection - + */ + mapMetadataToCollection(metadata: IMetadata[], parseType: string = 'parse') { + return metadata.map((model) => this.mapMetadataToCollection(model, parseType)); + } + + /** + * Load metadata to the metable collection. + * @param {Array} meta - + */ + from(meta: []) { + if (Array.isArray(meta)) { + meta.forEach((m) => { this.from(m); }); + return; + } + this.metadata.push(meta); + } + + /** + * + * @returns {array} + */ + toArray(): IMetadata[] { + return this.metadata; + } + + /** + * Static method to load metadata to the collection. + * @param {Array} meta + */ + static from(meta) { + const collection = new MetableCollection(); + collection.from(meta); + + return collection; + } +} diff --git a/server/src/lib/Metable/MetableStoreDB.ts b/server/src/lib/Metable/MetableStoreDB.ts new file mode 100644 index 000000000..5e7dba2a5 --- /dev/null +++ b/server/src/lib/Metable/MetableStoreDB.ts @@ -0,0 +1,213 @@ +import { Model } from 'objection'; +import { + IMetadata, + IMetableStoreStorage, +} from '@/interfaces'; +import MetableStore from './MetableStore'; + +export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{ + model: Model; + KEY_COLUMN: string; + VALUE_COLUMN: string; + TYPE_COLUMN: string; + extraQuery: Function; + loaded: Boolean; + + /** + * Constructor method. + */ + constructor() { + super(); + + this.loaded = false; + this.KEY_COLUMN = 'key'; + this.VALUE_COLUMN = 'value'; + this.TYPE_COLUMN = 'type'; + this.model = null; + + this.extraQuery = (query, meta) => { + query.where('key', meta[this.KEY_COLUMN]); + }; + } + + /** + * Set model of this metadata collection. + * @param {Object} model - + */ + setModel(model: Model) { + this.model = model; + } + + /** + * Sets a extra query callback. + * @param callback + */ + setExtraQuery(callback) { + this.extraQuery = callback; + } + + /** + * Saves the modified, deleted and insert metadata. + */ + save() { + this.validateStoreIsLoaded(); + + return Promise.all([ + this.saveUpdated(this.metadata), + this.saveDeleted(this.metadata), + this.saveInserted(this.metadata), + ]); + } + + /** + * Saves the updated metadata. + * @param {IMetadata[]} metadata - + * @returns {Promise} + */ + saveUpdated(metadata: IMetadata[]) { + const updated = metadata.filter((m) => (m._markAsUpdated === true)); + const opers = []; + + updated.forEach((meta) => { + const updateOper = this.model.query().onBuild((query) => { + this.extraQuery(query, meta); + }).patch({ + [this.VALUE_COLUMN]: meta.value, + }).then(() => { + meta._markAsUpdated = false; + }); + opers.push(updateOper); + }); + return Promise.all(opers); + } + + /** + * Saves the deleted metadata. + * @param {IMetadata[]} metadata - + * @returns {Promise} + */ + saveDeleted(metadata: IMetadata[]) { + const deleted = metadata.filter((m: IMetadata) => (m._markAsDeleted === true)); + const opers: Promise = []; + + if (deleted.length > 0) { + deleted.forEach((meta) => { + const deleteOper = this.model.query().onBuild((query) => { + this.extraQuery(query, meta); + }).delete().then(() => { + meta._markAsDeleted = false; + }); + opers.push(deleteOper); + }); + } + return Promise.all(opers); + } + + /** + * Saves the inserted metadata. + * @param {IMetadata[]} metadata - + * @returns {Promise} + */ + saveInserted(metadata: IMetadata[]) { + const inserted = metadata.filter((m: IMetadata) => (m._markAsInserted === true)); + const opers: Promise = []; + + inserted.forEach((meta) => { + const insertData = { + [this.KEY_COLUMN]: meta.key, + [this.VALUE_COLUMN]: meta.value, + + ...this.extraColumns.reduce((obj, column) => { + if (typeof meta[column] !== 'undefined') { + obj[column] = meta[column]; + } + return obj; + }, {}), + }; + const insertOper = this.model.query() + .insert(insertData) + .then(() => { + meta._markAsInserted = false; + }); + opers.push(insertOper); + }); + return Promise.all(opers); + } + + /** + * Loads the metadata from the storage. + * @param {String|Array} key - + * @param {Boolean} force - + */ + async load() { + const metadata = await this.model.query(); + const mappedMetadata = this.mapMetadataCollection(metadata); + + mappedMetadata.forEach((meta: IMetadata) => { + this.metadata.push(meta); + }); + this.loaded = true; + } + + /** + * Format the metadata before saving to the database. + * @param {String|Number|Boolean} value - + * @param {String} valueType - + * @return {String|Number|Boolean} - + */ + static formatMetaValue(value: string|number|boolean, valueType: striung|false) { + let parsedValue: string|number|boolean; + + switch (valueType) { + case 'number': + parsedValue = `${value}`; + break; + case 'boolean': + parsedValue = value ? '1' : '0'; + break; + case 'json': + parsedValue = JSON.stringify(parsedValue); + break; + default: + parsedValue = value; + break; + } + return parsedValue; + } + + /** + * Mapping and parse metadata to collection entries. + * @param {Meta} attr - + * @param {String} parseType - + */ + mapMetadata(metadata: IMetadata) { + return { + key: metadata[this.KEY_COLUMN], + value: MetableDBStore.formatMetaValue( + metadata[this.VALUE_COLUMN], + this.TYPE_COLUMN ? metadata[this.TYPE_COLUMN] : false, + ), + ...this.extraColumns.reduce((obj, extraCol: string) => { + obj[extraCol] = metadata[extraCol] || null; + return obj; + }, {}), + }; + } + + /** + * Parse the metadata to the collection. + * @param {Array} collection - + */ + mapMetadataCollection(metadata: IMetadata[]) { + return metadata.map((model) => this.mapMetadata(model)); + } + + /** + * Throw error in case the store is not loaded yet. + */ + private validateStoreIsLoaded() { + if (!this.loaded) { + throw new Error('You could not save the store before loaded from the storage.'); + } + } +} diff --git a/server/src/loaders/dbManager.ts b/server/src/loaders/dbManager.ts new file mode 100644 index 000000000..9a002432e --- /dev/null +++ b/server/src/loaders/dbManager.ts @@ -0,0 +1,14 @@ +import knexManager from 'knex-db-manager'; +import knexfile from '@/../config/systemKnexfile'; +import config from '@/../config/config'; + +const knexConfig = knexfile[process.env.NODE_ENV]; + +export default () => knexManager.databaseManagerFactory({ + knex: knexConfig, + dbManager: { + collate: [], + superUser: config.manager.superUser, + superPassword: config.manager.superPassword, + }, +}); \ No newline at end of file diff --git a/server/src/loaders/dependencyInjector.ts b/server/src/loaders/dependencyInjector.ts index 56acce66a..891f0aed4 100644 --- a/server/src/loaders/dependencyInjector.ts +++ b/server/src/loaders/dependencyInjector.ts @@ -1,13 +1,15 @@ import { Container } from 'typedi'; -import LoggerInstance from '@/services/Logger'; +import LoggerInstance from '@/loaders/Logger'; import agendaFactory from '@/loaders/agenda'; import SmsClientLoader from '@/loaders/smsClient'; import mailInstance from '@/loaders/mail'; +import dbManagerFactory from '@/loaders/dbManager'; export default ({ mongoConnection, knex }) => { try { const agendaInstance = agendaFactory({ mongoConnection }); const smsClientInstance = SmsClientLoader(); + const dbManager = dbManagerFactory(); Container.set('agenda', agendaInstance); LoggerInstance.info('Agenda has been injected into container'); @@ -24,6 +26,9 @@ export default ({ mongoConnection, knex }) => { Container.set('mail', mailInstance); LoggerInstance.info('Mail instance has been injected into container'); + Container.set('dbManager', dbManager); + LoggerInstance.info('Database manager has been injected into container.'); + return { agenda: agendaInstance }; } catch (e) { LoggerInstance.error('Error on dependency injector loader: %o', e); diff --git a/server/src/loaders/index.ts b/server/src/loaders/index.ts index 2edf4305f..f34bd3583 100644 --- a/server/src/loaders/index.ts +++ b/server/src/loaders/index.ts @@ -1,4 +1,4 @@ -import Logger from '@/services/Logger'; +import Logger from '@/loaders/Logger'; import mongooseLoader from '@/loaders/mongoose'; import jobsLoader from '@/loaders/jobs'; import expressLoader from '@/loaders/express'; diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index bb72e93ee..4909dae85 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -1,7 +1,8 @@ import Agenda from 'agenda'; -import WelcomeEmailJob from '@/Jobs/welcomeEmail'; -import ResetPasswordMailJob from '@/Jobs/ResetPasswordMail'; -import ComputeItemCost from '@/Jobs/ComputeItemCost'; +import WelcomeEmailJob from '@/jobs/WelcomeEmail'; +import WelcomeSMSJob from '@/jobs/WelcomeSMS'; +import ResetPasswordMailJob from '@/jobs/ResetPasswordMail'; +import ComputeItemCost from '@/jobs/ComputeItemCost'; import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries'; import SendVoucherViaPhoneJob from '@/jobs/SendVoucherPhone'; import SendVoucherViaEmailJob from '@/jobs/SendVoucherEmail'; @@ -12,16 +13,24 @@ import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd'; import UserInviteMailJob from '@/jobs/UserInviteMail'; export default ({ agenda }: { agenda: Agenda }) => { + // Welcome mail and SMS message. agenda.define( 'welcome-email', { priority: 'high' }, new WelcomeEmailJob().handler, ); + agenda.define( + 'welcome-sms', + { priority: 'high' }, + new WelcomeSMSJob().handler + ); + // Reset password mail. agenda.define( 'reset-password-mail', { priority: 'high' }, new ResetPasswordMailJob().handler, ); + // User invite mail. agenda.define( 'user-invite-mail', { priority: 'high' }, diff --git a/server/src/services/Logger/index.js b/server/src/loaders/logger.ts similarity index 100% rename from server/src/services/Logger/index.js rename to server/src/loaders/logger.ts diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts new file mode 100644 index 000000000..33354921e --- /dev/null +++ b/server/src/loaders/tenantModels.ts @@ -0,0 +1,70 @@ +import { mapValues } from 'lodash'; + +import Account from '@/models/Account'; +import AccountBalance from '@/models/AccountBalance'; +import AccountTransaction from '@/models/AccountTransaction'; +import AccountType from '@/models/AccountType'; +import Bill from '@/models/Bill'; +import BillPayment from '@/models/BillPayment'; +import BillPaymentEntry from '@/models/BillPaymentEntry'; +import Currency from '@/models/Currency'; +import Customer from '@/models/Customer'; +import Vendor from '@/models/Vendor'; +import ExchangeRate from '@/models/ExchangeRate'; +import Expense from '@/models/Expense'; +import ExpenseCategory from '@/models/ExpenseCategory'; +import View from '@/models/View'; +import ViewRole from '@/models/ViewRole'; +import ViewColumn from '@/models/ViewColumn'; +import Setting from '@/models/Setting'; +import SaleInvoice from '@/models/SaleInvoice'; +import SaleInvoiceEntry from '@/models/SaleInvoiceEntry'; +import SaleReceipt from '@/models/SaleReceipt'; +import SaleReceiptEntry from '@/models/SaleReceiptEntry'; +import SaleEstimate from '@/models/SaleEstimate'; +import SaleEstimateEntry from '@/models/SaleEstimateEntry'; +import PaymentReceive from '@/models/PaymentReceive'; +import PaymentReceiveEntry from '@/models/PaymentReceiveEntry'; +import Option from '@/models/Option'; +import Resource from '@/models/Resource'; +import InventoryCostLotTracker from '@/models/InventoryCostLotTracker'; +import InventoryTransaction from '@/models/InventoryTransaction'; +import ResourceField from '@/models/ResourceField'; +import ResourceFieldMetadata from '@/models/ResourceFieldMetadata'; + +export default (knex) => { + const models = { + Option, + Account, + AccountBalance, + AccountTransaction, + AccountType, + Bill, + BillPayment, + BillPaymentEntry, + Currency, + Customer, + Vendor, + ExchangeRate, + Expense, + ExpenseCategory, + View, + ViewRole, + ViewColumn, + Setting, + SaleInvoice, + SaleInvoiceEntry, + SaleReceipt, + SaleReceiptEntry, + SaleEstimate, + SaleEstimateEntry, + PaymentReceive, + PaymentReceiveEntry, + Resource, + InventoryTransaction, + InventoryCostLotTracker, + ResourceField, + ResourceFieldMetadata, + }; + return mapValues(models, (model) => model.bindKnex(knex)); +} \ No newline at end of file diff --git a/server/src/models/Budget.js b/server/src/models/Budget.js deleted file mode 100644 index a4486a3ff..000000000 --- a/server/src/models/Budget.js +++ /dev/null @@ -1,60 +0,0 @@ -import TenantModel from '@/models/Model'; - -export default class Budget extends TenantModel { - /** - * Table name - */ - static get tableName() { - return 'budgets'; - } - - static get virtualAttributes() { - return ['rangeBy', 'rangeIncrement']; - } - - /** - * Model modifiers. - */ - static get modifiers() { - return { - filterByYear(query, year) { - query.where('year', year); - }, - filterByIncomeStatement(query) { - query.where('account_types', 'income_statement'); - }, - filterByProfitLoss(query) { - query.where('accounts_types', 'profit_loss'); - }, - }; - } - - get rangeBy() { - switch (this.period) { - case 'half-year': - case 'quarter': - return 'month'; - default: - return this.period; - } - } - - get rangeIncrement() { - switch (this.period) { - case 'half-year': - return 6; - case 'quarter': - return 3; - default: - return 1; - } - } - - get rangeOffset() { - switch (this.period) { - case 'half-year': return 5; - case 'quarter': return 2; - default: return 0; - } - } -} diff --git a/server/src/models/BudgetEntry.js b/server/src/models/BudgetEntry.js deleted file mode 100644 index 6774666c2..000000000 --- a/server/src/models/BudgetEntry.js +++ /dev/null @@ -1,10 +0,0 @@ -import TenantModel from '@/models/TenantModel'; - -export default class Budget extends TenantModel { - /** - * Table name - */ - static get tableName() { - return 'budget_entries'; - } -} diff --git a/server/src/models/Option.js b/server/src/models/Option.js index dedd921c8..ebe39a0e9 100644 --- a/server/src/models/Option.js +++ b/server/src/models/Option.js @@ -1,5 +1,4 @@ import TenantModel from '@/models/TenantModel'; -import MetableCollection from '@/lib/Metable/MetableCollection'; import definedOptions from '@/data/options'; @@ -11,27 +10,6 @@ export default class Option extends TenantModel { return 'options'; } - /** - * Override the model query. - * @param {...any} args - - */ - static query(...args) { - return super.query(...args).runAfter((result) => { - if (result instanceof MetableCollection) { - result.setModel(this.tenant()); - result.setExtraColumns(['group']); - } - return result; - }); - } - - /** - * Model collection. - */ - static get collection() { - return MetableCollection; - } - /** * Validates the given options is defined or either not. * @param {Array} options diff --git a/server/src/models/Permission.js b/server/src/models/Permission.js deleted file mode 100644 index c8d8dfd4c..000000000 --- a/server/src/models/Permission.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Model } from 'objection'; -import path from 'path'; -import TenantModel from '@/models/TenantModel'; - -export default class Permission extends TenantModel { - /** - * Table name of Role model. - * @type {String} - */ - static get tableName() { - return 'permissions'; - } - - /** - * 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').bindKnex(this.knexBinded), - // join: { - // from: 'permissions.role_id', - // to: 'roles.id', - // }, - // }, - - // resource: { - // relation: Model.BelongsToOneRelation, - // modelBase: path.join(__dirname, 'Resource'), - // join: { - // from: 'permissions.', - // to: '', - // } - // } - }; - } -} diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js index 2112473e4..a913c8778 100644 --- a/server/src/models/Resource.js +++ b/server/src/models/Resource.js @@ -31,7 +31,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) { static get relationMappings() { const View = require('@/models/View'); const ResourceField = require('@/models/ResourceField'); - const Permission = require('@/models/Permission'); return { /** @@ -57,22 +56,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) { to: 'resource_fields.resourceId', }, }, - - /** - * Resource model may has many associated permissions. - */ - permissions: { - relation: Model.ManyToManyRelation, - modelClass: this.relationBindKnex(Permission.default), - join: { - from: 'resources.id', - through: { - from: 'role_has_permissions.resourceId', - to: 'role_has_permissions.permissionId', - }, - to: 'permissions.id', - }, - }, }; } } diff --git a/server/src/models/ResourceFieldMetadata.js b/server/src/models/ResourceFieldMetadata.js index 89ceebcff..8b3a54c84 100644 --- a/server/src/models/ResourceFieldMetadata.js +++ b/server/src/models/ResourceFieldMetadata.js @@ -1,5 +1,4 @@ import TenantModel from '@/models/TenantModel'; -import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection'; export default class ResourceFieldMetadata extends TenantModel { /** @@ -8,11 +7,4 @@ export default class ResourceFieldMetadata extends TenantModel { static get tableName() { return 'resource_custom_fields_metadata'; } - - /** - * Override the resource field metadata collection. - */ - static get collection() { - return ResourceFieldMetadataCollection; - } } diff --git a/server/src/models/Role.js b/server/src/models/Role.js deleted file mode 100644 index 9f0f9f401..000000000 --- a/server/src/models/Role.js +++ /dev/null @@ -1,91 +0,0 @@ -import { Model } from 'objection'; -import TenantModel from '@/models/TenantModel'; - -export default class Role extends TenantModel { - /** - * Table name of Role model. - * @type {String} - */ - static get tableName() { - return 'roles'; - } - - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - - /** - * Relationship mapping. - */ - static get relationMappings() { - const Permission = require('@/models/Permission'); - const Resource = require('@/models/Resource'); - const User = require('@/models/TenantUser'); - const ResourceField = require('@/models/ResourceField'); - - return { - /** - * Role may has many permissions. - */ - permissions: { - relation: Model.ManyToManyRelation, - modelClass: Permission.default.bindKnex(this.knexBinded), - join: { - from: 'roles.id', - through: { - from: 'role_has_permissions.roleId', - to: 'role_has_permissions.permissionId', - }, - to: 'permissions.id', - }, - }, - - /** - * Role may has many resources. - */ - resources: { - relation: Model.ManyToManyRelation, - modelClass: Resource.default.bindKnex(this.knexBinded), - join: { - from: 'roles.id', - through: { - from: 'role_has_permissions.roleId', - to: 'role_has_permissions.resourceId', - }, - to: 'resources.id', - }, - }, - - /** - * Role may has resource field. - */ - field: { - relation: Model.BelongsToOneRelation, - modelClass: ResourceField.default.bindKnex(this.knexBinded), - join: { - from: 'roles.fieldId', - to: 'resource_fields.id', - }, - }, - - /** - * Role may has many associated users. - */ - users: { - relation: Model.ManyToManyRelation, - modelClass: User.default.bindKnex(this.knexBinded), - join: { - from: 'roles.id', - through: { - from: 'user_has_roles.roleId', - to: 'user_has_roles.userId', - }, - to: 'users.id', - }, - }, - }; - } -} diff --git a/server/src/models/TenantUser.js b/server/src/models/TenantUser.js index b39011e41..343260076 100644 --- a/server/src/models/TenantUser.js +++ b/server/src/models/TenantUser.js @@ -1,7 +1,5 @@ import bcrypt from 'bcryptjs'; -import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -// import PermissionsService from '@/services/PermissionsService'; export default class TenantUser extends TenantModel { /** @@ -24,29 +22,7 @@ export default class TenantUser extends TenantModel { static get timestamps() { return ['createdAt', 'updatedAt']; } - - /** - * 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. diff --git a/server/src/services/Authentication/AuthenticationMailMessages.ts b/server/src/services/Authentication/AuthenticationMailMessages.ts new file mode 100644 index 000000000..f9461a1fd --- /dev/null +++ b/server/src/services/Authentication/AuthenticationMailMessages.ts @@ -0,0 +1,35 @@ +import { Service } from "typedi"; + +@Service() +export default class AuthenticationMailMesssages { + + sendWelcomeMessage() { + const Logger = Container.get('logger'); + const Mail = Container.get('mail'); + + const filePath = path.join(global.rootPath, 'views/mail/Welcome.html'); + const template = fs.readFileSync(filePath, 'utf8'); + const rendered = Mustache.render(template, { + email, organizationName, firstName, + }); + const mailOptions = { + to: email, + from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, + subject: 'Welcome to Bigcapital', + html: rendered, + }; + Mail.sendMail(mailOptions, (error) => { + if (error) { + Logger.error('Failed send welcome mail', { error, form }); + done(error); + return; + } + Logger.info('User has been sent welcome email successfuly.', { form }); + done(); + }); + } + + sendResetPasswordMessage() { + + } +} \ No newline at end of file diff --git a/server/src/services/Authentication/AuthenticationSMSMessages.ts b/server/src/services/Authentication/AuthenticationSMSMessages.ts new file mode 100644 index 000000000..3635b7054 --- /dev/null +++ b/server/src/services/Authentication/AuthenticationSMSMessages.ts @@ -0,0 +1,13 @@ +import { Service } from "typedi"; + +@Service() +export default class AuthenticationSMSMessages { + smsClient: any; + + sendWelcomeMessage() { + const message: string = `Hi ${firstName}, Welcome to Bigcapital, You've joined the new workspace, + if you need any help please don't hesitate to contact us.` + + + } +} \ No newline at end of file diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 841a7720a..c14f36d02 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -3,8 +3,8 @@ import JWT from 'jsonwebtoken'; import uniqid from 'uniqid'; import { omit } from 'lodash'; import { - EventDispatcher - EventDispatcherInterface + EventDispatcher, + EventDispatcherInterface, } from '@/decorators/eventDispatcher'; import { SystemUser, @@ -22,6 +22,8 @@ import { hashPassword } from '@/utils'; import { ServiceError, ServiceErrors } from "@/exceptions"; import config from '@/../config/config'; import events from '@/subscribers/events'; +import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages'; +import AuthenticationSMSMessages from '@/services/Authentication/AuthenticationSMSMessages'; @Service() export default class AuthenticationService { @@ -34,6 +36,12 @@ export default class AuthenticationService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; + @Inject() + smsMessages: AuthenticationSMSMessages; + + @Inject() + mailMessages: AuthenticationMailMessages; + /** * Signin and generates JWT token. * @throws {ServiceError} @@ -70,6 +78,7 @@ export default class AuthenticationService { this.logger.info('[login] Logging success.', { user, token }); + // Triggers `onLogin` event. this.eventDispatcher.dispatch(events.auth.login, { emailOrPhone, password, }); @@ -191,6 +200,7 @@ export default class AuthenticationService { const passwordReset = await PasswordReset.query().insert({ email, token }); const user = await SystemUser.query().findOne('email', email); + // Triggers `onSendResetPassword` event. this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token }); return passwordReset; @@ -225,25 +235,26 @@ export default class AuthenticationService { // Delete the reset password token. await PasswordReset.query().where('email', user.email).delete(); - this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token, password }); + // Triggers `onResetPassword` event. + this.eventDispatcher.dispatch(events.auth.resetPassword, { user, token, password }); this.logger.info('[reset_password] reset password success.'); } /** * Generates JWT token for the given user. - * @param {IUser} user + * @param {ISystemUser} user * @return {string} token */ - generateToken(user: IUser): string { + generateToken(user: ISystemUser): string { const today = new Date(); const exp = new Date(today); exp.setDate(today.getDate() + 60); - this.logger.silly(`Sign JWT for userId: ${user._id}`); + this.logger.silly(`Sign JWT for userId: ${user.id}`); return JWT.sign( { - _id: user._id, // We are gonna use this in the middleware 'isAuth' + id: user.id, // We are gonna use this in the middleware 'isAuth' exp: exp.getTime() / 1000, }, config.jwtSecret, diff --git a/server/src/services/InviteUsers/InviteUsersMailMessages.ts b/server/src/services/InviteUsers/InviteUsersMailMessages.ts new file mode 100644 index 000000000..bb37ff4ba --- /dev/null +++ b/server/src/services/InviteUsers/InviteUsersMailMessages.ts @@ -0,0 +1,31 @@ +import { Service } from "typedi"; + +@Service() +export default class InviteUsersMailMessages { + + sendInviteMail() { + const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html'); + const template = fs.readFileSync(filePath, 'utf8'); + + const rendered = Mustache.render(template, { + acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`, + fullName: `${user.firstName} ${user.lastName}`, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + organizationName: organizationOptions.getMeta('organization_name'), + }); + const mailOptions = { + to: user.email, + from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, + subject: `${user.fullName} has invited you to join a Bigcapital`, + html: rendered, + }; + mail.sendMail(mailOptions, (error) => { + if (error) { + Logger.log('error', 'Failed send user invite mail', { error, form }); + } + Logger.log('info', 'User has been sent invite user email successfuly.', { form }); + }); + } +} \ No newline at end of file diff --git a/server/src/services/InviteUsers/InviteUsersSMSMessages.ts b/server/src/services/InviteUsers/InviteUsersSMSMessages.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/services/InviteUsers/index.ts b/server/src/services/InviteUsers/index.ts new file mode 100644 index 000000000..13ee5927e --- /dev/null +++ b/server/src/services/InviteUsers/index.ts @@ -0,0 +1,172 @@ +import { Service, Inject } from "typedi"; +import uniqid from 'uniqid'; +import { + EventDispatcher, + EventDispatcherInterface, +} from '@/decorators/eventDispatcher'; +import { ServiceError, ServiceErrors } from "@/exceptions"; +import { SystemUser, Invite } from "@/system/models"; +import { hashPassword } from '@/utils'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import TenantsManager from "@/system/TenantsManager"; +import InviteUsersMailMessages from "@/services/InviteUsers/InviteUsersMailMessages"; +import events from '@/subscribers/events'; +import { + ISystemUser, + IInviteUserInput, +} from '@/interfaces'; + +@Service() +export default class InviteUserService { + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject() + tenancy: TenancyService; + + @Inject() + tenantsManager: TenantsManager; + + @Inject('logger') + logger: any; + + @Inject() + mailMessages: InviteUsersMailMessages; + + /** + * Accept the received invite. + * @param {string} token + * @param {IInviteUserInput} inviteUserInput + * @throws {ServiceErrors} + * @returns {Promise} + */ + async acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise { + const inviteToken = await this.getInviteOrThrowError(token); + await this.validateUserEmailAndPhone(inviteUserInput); + + this.logger.info('[aceept_invite] trying to hash the user password.'); + const hashedPassword = await hashPassword(inviteUserInput.password); + + const user = SystemUser.query() + .where('email', inviteUserInput.email) + .patch({ + ...inviteUserInput, + active: 1, + email: inviteToken.email, + invite_accepted_at: moment().format('YYYY/MM/DD'), + password: hashedPassword, + tenant_id: inviteToken.tenantId, + }); + + const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete(); + + await Promise.all([ + insertUserOper, + deleteInviteTokenOper, + ]); + + // Triggers `onUserAcceptInvite` event. + this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, { + inviteToken, user, + }); + } + + /** + * Sends invite mail to the given email from the given tenant and user. + * @param {number} tenantId - + * @param {string} email - + * @param {IUser} authorizedUser - + * + * @return {Promise} + */ + public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise { + const { Option } = this.tenancy.models(tenantId); + await this.throwErrorIfUserEmailExists(email); + + const invite = await Invite.query().insert({ + email, + tenant_id: authorizedUser.tenantId, + token: uniqid(), + }); + + // Triggers `onUserSendInvite` event. + this.eventDispatcher.dispatch(events.inviteUser.sendInvite, { + invite, + }); + return { invite }; + } + + /** + * Validate the given invite token. + * @param {string} token - the given token string. + * @throws {ServiceError} + */ + public async checkInvite(token: string) { + const inviteToken = await this.getInviteOrThrowError(token) + + // Find the tenant that associated to the given token. + const tenant = await Tenant.query().findOne('id', inviteToken.tenantId); + + const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId); + + const organizationOptions = await Option.bindKnex(tenantDb).query() + .where('key', 'organization_name'); + + // Triggers `onUserCheckInvite` event. + this.eventDispatcher.dispatch(events.inviteUser.checkInvite, { + inviteToken, organizationOptions, + }); + return { inviteToken, organizationOptions }; + } + + private async throwErrorIfUserEmailExists(email: string) { + const foundUser = await SystemUser.query().findOne('email', email); + + if (foundUser) { + throw new ServiceError('email_already_invited'); + } + } + + /** + * Retrieve invite model from the given token or throw error. + * @param {string} token - Then given token string. + * @throws {ServiceError} + */ + private async getInviteOrThrowError(token: string) { + const inviteToken = await Invite.query().findOne('token', token); + + if (!inviteToken) { + this.logger.info('[aceept_invite] the invite token is invalid.'); + throw new ServiceError('invite_token_invalid'); + } + } + + /** + * Validate the given user email and phone number uniquine. + * @param {IInviteUserInput} inviteUserInput + */ + private async validateUserEmailAndPhone(inviteUserInput: IInviteUserInput) { + const foundUser = await SystemUser.query() + .onBuild(query => { + query.where('email', inviteUserInput.email); + + if (inviteUserInput.phoneNumber) { + query.where('phone_number', inviteUserInput.phoneNumber); + } + }); + const serviceErrors: ServiceError[] = []; + + if (foundUser && foundUser.email === inviteUserInput.email) { + this.logger.info('[send_user_invite] the given email exists.'); + serviceErrors.push(new ServiceError('email_exists')); + } + if (foundUser && foundUser.phoneNumber === inviteUserInput.phoneNumber) { + this.logger.info('[send_user_invite] the given phone number exists.'); + serviceErrors.push(new ServiceError('phone_number_exists')); + } + if (serviceErrors.length > 0) { + throw new ServiceErrors(serviceErrors); + } + } + +} \ No newline at end of file diff --git a/server/src/services/Moment/index.js b/server/src/services/Moment/index.js deleted file mode 100644 index 525adfada..000000000 --- a/server/src/services/Moment/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import Moment from 'moment'; -import { extendMoment } from 'moment-range'; - -const moment = extendMoment(Moment); - -export default moment; diff --git a/server/src/services/Organization/index.ts b/server/src/services/Organization/index.ts new file mode 100644 index 000000000..40e1f9d5c --- /dev/null +++ b/server/src/services/Organization/index.ts @@ -0,0 +1,56 @@ +import { Service, Inject, Container } from 'typedi'; +import { Tenant } from '@/system/models'; +import TenantsManager from '@/system/TenantsManager'; +import { ServiceError } from '@/exceptions'; +import { ITenant } from '@/interfaces'; + +@Service() +export default class OrganizationService { + @Inject() + tenantsManager: TenantsManager; + + @Inject('dbManager') + dbManager: any; + + @Inject('logger') + logger: any; + + /** + * Builds the database schema and seed data of the given organization id. + * @param {srting} organizationId + * @return {Promise} + */ + async build(organizationId: string): Promise { + const tenant = await Tenant.query().findOne('organization_id', organizationId); + this.throwIfTenantNotExists(tenant); + this.throwIfTenantInitizalized(tenant); + + this.logger.info('[tenant_db_build] tenant DB creating.', { tenant }); + await this.dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`); + + const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId); + + this.logger.info('[tenant_db_build] tenant DB migrating to latest version.', { tenant }); + await tenantDb.migrate.latest(); + + this.logger.info('[tenant_db_build] mark tenant as initialized.', { tenant }); + await tenant.$query().update({ initialized: true }); + } + + private throwIfTenantNotExists(tenant: ITenant) { + if (!tenant) { + this.logger.info('[tenant_db_build] organization id not found.'); + throw new ServiceError('tenant_not_found'); + } + } + + private throwIfTenantInitizalized(tenant: ITenant) { + if (tenant.initialized) { + throw new ServiceError('tenant_initialized'); + } + } + + destroy() { + + } +} \ No newline at end of file diff --git a/server/src/services/Permissions/PermissionsService.js b/server/src/services/Permissions/PermissionsService.js deleted file mode 100644 index 9bd267a16..000000000 --- a/server/src/services/Permissions/PermissionsService.js +++ /dev/null @@ -1,77 +0,0 @@ -import cache from 'memory-cache'; -import { difference } from 'lodash'; -import Role from '@/models/Role'; - -export default { - cacheKey: 'ratteb.cache,', - cacheExpirationTime: null, - permissions: [], - cache: null, - - /** - * Initialize the cache. - */ - initializeCache() { - if (!this.cache) { - this.cache = new cache.Cache(); - } - }, - - /** - * Purge all cached permissions. - */ - forgetCachePermissions() { - this.cache.del(this.cacheKey); - this.permissions = []; - }, - - /** - * Get all stored permissions. - */ - async getPermissions() { - if (this.permissions.length <= 0) { - const cachedPerms = this.cache.get(this.cacheKey); - - if (!cachedPerms) { - this.permissions = await this.getPermissionsFromStorage(); - this.cache.put(this.cacheKey, this.permissions); - } else { - this.permissions = cachedPerms; - } - } - return this.permissions; - }, - - /** - * Fetches all roles and permissions from the storage. - */ - async getPermissionsFromStorage() { - const roles = await Role.fetchAll({ - withRelated: ['resources.permissions'], - }); - return roles.toJSON(); - }, - - /** - * Detarmine the given resource has the permissions. - * @param {String} resource - - * @param {Array} permissions - - */ - async hasPermissions(resource, permissions) { - await this.getPermissions(); - - const userRoles = this.permissions.filter((role) => role.id === this.id); - const perms = []; - - userRoles.forEach((role) => { - const roleResources = role.resources || []; - const foundResource = roleResources.find((r) => r.name === resource); - - if (foundResource && foundResource.permissions) { - foundResource.permissions.forEach((p) => perms.push(p.name)); - } - }); - const notAllowedPerms = difference(permissions, perms); - return (notAllowedPerms.length <= 0); - }, -}; diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 72e4bda5e..b6be2a26b 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -27,6 +27,9 @@ export default class PaymentReceiveService { @Inject() journalService: JournalPosterService; + @Inject('logger') + logger: any; + /** * Creates a new payment receive and store it to the storage * with associated invoices payment and journal transactions. @@ -43,6 +46,8 @@ export default class PaymentReceiveService { } = this.tenancy.models(tenantId); const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); + + this.logger.info('[payment_receive] inserting to the storage.'); const storedPaymentReceive = await PaymentReceive.query() .insert({ amount: paymentAmount, @@ -50,12 +55,15 @@ export default class PaymentReceiveService { }); const storeOpers: Array = []; + this.logger.info('[payment_receive] inserting associated entries to the storage.'); paymentReceive.entries.forEach((entry: any) => { const oper = PaymentReceiveEntry.query() .insert({ payment_receive_id: storedPaymentReceive.id, ...entry, }); + + this.logger.info('[payment_receive] increment the sale invoice payment amount.'); // Increment the invoice payment amount. const invoice = SaleInvoice.query() .where('id', entry.invoice_id) @@ -64,6 +72,8 @@ export default class PaymentReceiveService { storeOpers.push(oper); storeOpers.push(invoice); }); + + this.logger.info('[payment_receive] decrementing customer balance.'); const customerIncrementOper = Customer.decrementBalance( paymentReceive.customer_id, paymentAmount, diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index 72596af3e..a0649751a 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -16,6 +16,9 @@ export default class SaleEstimateService { @Inject() itemsEntriesService: HasItemsEntries; + @Inject('logger') + logger: any; + /** * Creates a new estimate with associated entries. * @async @@ -31,12 +34,15 @@ export default class SaleEstimateService { amount, ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), }; + + this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); const storedEstimate = await SaleEstimate.query() .insert({ ...omit(estimate, ['entries']), }); const storeEstimateEntriesOpers: any[] = []; + this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.'); estimate.entries.forEach((entry: any) => { const oper = ItemEntry.query() .insert({ @@ -48,6 +54,8 @@ export default class SaleEstimateService { }); await Promise.all([...storeEstimateEntriesOpers]); + this.logger.info('[sale_estimate] insert sale estimated success.'); + return storedEstimate; } @@ -67,6 +75,7 @@ export default class SaleEstimateService { amount, ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), }; + this.logger.info('[sale_estimate] editing sale estimate on the storage.'); const updatedEstimate = await SaleEstimate.query() .update({ ...omit(estimate, ['entries']), @@ -96,14 +105,14 @@ export default class SaleEstimateService { */ async deleteEstimate(tenantId: number, estimateId: number) { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + + this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.'); await ItemEntry.query() .where('reference_id', estimateId) .where('reference_type', 'SaleEstimate') .delete(); - await SaleEstimate.query() - .where('id', estimateId) - .delete(); + await SaleEstimate.query().where('id', estimateId).delete(); } /** @@ -113,10 +122,10 @@ export default class SaleEstimateService { * @param {Numeric} estimateId * @return {Boolean} */ - async isEstimateExists(estimateId: number) { + async isEstimateExists(tenantId: number, estimateId: number) { const { SaleEstimate } = this.tenancy.models(tenantId); - const foundEstimate = await SaleEstimate.query() - .where('id', estimateId); + const foundEstimate = await SaleEstimate.query().where('id', estimateId); + return foundEstimate.length !== 0; } @@ -192,7 +201,6 @@ export default class SaleEstimateService { const foundEstimates = await SaleEstimate.query() .onBuild((query: any) => { query.where('estimate_number', estimateNumber); - if (excludeEstimateId) { query.whereNot('id', excludeEstimateId); } diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 581760148..730aea20a 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -23,6 +23,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @Inject() itemsEntriesService: HasItemsEntries; + @Inject('logger') + logger: any; + /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. @@ -43,12 +46,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost { paymentAmount: 0, invLotNumber, }; + + this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); const storedInvoice = await SaleInvoice.query() .insert({ ...omit(saleInvoice, ['entries']), }); const opers: Array = []; + this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.'); saleInvoice.entries.forEach((entry: any) => { const oper = ItemEntry.query() .insertAndFetch({ @@ -61,15 +67,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost { opers.push(oper); }); + this.logger.info('[sale_invoice] trying to increment the customer balance.'); // Increment the customer balance after deliver the sale invoice. const incrementOper = Customer.incrementBalance( saleInvoice.customer_id, balance, ); + // Await all async operations. - await Promise.all([ - ...opers, incrementOper, - ]); + await Promise.all([ ...opers, incrementOper ]); + // Records the inventory transactions for inventory items. await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id); @@ -100,6 +107,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { balance, invLotNumber: oldSaleInvoice.invLotNumber, }; + + this.logger.info('[sale_invoice] trying to update sale invoice.'); const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query() .where('id', saleInvoiceId) .update({ @@ -114,6 +123,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries( tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId, ); + + this.logger.info('[sale_invoice] change customer different balance.'); // Changes the diff customer balance between old and new amount. const changeCustomerBalanceOper = Customer.changeDiffBalance( saleInvoice.customer_id, @@ -155,12 +166,14 @@ export default class SaleInvoicesService extends SalesInvoicesCost { .findById(saleInvoiceId) .withGraphFetched('entries'); + this.logger.info('[sale_invoice] delete sale invoice with entries.'); await SaleInvoice.query().where('id', saleInvoiceId).delete(); await ItemEntry.query() .where('reference_id', saleInvoiceId) .where('reference_type', 'SaleInvoice') .delete(); + this.logger.info('[sale_invoice] revert the customer balance.'); const revertCustomerBalanceOper = Customer.changeBalance( oldSaleInvoice.customerId, oldSaleInvoice.balance * -1, @@ -203,7 +216,13 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {number} saleInvoiceId - * @param {boolean} override - */ - recordInventoryTranscactions(tenantId: number, saleInvoice, saleInvoiceId: number, override?: boolean){ + recordInventoryTranscactions( + tenantId: number, + saleInvoice, + saleInvoiceId: number, + override?: boolean + ){ + this.logger.info('[sale_invoice] saving inventory transactions'); const inventortyTransactions = saleInvoice.entries .map((entry) => ({ ...pick(entry, ['item_id', 'quantity', 'rate',]), @@ -228,6 +247,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { const { InventoryTransaction } = this.tenancy.models(tenantId); const opers: Promise<[]>[] = []; + + this.logger.info('[sale_invoice] reverting inventory transactions'); inventoryTransactions.forEach((trans: any) => { switch(trans.direction) { @@ -359,7 +380,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * Writes the sale invoice journal entries. * @param {SaleInvoice} saleInvoice - */ - async writeNonInventoryInvoiceJournals(tenantId: number, saleInvoice: ISaleInvoice, override: boolean) { + async writeNonInventoryInvoiceJournals( + tenantId: number, + saleInvoice: ISaleInvoice, + override: boolean + ) { const { Account, AccountTransaction } = this.tenancy.models(tenantId); const accountsDepGraph = await Account.depGraph().query(); diff --git a/server/src/services/Settings/SettingsStore.ts b/server/src/services/Settings/SettingsStore.ts new file mode 100644 index 000000000..66eb0dda5 --- /dev/null +++ b/server/src/services/Settings/SettingsStore.ts @@ -0,0 +1,15 @@ +import Knex from 'knex'; +import MetableStoreDB from '@/lib/Metable/MetableStoreDB'; +import Setting from '@/models/Setting'; + +export default class SettingsStore extends MetableStoreDB { + /** + * Constructor method. + * @param {number} tenantId + */ + constructor(knex: Knex) { + super(); + this.setExtraColumns(['group']); + this.setModel(Setting.bindKnex(knex)); + } +} \ No newline at end of file diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 3c4ffd6c7..f8ca06b3a 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -15,7 +15,6 @@ export default class HasTenancyService { * @param {number} tenantId - The tenant id. */ models(tenantId: number) { - console.log(tenantId); return this.tenantContainer(tenantId).get('models'); } } \ No newline at end of file diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index fd3c9611d..73a8a6481 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -7,4 +7,10 @@ export default { sendResetPassword: 'onSendResetPassword', resetPassword: 'onResetPassword', }, -} \ No newline at end of file + + inviteUser: { + acceptInvite: 'onUserAcceptInvite', + sendInvite: 'onUserSendInvite', + checkInvite: 'onUserCheckInvite' + } +} diff --git a/server/src/subscribers/inviteUser.ts b/server/src/subscribers/inviteUser.ts new file mode 100644 index 000000000..a4be953db --- /dev/null +++ b/server/src/subscribers/inviteUser.ts @@ -0,0 +1,29 @@ +import { Container } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from '@/subscribers/events'; + +@EventSubscriber() +export class InviteUserSubscriber { + + @On(events.inviteUser.acceptInvite) + public onAcceptInvite(payload) { + const { inviteToken, user } = payload; + const agenda = Container.get('agenda'); + + } + + @On(events.inviteUser.checkInvite) + public onCheckInvite(payload) { + const { inviteToken, organizationOptions } = payload; + const agenda = Container.get('agenda'); + + } + + @On(events.inviteUser.sendInvite) + public onSendInvite(payload) { + const { invite } = payload; + const agenda = Container.get('agenda'); + + + } +} \ No newline at end of file diff --git a/server/src/system/TenantEnvironment.js b/server/src/system/TenantEnvironment.js deleted file mode 100644 index 5af74c39c..000000000 --- a/server/src/system/TenantEnvironment.js +++ /dev/null @@ -1,12 +0,0 @@ - - -export default class TenantEnviroment { - - static get currentTenant() { - return this.currentTenantWebsite; - } - - static set currentTenant(website) { - this.currentTenantWebsite = website; - } -} \ No newline at end of file diff --git a/server/src/system/migrations/20200420134631_create_tenants_table.js b/server/src/system/migrations/20200420134631_create_tenants_table.js index 5121509f3..51bfa3bf2 100644 --- a/server/src/system/migrations/20200420134631_create_tenants_table.js +++ b/server/src/system/migrations/20200420134631_create_tenants_table.js @@ -3,6 +3,7 @@ exports.up = function(knex) { return knex.schema.createTable('tenants', (table) => { table.bigIncrements(); table.string('organization_id'); + table.boolean('initialized').defaultTo(false); table.timestamps(); }); }; diff --git a/server/src/system/models/SystemOption.js b/server/src/system/models/SystemOption.js index 272bbd594..0c53588da 100644 --- a/server/src/system/models/SystemOption.js +++ b/server/src/system/models/SystemOption.js @@ -1,6 +1,5 @@ import { mixin } from 'objection'; import SystemModel from '@/system/models/SystemModel'; -import MetableCollection from '@/lib/Metable/MetableCollection'; export default class Option extends SystemModel { /** @@ -9,21 +8,4 @@ export default class Option extends SystemModel { 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/tests/lib/MetableStore.test.ts b/server/tests/lib/MetableStore.test.ts new file mode 100644 index 000000000..a2be0bdca --- /dev/null +++ b/server/tests/lib/MetableStore.test.ts @@ -0,0 +1,39 @@ +import { expect } from '~/testInit'; +import MetableStore from '@/lib/MetableStore'; + +describe('MetableStore()', () => { + + describe('find', () => { + it('Find metadata by the given key.', () => { + const store = new MetableStore(); + store.metadata = [{ key: 'first-key', value: 'first-value' }]; + + const meta = store.find('first-key'); + + expect(meta.value).equals('first-value'); + expect(meta.key).equals('first-key'); + }); + + it('Find metadata by the key as payload.', () => { + + }); + + it('Find metadata by the given key and extra columns.', () => { + + }); + }); + + describe('all()', () => { + it('Should retrieve all metadata in the store.', () => { + + }); + }); + + describe('get()', () => { + it('Should retrieve data of the given metadata query.', () => { + + }); + }); + + describe('removeMeta') +}); \ No newline at end of file diff --git a/server/tests/models/Option.test.js b/server/tests/models/Option.test.js deleted file mode 100644 index b8122afc9..000000000 --- a/server/tests/models/Option.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import { create, expect } from '~/testInit'; -import Option from '@/models/Option'; -import MetableCollection from '@/lib/Metable/MetableCollection'; -import { - tenantFactory, - tenantWebsite, -} from '~/dbInit'; - - -describe('Model: Option', () => { - it('Should result collection be instance of `MetableCollection` class.', async () => { - await tenantFactory.create('option'); - await tenantFactory.create('option'); - const options = await Option.tenant().query(); - - expect(options).to.be.an.instanceof(MetableCollection); - }); -}); diff --git a/server/tests/routes/bills.test.js b/server/tests/routes/bills.test.js index 1e5ad10ca..5ace5cec2 100644 --- a/server/tests/routes/bills.test.js +++ b/server/tests/routes/bills.test.js @@ -8,7 +8,7 @@ import { loginRes } from '~/dbInit'; -describe.only('route: `/api/purchases/bills`', () => { +describe('route: `/api/purchases/bills`', () => { describe('POST: `/api/purchases/bills`', () => { it('Should `bill_number` be required.', async () => { const res = await request() diff --git a/server/tsconfig.json b/server/tsconfig.json index 6f74ca9f2..82c123afa 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,4 +1,6 @@ { + "include": ["./src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"], "compilerOptions": { "outDir": "./dist/", "sourceMap": true,