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 { 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))

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 { 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(
'/',

View File

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

View File

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

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' }] },
);
}
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

@@ -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',
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,
},
],
};

View File

@@ -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,
};
};
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,
lastLoginAt: Date,
deletedAt: Date,
createdAt: Date,
updatedAt: Date,
}
export interface ISystemUserDTO {

View File

@@ -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<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() {
const resourceId = 1;
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';
@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<ISystemUser> {
public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
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<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email);
@@ -176,7 +176,7 @@ export default class AuthenticationService {
* @param {string} email
* @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.');
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<void>}
*/
async resetPassword(token: string, password: string): Promise<void> {
public async resetPassword(token: string, password: string): Promise<void> {
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.');

View File

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

View File

@@ -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<void> {
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<void> {
const message: string = `
Your remaining free trial is ${remainingDays} days,
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.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');

View File

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