mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 04:10:32 +00:00
feat: ensure organization tenant configured.
This commit is contained in:
20
README.md
20
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
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
'/',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
12
server/src/api/middleware/EnsureConfiguredMiddleware.ts
Normal file
12
server/src/api/middleware/EnsureConfiguredMiddleware.ts
Normal 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();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
19
server/src/api/middleware/EnsureTenantIsSeeded.ts
Normal file
19
server/src/api/middleware/EnsureTenantIsSeeded.ts
Normal 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();
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -14,6 +14,10 @@ export interface ISystemUser {
|
||||
|
||||
inviteAcceptAt: Date,
|
||||
lastLoginAt: Date,
|
||||
deletedAt: Date,
|
||||
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
}
|
||||
|
||||
export interface ISystemUserDTO {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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}`, () => {
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user