feat: ensure organization tenant configured.

This commit is contained in:
Ahmed Bouhuolia
2020-09-28 13:30:50 +02:00
parent 9f315ca657
commit d3d772f735
19 changed files with 140 additions and 76 deletions

View File

@@ -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

View File

@@ -8,6 +8,8 @@ import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser';
import OrganizationService from 'services/Organization'; import OrganizationService from 'services/Organization';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware';
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
@Service() @Service()
export default class OrganizationController extends BaseController{ export default class OrganizationController extends BaseController{
@@ -27,6 +29,10 @@ export default class OrganizationController extends BaseController{
router.use(TenancyMiddleware); router.use(TenancyMiddleware);
router.use(SubscriptionMiddleware('main')); router.use(SubscriptionMiddleware('main'));
// Should to seed organization tenant be configured.
router.use('/seed', SettingsMiddleware);
router.use('/seed', EnsureConfiguredMiddleware);
router.post( router.post(
'/build', '/build',
asyncMiddleware(this.build.bind(this)) asyncMiddleware(this.build.bind(this))

View File

@@ -1,4 +1,4 @@
import express from 'express'; import { Router, Request, Response } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query, matchedData } from 'express-validator';
import { difference } from 'lodash'; import { difference } from 'lodash';
import { raw } from 'objection'; import { raw } from 'objection';
@@ -21,7 +21,7 @@ export default class SaleInvoicesController {
* Router constructor. * Router constructor.
*/ */
router() { router() {
const router = express.Router(); const router = Router();
router.post( router.post(
'/', '/',

View File

@@ -62,9 +62,7 @@ export default class SettingsController extends BaseController{
errorReasons.push({ errorReasons.push({
type: 'OPTIONS.KEY.NOT.DEFINED', type: 'OPTIONS.KEY.NOT.DEFINED',
code: 200, code: 200,
keys: notDefinedOptions.map((o) => ({ keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })),
...pick(o, ['key', 'group'])
})),
}); });
} }
if (errorReasons.length) { if (errorReasons.length) {

View File

@@ -9,6 +9,8 @@ import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
import EnsureTenantIsInitialized from 'api/middleware/EnsureTenantIsInitialized'; import EnsureTenantIsInitialized from 'api/middleware/EnsureTenantIsInitialized';
import SettingsMiddleware from 'api/middleware/SettingsMiddleware'; import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
import I18nMiddleware from 'api/middleware/I18nMiddleware'; import I18nMiddleware from 'api/middleware/I18nMiddleware';
import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware';
import EnsureTenantIsSeeded from 'api/middleware/EnsureTenantIsSeeded';
// Routes // Routes
import Authentication from 'api/controllers/Authentication'; import Authentication from 'api/controllers/Authentication';
@@ -57,6 +59,8 @@ export default () => {
dashboard.use(SubscriptionMiddleware('main')); dashboard.use(SubscriptionMiddleware('main'));
dashboard.use(EnsureTenantIsInitialized); dashboard.use(EnsureTenantIsInitialized);
dashboard.use(SettingsMiddleware); dashboard.use(SettingsMiddleware);
dashboard.use(EnsureConfiguredMiddleware);
dashboard.use(EnsureTenantIsSeeded);
dashboard.use('/users', Container.get(Users).router()); dashboard.use('/users', Container.get(Users).router());
dashboard.use('/invite', Container.get(InviteUsers).authRouter()); dashboard.use('/invite', Container.get(InviteUsers).authRouter());
@@ -76,7 +80,7 @@ export default () => {
dashboard.use('/purchases', Purchases.router()); dashboard.use('/purchases', Purchases.router());
dashboard.use('/resources', Resources.router()); dashboard.use('/resources', Resources.router());
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
dashboard.use('/media', Media.router()) dashboard.use('/media', Media.router());
app.use('/', dashboard); app.use('/', dashboard);

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -16,12 +16,5 @@ export default (req: Request, res: Response, next: Function) => {
{ errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }] }, { 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(); next();
}; };

View File

@@ -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();
};

View File

@@ -5,10 +5,12 @@ export default {
{ {
key: 'name', key: 'name',
type: 'string', type: 'string',
configure: true,
}, },
{ {
key: 'base_currency', key: 'base_currency',
type: 'string', type: 'string',
configure: true,
}, },
{ {
key: 'industry', key: 'industry',
@@ -21,18 +23,22 @@ export default {
{ {
key: 'fiscal_year', key: 'fiscal_year',
type: 'string', type: 'string',
configure: true,
}, },
{ {
key: 'language', key: 'language',
type: 'string', type: 'string',
configure: true,
}, },
{ {
key: 'time_zone', key: 'time_zone',
type: 'string', type: 'string',
configure: true,
}, },
{ {
key: 'date_format', key: 'date_format',
type: 'string', type: 'string',
configure: true,
}, },
], ],
}; };

View File

@@ -1,4 +1,5 @@
import { ISystemUser } from './User';
import { ITenant } from './Tenancy';
export interface IRegisterDTO { export interface IRegisterDTO {
firstName: string, firstName: string,
@@ -14,3 +15,10 @@ export interface IPasswordReset {
token: string, token: string,
createdAt: Date, createdAt: Date,
}; };
export interface IAuthenticationService {
signIn(emailOrPhone: string, password: string): Promise<{ user: ISystemUser, token: string, tenant: ITenant }>;
register(registerDTO: IRegisterDTO): Promise<ISystemUser>;
sendResetPassword(email: string): Promise<IPasswordReset>;
resetPassword(token: string, password: string): Promise<void>;
}

View File

@@ -14,6 +14,10 @@ export interface ISystemUser {
inviteAcceptAt: Date, inviteAcceptAt: Date,
lastLoginAt: Date, lastLoginAt: Date,
deletedAt: Date,
createdAt: Date,
updatedAt: Date,
} }
export interface ISystemUserDTO { export interface ISystemUserDTO {

View File

@@ -3,7 +3,7 @@ export interface IView {
id: number, id: number,
name: string, name: string,
predefined: boolean, predefined: boolean,
resourceId: number, resourceModel: string,
favourite: boolean, favourite: boolean,
rolesLogicRxpression: string, rolesLogicRxpression: string,
}; };
@@ -18,7 +18,44 @@ export interface IViewRole {
}; };
export interface IViewHasColumn { export interface IViewHasColumn {
id :number,
viewId: number, viewId: number,
fieldId: number, fieldId: number,
index: 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<void>;
newView(tenantId: number, viewDTO: IViewDTO): Promise<void>;
editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void>;
deleteView(tenantId: number, viewId: number): Promise<void>;
}

View File

@@ -31,6 +31,9 @@ export default class ViewRepository extends TenantRepository {
}); });
} }
/**
* Retrieve all views of the given resource id.
*/
allByResource() { allByResource() {
const resourceId = 1; const resourceId = 1;
return this.cache.get(`customView.resource.id.${resourceId}`, () => { return this.cache.get(`customView.resource.id.${resourceId}`, () => {

View File

@@ -23,7 +23,7 @@ import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMS
import TenantsManager from 'services/Tenancy/TenantsManager'; import TenantsManager from 'services/Tenancy/TenantsManager';
@Service() @Service()
export default class AuthenticationService { export default class AuthenticationService implements IAuthenticationService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -49,7 +49,7 @@ export default class AuthenticationService {
* @param {string} password - Password. * @param {string} password - Password.
* @return {Promise<{user: IUser, token: string}>} * @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 }); this.logger.info('[login] Someone trying to login.', { emailOrPhone, password });
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
@@ -122,7 +122,7 @@ export default class AuthenticationService {
* @throws {ServiceErrors} * @throws {ServiceErrors}
* @param {IUserDTO} user * @param {IUserDTO} user
*/ */
async register(registerDTO: IRegisterDTO): Promise<ISystemUser> { public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
this.logger.info('[register] Someone trying to register.'); this.logger.info('[register] Someone trying to register.');
await this.validateEmailAndPhoneUniqiness(registerDTO); await this.validateEmailAndPhoneUniqiness(registerDTO);
@@ -160,7 +160,7 @@ export default class AuthenticationService {
* @throws {ServiceError} * @throws {ServiceError}
* @param {string} email - email address. * @param {string} email - email address.
*/ */
private async validateEmailExistance(email: string) { private async validateEmailExistance(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email); const userByEmail = await systemUserRepository.getByEmail(email);
@@ -176,7 +176,7 @@ export default class AuthenticationService {
* @param {string} email * @param {string} email
* @return {<Promise<IPasswordReset>} * @return {<Promise<IPasswordReset>}
*/ */
async sendResetPassword(email: string): Promise<IPasswordReset> { public async sendResetPassword(email: string): Promise<IPasswordReset> {
this.logger.info('[send_reset_password] Trying to send reset password.'); this.logger.info('[send_reset_password] Trying to send reset password.');
const user = await this.validateEmailExistance(email); 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.logger.info('[send_reset_password] trying to delete all tokens by email.');
this.deletePasswordResetToken(email); this.deletePasswordResetToken(email);
const token = uniqid(); const token: string = uniqid();
this.logger.info('[send_reset_password] insert the generated token.'); this.logger.info('[send_reset_password] insert the generated token.');
const passwordReset = await PasswordReset.query().insert({ email, token }); const passwordReset = await PasswordReset.query().insert({ email, token });
@@ -201,9 +201,9 @@ export default class AuthenticationService {
* @param {string} password - New Password. * @param {string} password - New Password.
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async resetPassword(token: string, password: string): Promise<void> { public async resetPassword(token: string, password: string): Promise<void> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const tokenModel = await PasswordReset.query().findOne('token', token); const tokenModel: IPasswordReset = await PasswordReset.query().findOne('token', token);
if (!tokenModel) { if (!tokenModel) {
this.logger.info('[reset_password] token invalid.'); this.logger.info('[reset_password] token invalid.');

View File

@@ -7,7 +7,14 @@ export default class SMSAPI {
this.smsClient = smsClient; 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); return this.smsClient.send(to, message);
} }
} }

View File

@@ -6,7 +6,12 @@ export default class SubscriptionSMSMessages {
@Inject('SMSClient') @Inject('SMSClient')
smsClient: 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<void> {
const message: string = ` const message: string = `
Your remaining subscription is ${remainingDays} days, Your remaining subscription is ${remainingDays} days,
please renew your subscription before expire. please renew your subscription before expire.
@@ -14,7 +19,12 @@ export default class SubscriptionSMSMessages {
this.smsClient.sendMessage(phoneNumber, message); 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<void> {
const message: string = ` const message: string = `
Your remaining free trial is ${remainingDays} days, Your remaining free trial is ${remainingDays} days,
please subscription before ends, if you have any quation to contact us.`; please subscription before ends, if you have any quation to contact us.`;

View File

@@ -9,10 +9,6 @@ exports.up = function(knex) {
table.integer('license_period').unsigned(); table.integer('license_period').unsigned();
table.string('period_interval'); 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('sent_at');
table.dateTime('disabled_at'); table.dateTime('disabled_at');
table.dateTime('used_at'); table.dateTime('used_at');

View File

@@ -1,7 +1,6 @@
import { Model, mixin } from 'objection'; import { Model, mixin } from 'objection';
import moment from 'moment'; import moment from 'moment';
import SystemModel from 'system/models/SystemModel'; import SystemModel from 'system/models/SystemModel';
import { ILicensesFilter } from 'interfaces';
export default class License extends SystemModel { export default class License extends SystemModel {
/** /**
@@ -25,8 +24,8 @@ export default class License extends SystemModel {
return { return {
// Filters active licenses. // Filters active licenses.
filterActiveLicense(query) { filterActiveLicense(query) {
query.where('disabled', false); query.where('disabled_at', null);
query.where('used', false); query.where('used_at', null);
}, },
// Find license by its code or id. // Find license by its code or id.
@@ -45,13 +44,13 @@ export default class License extends SystemModel {
builder.modify('filterActiveLicense') builder.modify('filterActiveLicense')
} }
if (licensesFilter.disabled) { if (licensesFilter.disabled) {
builder.where('disabled', true); builder.whereNot('disabled_at', null);
} }
if (licensesFilter.used) { if (licensesFilter.used) {
builder.where('used', true); builder.whereNot('used_at', null);
} }
if (licensesFilter.sent) { 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() return this.query()
.where(viaAttribute, licenseCode) .where(viaAttribute, licenseCode)
.patch({ .patch({
disabled: true,
disabled_at: moment().toMySqlDateTime(), disabled_at: moment().toMySqlDateTime(),
}); });
} }
@@ -108,7 +106,6 @@ export default class License extends SystemModel {
return this.query() return this.query()
.where(viaAttribute, licenseCode) .where(viaAttribute, licenseCode)
.patch({ .patch({
sent: true,
sent_at: moment().toMySqlDateTime(), sent_at: moment().toMySqlDateTime(),
}); });
} }
@@ -122,7 +119,6 @@ export default class License extends SystemModel {
return this.query() return this.query()
.where(viaAttribute, licenseCode) .where(viaAttribute, licenseCode)
.patch({ .patch({
used: true,
used_at: moment().toMySqlDateTime() used_at: moment().toMySqlDateTime()
}); });
} }
@@ -136,5 +132,4 @@ export default class License extends SystemModel {
return (this.invoicePeriod === plan.invoiceInterval && return (this.invoicePeriod === plan.invoiceInterval &&
license.licensePeriod === license.periodInterval); license.licensePeriod === license.periodInterval);
} }
} }