From d3d772f7355b2db9660ec21c8b031861f2fc9142 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 28 Sep 2020 13:30:50 +0200 Subject: [PATCH] feat: ensure organization tenant configured. --- README.md | 20 ---------- server/src/api/controllers/Organization.ts | 6 +++ .../api/controllers/Sales/SalesInvoices.ts | 4 +- server/src/api/controllers/Settings.ts | 6 +-- server/src/api/index.ts | 6 ++- .../api/middleware/ConfiguredMiddleware.ts | 14 ------- .../middleware/EnsureConfiguredMiddleware.ts | 12 ++++++ .../middleware/EnsureTenantIsInitialized.ts | 7 ---- .../api/middleware/EnsureTenantIsSeeded.ts | 19 +++++++++ server/src/data/options.js | 6 +++ server/src/interfaces/Register.ts | 12 +++++- server/src/interfaces/User.ts | 4 ++ server/src/interfaces/View.ts | 39 ++++++++++++++++++- server/src/repositories/ViewRepository.ts | 3 ++ server/src/services/Authentication/index.ts | 16 ++++---- server/src/services/SMSClient/SMSAPI.ts | 9 ++++- .../src/services/Subscription/SMSMessages.ts | 14 ++++++- ...5339_create_subscription_licenses_table.js | 4 -- .../system/models/Subscriptions/License.js | 15 +++---- 19 files changed, 140 insertions(+), 76 deletions(-) delete mode 100644 server/src/api/middleware/ConfiguredMiddleware.ts create mode 100644 server/src/api/middleware/EnsureConfiguredMiddleware.ts create mode 100644 server/src/api/middleware/EnsureTenantIsSeeded.ts diff --git a/README.md b/README.md index f609358b5..e69de29bb 100644 --- a/README.md +++ b/README.md @@ -1,20 +0,0 @@ - -CLIENT ------------------------- -1. `cd client` -2. `npm install` - -RUN CLINET -1. npm run start - -SERVER ------------------------ -1. cd server -2. npm install -3. npm install -g knex webpack -4. write database details to .env files. -5. `knex migrate:latest` -6. `knex seed:run` - -RUN SERVER -1. npm run start \ No newline at end of file diff --git a/server/src/api/controllers/Organization.ts b/server/src/api/controllers/Organization.ts index b43126018..9d2351e05 100644 --- a/server/src/api/controllers/Organization.ts +++ b/server/src/api/controllers/Organization.ts @@ -8,6 +8,8 @@ import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser'; import OrganizationService from 'services/Organization'; import { ServiceError } from 'exceptions'; import BaseController from 'api/controllers/BaseController'; +import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware'; +import SettingsMiddleware from 'api/middleware/SettingsMiddleware'; @Service() export default class OrganizationController extends BaseController{ @@ -27,6 +29,10 @@ export default class OrganizationController extends BaseController{ router.use(TenancyMiddleware); router.use(SubscriptionMiddleware('main')); + // Should to seed organization tenant be configured. + router.use('/seed', SettingsMiddleware); + router.use('/seed', EnsureConfiguredMiddleware); + router.post( '/build', asyncMiddleware(this.build.bind(this)) diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 5b3c8a19a..96c0ef719 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import { Router, Request, Response } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { difference } from 'lodash'; import { raw } from 'objection'; @@ -21,7 +21,7 @@ export default class SaleInvoicesController { * Router constructor. */ router() { - const router = express.Router(); + const router = Router(); router.post( '/', diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts index 276d25199..a29cf574f 100644 --- a/server/src/api/controllers/Settings.ts +++ b/server/src/api/controllers/Settings.ts @@ -62,9 +62,7 @@ export default class SettingsController extends BaseController{ errorReasons.push({ type: 'OPTIONS.KEY.NOT.DEFINED', code: 200, - keys: notDefinedOptions.map((o) => ({ - ...pick(o, ['key', 'group']) - })), + keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })), }); } if (errorReasons.length) { @@ -80,7 +78,7 @@ export default class SettingsController extends BaseController{ message: 'Options have been saved successfully.', }); } - + /** * Retrieve settings. * @param {Request} req diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 399e06e91..9ec4ca04a 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -9,6 +9,8 @@ import TenancyMiddleware from 'api/middleware/TenancyMiddleware'; import EnsureTenantIsInitialized from 'api/middleware/EnsureTenantIsInitialized'; import SettingsMiddleware from 'api/middleware/SettingsMiddleware'; import I18nMiddleware from 'api/middleware/I18nMiddleware'; +import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware'; +import EnsureTenantIsSeeded from 'api/middleware/EnsureTenantIsSeeded'; // Routes import Authentication from 'api/controllers/Authentication'; @@ -57,6 +59,8 @@ export default () => { dashboard.use(SubscriptionMiddleware('main')); dashboard.use(EnsureTenantIsInitialized); dashboard.use(SettingsMiddleware); + dashboard.use(EnsureConfiguredMiddleware); + dashboard.use(EnsureTenantIsSeeded); dashboard.use('/users', Container.get(Users).router()); dashboard.use('/invite', Container.get(InviteUsers).authRouter()); @@ -76,7 +80,7 @@ export default () => { dashboard.use('/purchases', Purchases.router()); dashboard.use('/resources', Resources.router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); - dashboard.use('/media', Media.router()) + dashboard.use('/media', Media.router()); app.use('/', dashboard); diff --git a/server/src/api/middleware/ConfiguredMiddleware.ts b/server/src/api/middleware/ConfiguredMiddleware.ts deleted file mode 100644 index 40064320b..000000000 --- a/server/src/api/middleware/ConfiguredMiddleware.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Container } from 'typedi'; -import { Request, Response, NextFunction } from 'express'; - -export default async (req: Request, res: Response, next: NextFunction) => { - const { Option } = req.models; - const option = await Option.query().where('key', 'app_configured'); - - if (option.getMeta('app_configured', false)) { - return res.res(400).send({ - errors: [{ type: 'TENANT.NOT.CONFIGURED', code: 700 }], - }); - } - next(); -}; diff --git a/server/src/api/middleware/EnsureConfiguredMiddleware.ts b/server/src/api/middleware/EnsureConfiguredMiddleware.ts new file mode 100644 index 000000000..1509d4d25 --- /dev/null +++ b/server/src/api/middleware/EnsureConfiguredMiddleware.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from 'express'; + +export default (req: Request, res: Response, next: NextFunction) => { + const { settings } = req; + + if (!settings.get('app_configured', false)) { + return res.boom.badRequest(null, { + errors: [{ type: 'APP.NOT.CONFIGURED', code: 100 }], + }); + } + next(); +}; diff --git a/server/src/api/middleware/EnsureTenantIsInitialized.ts b/server/src/api/middleware/EnsureTenantIsInitialized.ts index 189bb8013..e9e3203bc 100644 --- a/server/src/api/middleware/EnsureTenantIsInitialized.ts +++ b/server/src/api/middleware/EnsureTenantIsInitialized.ts @@ -16,12 +16,5 @@ export default (req: Request, res: Response, next: Function) => { { errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }] }, ); } - if (!req.tenant.seededAt) { - Logger.info('[ensure_tenant_initialized_middleware] tenant databae not seeded.'); - return res.boom.badRequest( - 'Tenant database is not seeded with initial data yet.', - { errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }, - ); - } next(); }; \ No newline at end of file diff --git a/server/src/api/middleware/EnsureTenantIsSeeded.ts b/server/src/api/middleware/EnsureTenantIsSeeded.ts new file mode 100644 index 000000000..20d31e690 --- /dev/null +++ b/server/src/api/middleware/EnsureTenantIsSeeded.ts @@ -0,0 +1,19 @@ +import { Container } from 'typedi'; +import { Request, Response } from 'express'; + +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.seededAt) { + Logger.info('[ensure_tenant_initialized_middleware] tenant databae not seeded.'); + return res.boom.badRequest( + 'Tenant database is not seeded with initial data yet.', + { errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }, + ); + } + next(); +}; \ No newline at end of file diff --git a/server/src/data/options.js b/server/src/data/options.js index e2924d59b..9e0e5ed00 100644 --- a/server/src/data/options.js +++ b/server/src/data/options.js @@ -5,10 +5,12 @@ export default { { key: 'name', type: 'string', + configure: true, }, { key: 'base_currency', type: 'string', + configure: true, }, { key: 'industry', @@ -21,18 +23,22 @@ export default { { key: 'fiscal_year', type: 'string', + configure: true, }, { key: 'language', type: 'string', + configure: true, }, { key: 'time_zone', type: 'string', + configure: true, }, { key: 'date_format', type: 'string', + configure: true, }, ], }; \ No newline at end of file diff --git a/server/src/interfaces/Register.ts b/server/src/interfaces/Register.ts index 9f589a535..375c800ac 100644 --- a/server/src/interfaces/Register.ts +++ b/server/src/interfaces/Register.ts @@ -1,4 +1,5 @@ - +import { ISystemUser } from './User'; +import { ITenant } from './Tenancy'; export interface IRegisterDTO { firstName: string, @@ -13,4 +14,11 @@ export interface IPasswordReset { email: string, token: string, createdAt: Date, -}; \ No newline at end of file +}; + +export interface IAuthenticationService { + signIn(emailOrPhone: string, password: string): Promise<{ user: ISystemUser, token: string, tenant: ITenant }>; + register(registerDTO: IRegisterDTO): Promise; + sendResetPassword(email: string): Promise; + resetPassword(token: string, password: string): Promise; +} \ No newline at end of file diff --git a/server/src/interfaces/User.ts b/server/src/interfaces/User.ts index bbbd0475e..4389c0613 100644 --- a/server/src/interfaces/User.ts +++ b/server/src/interfaces/User.ts @@ -14,6 +14,10 @@ export interface ISystemUser { inviteAcceptAt: Date, lastLoginAt: Date, + deletedAt: Date, + + createdAt: Date, + updatedAt: Date, } export interface ISystemUserDTO { diff --git a/server/src/interfaces/View.ts b/server/src/interfaces/View.ts index 9c308b0a7..5e7ccbcf3 100644 --- a/server/src/interfaces/View.ts +++ b/server/src/interfaces/View.ts @@ -3,7 +3,7 @@ export interface IView { id: number, name: string, predefined: boolean, - resourceId: number, + resourceModel: string, favourite: boolean, rolesLogicRxpression: string, }; @@ -18,7 +18,44 @@ export interface IViewRole { }; export interface IViewHasColumn { + id :number, viewId: number, fieldId: number, index: number, +} + +export interface IViewRoleDTO { + index: number, + fieldKey: string, + comparator: string, + value: string, + viewId: number, +} + +export interface IViewColumnDTO { + id: number, + index: number, + viewId: number, + fieldKey: string, +}; + +export interface IViewDTO { + name: string, + logicExpression: string, + roles: IViewRoleDTO[], + columns: IViewColumnDTO[], +}; + +export interface IViewEditDTO { + name: string, + logicExpression: string, + roles: IViewRoleDTO[], + columns: IViewColumnDTO[], +}; + +export interface IViewsService { + listViews(tenantId: number, resourceModel: string): Promise; + newView(tenantId: number, viewDTO: IViewDTO): Promise; + editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise; + deleteView(tenantId: number, viewId: number): Promise; } \ No newline at end of file diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index 5c31253b5..ab4b0b8d9 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -31,6 +31,9 @@ export default class ViewRepository extends TenantRepository { }); } + /** + * Retrieve all views of the given resource id. + */ allByResource() { const resourceId = 1; return this.cache.get(`customView.resource.id.${resourceId}`, () => { diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index dba1cd6f7..90344d9cf 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -23,7 +23,7 @@ import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMS import TenantsManager from 'services/Tenancy/TenantsManager'; @Service() -export default class AuthenticationService { +export default class AuthenticationService implements IAuthenticationService { @Inject('logger') logger: any; @@ -49,7 +49,7 @@ export default class AuthenticationService { * @param {string} password - Password. * @return {Promise<{user: IUser, token: string}>} */ - async signIn(emailOrPhone: string, password: string): Promise<{user: ISystemUser, token: string, tenant: ITenant }> { + public async signIn(emailOrPhone: string, password: string): Promise<{user: ISystemUser, token: string, tenant: ITenant }> { this.logger.info('[login] Someone trying to login.', { emailOrPhone, password }); const { systemUserRepository } = this.sysRepositories; @@ -122,7 +122,7 @@ export default class AuthenticationService { * @throws {ServiceErrors} * @param {IUserDTO} user */ - async register(registerDTO: IRegisterDTO): Promise { + public async register(registerDTO: IRegisterDTO): Promise { this.logger.info('[register] Someone trying to register.'); await this.validateEmailAndPhoneUniqiness(registerDTO); @@ -160,7 +160,7 @@ export default class AuthenticationService { * @throws {ServiceError} * @param {string} email - email address. */ - private async validateEmailExistance(email: string) { + private async validateEmailExistance(email: string): Promise { const { systemUserRepository } = this.sysRepositories; const userByEmail = await systemUserRepository.getByEmail(email); @@ -176,7 +176,7 @@ export default class AuthenticationService { * @param {string} email * @return {} */ - async sendResetPassword(email: string): Promise { + public async sendResetPassword(email: string): Promise { this.logger.info('[send_reset_password] Trying to send reset password.'); const user = await this.validateEmailExistance(email); @@ -184,7 +184,7 @@ export default class AuthenticationService { this.logger.info('[send_reset_password] trying to delete all tokens by email.'); this.deletePasswordResetToken(email); - const token = uniqid(); + const token: string = uniqid(); this.logger.info('[send_reset_password] insert the generated token.'); const passwordReset = await PasswordReset.query().insert({ email, token }); @@ -201,9 +201,9 @@ export default class AuthenticationService { * @param {string} password - New Password. * @return {Promise} */ - async resetPassword(token: string, password: string): Promise { + public async resetPassword(token: string, password: string): Promise { const { systemUserRepository } = this.sysRepositories; - const tokenModel = await PasswordReset.query().findOne('token', token); + const tokenModel: IPasswordReset = await PasswordReset.query().findOne('token', token); if (!tokenModel) { this.logger.info('[reset_password] token invalid.'); diff --git a/server/src/services/SMSClient/SMSAPI.ts b/server/src/services/SMSClient/SMSAPI.ts index 6fc1d21a4..0b2e85251 100644 --- a/server/src/services/SMSClient/SMSAPI.ts +++ b/server/src/services/SMSClient/SMSAPI.ts @@ -7,7 +7,14 @@ export default class SMSAPI { this.smsClient = smsClient; } - sendMessage(to: string, message: string, extraParams: [], extraHeaders: []) { + /** + * + * @param {string} to + * @param {string} message + * @param {array} extraParams + * @param {array} extraHeaders + */ + sendMessage(to: string, message: string, extraParams?: [], extraHeaders?: []) { return this.smsClient.send(to, message); } } \ No newline at end of file diff --git a/server/src/services/Subscription/SMSMessages.ts b/server/src/services/Subscription/SMSMessages.ts index 3fa34afe9..181862059 100644 --- a/server/src/services/Subscription/SMSMessages.ts +++ b/server/src/services/Subscription/SMSMessages.ts @@ -6,7 +6,12 @@ export default class SubscriptionSMSMessages { @Inject('SMSClient') smsClient: SMSClient; - public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) { + /** + * Send remaining subscription period SMS message. + * @param {string} phoneNumber - + * @param {number} remainingDays - + */ + public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number): Promise { const message: string = ` Your remaining subscription is ${remainingDays} days, please renew your subscription before expire. @@ -14,7 +19,12 @@ export default class SubscriptionSMSMessages { this.smsClient.sendMessage(phoneNumber, message); } - public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) { + /** + * Send remaining trial period SMS message. + * @param {string} phoneNumber - + * @param {number} remainingDays - + */ + public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number): Promise { const message: string = ` Your remaining free trial is ${remainingDays} days, please subscription before ends, if you have any quation to contact us.`; diff --git a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js index 576abffca..5b79cd4a2 100644 --- a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js +++ b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js @@ -9,10 +9,6 @@ exports.up = function(knex) { table.integer('license_period').unsigned(); table.string('period_interval'); - table.boolean('sent').defaultTo(false); - table.boolean('disabled').defaultTo(false); - table.boolean('used').defaultTo(false); - table.dateTime('sent_at'); table.dateTime('disabled_at'); table.dateTime('used_at'); diff --git a/server/src/system/models/Subscriptions/License.js b/server/src/system/models/Subscriptions/License.js index 3bbc76e31..7713fbf9d 100644 --- a/server/src/system/models/Subscriptions/License.js +++ b/server/src/system/models/Subscriptions/License.js @@ -1,7 +1,6 @@ import { Model, mixin } from 'objection'; import moment from 'moment'; import SystemModel from 'system/models/SystemModel'; -import { ILicensesFilter } from 'interfaces'; export default class License extends SystemModel { /** @@ -25,8 +24,8 @@ export default class License extends SystemModel { return { // Filters active licenses. filterActiveLicense(query) { - query.where('disabled', false); - query.where('used', false); + query.where('disabled_at', null); + query.where('used_at', null); }, // Find license by its code or id. @@ -45,13 +44,13 @@ export default class License extends SystemModel { builder.modify('filterActiveLicense') } if (licensesFilter.disabled) { - builder.where('disabled', true); + builder.whereNot('disabled_at', null); } if (licensesFilter.used) { - builder.where('used', true); + builder.whereNot('used_at', null); } if (licensesFilter.sent) { - builder.where('sent', true); + builder.whereNot('sent_at', null); } } }; @@ -95,7 +94,6 @@ export default class License extends SystemModel { return this.query() .where(viaAttribute, licenseCode) .patch({ - disabled: true, disabled_at: moment().toMySqlDateTime(), }); } @@ -108,7 +106,6 @@ export default class License extends SystemModel { return this.query() .where(viaAttribute, licenseCode) .patch({ - sent: true, sent_at: moment().toMySqlDateTime(), }); } @@ -122,7 +119,6 @@ export default class License extends SystemModel { return this.query() .where(viaAttribute, licenseCode) .patch({ - used: true, used_at: moment().toMySqlDateTime() }); } @@ -136,5 +132,4 @@ export default class License extends SystemModel { return (this.invoicePeriod === plan.invoiceInterval && license.licensePeriod === license.periodInterval); } - }