Skip to content

Commit

Permalink
Merge pull request #616 from bigcapitalhq/one-click-demo-account
Browse files Browse the repository at this point in the history
feat(ee): One-click demo account
  • Loading branch information
abouolia authored Aug 22, 2024
2 parents f46cd28 + d15fb6f commit 07740a5
Show file tree
Hide file tree
Showing 53 changed files with 1,311 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
import config from '@/config';
@Service()
export class OneClickDemoController extends BaseController {
@Inject()
private oneClickDemoApp: OneClickDemoApplication;

/**
* Router constructor method.
*/
router() {
const router = Router();

// Protects the endpoints if the feature is not enabled.
const protectMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Add your protection logic here
if (config.oneClickDemoAccounts) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
};
router.post(
'/one_click',
protectMiddleware,
asyncMiddleware(this.oneClickDemo.bind(this))
);
router.post(
'/one_click_signin',
[body('demo_id').exists()],
this.validationResult,
protectMiddleware,
asyncMiddleware(this.oneClickSignIn.bind(this))
);
return router;
}

/**
* One-click demo application.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
try {
const data = await this.oneClickDemoApp.createOneClick();

return res.status(200).send({
data,
message: 'The one-click demo has been created successfully.',
});
} catch (error) {
next(error);
}
}

/**
* Sign-in to one-click demo account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async oneClickSignIn(
req: Request,
res: Response,
next: NextFunction
) {
const { demoId } = this.matchedBodyData(req);

try {
const data = await this.oneClickDemoApp.autoSignIn(demoId);

return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}
2 changes: 2 additions & 0 deletions packages/server/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';

export default () => {
const app = Router();
Expand All @@ -80,6 +81,7 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())

// - Dashboard routes.
// ---------------------------
Expand Down
23 changes: 14 additions & 9 deletions packages/server/src/api/middleware/SettingsMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import SettingsStore from '@/services/Settings/SettingsStore';

export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;

const Logger = Container.get('logger');
const settings = await initializeTenantSettings(tenantId);
req.settings = settings;

res.on('finish', async () => {
await settings.save();
});
next();
}


export const initializeTenantSettings = async (tenantId: number) => {
const tenantContainer = Container.of(`tenant-${tenantId}`);

if (tenantContainer && !tenantContainer.has('settings')) {
Expand All @@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {

await settings.load();

req.settings = settings;

res.on('finish', async () => {
await settings.save();
});
next();
return settings;
}
28 changes: 25 additions & 3 deletions packages/server/src/api/middleware/TenantDependencyInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
import { Tenant } from '@/system/models';

export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant;
Expand All @@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => {

const tenantContainer = tenantServices.tenantContainer(tenantId);

tenantContainer.set('i18n', injectI18nUtils(req));
tenantContainer.set('i18n', injectI18nUtils());

const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId);
Expand All @@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => {
};

export const injectI18nUtils = (req) => {
const locale = req.getLocale();
const globalI18n = Container.get('i18n');
const locale = globalI18n.getLocale();
const direction = rtlDetect.getLangDir(locale);

return {
locale,
__: req.__,
__: globalI18n.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
};
};

export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');

const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);

// Initialize the knex instance.
tenantsManager.setupKnexInstance(tenant);

const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());

tenantServices.knex(tenantId);
tenantServices.models(tenantId);
tenantServices.repositories(tenantId);
tenantServices.cache(tenantId);
};
5 changes: 5 additions & 0 deletions packages/server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,9 @@ module.exports = {
loops: {
apiKey: process.env.LOOPS_API_KEY,
},

oneClickDemoAccounts: parseBoolean(
process.env.ONE_CLICK_DEMO_ACCOUNTS,
false
),
};
1 change: 0 additions & 1 deletion packages/server/src/interfaces/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface IRegisterDTO {
lastName: string;
email: string;
password: string;
organizationName: string;
}

export interface ILoginDTO {
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/interfaces/Setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload {
buildDTO: IOrganizationBuildDTO;
systemUser: ISystemUser;
}

export interface IOrganizationBuiltEventPayload {
tenantId: number;
}
4 changes: 4 additions & 0 deletions packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashfl
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';

export default () => {
return new EventPublisher();
Expand Down Expand Up @@ -282,5 +283,8 @@ export const susbcribers = () => {

// Loops
LoopsEventsSubscriber

// Demo Account
SeedInitialDemoAccountDataOnOrgBuild
];
};
1 change: 1 addition & 0 deletions packages/server/src/loaders/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { I18n } from 'i18n';

export default () => new I18n({
locales: ['en', 'ar'],
defaultLocale: 'en',
register: global,
directory: global.__locales_dir,
updateFiles: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export class TriggerRecognizedTransactions {
const batch = importFile.paramsParsed.batch;
const payload = { tenantId, transactionsCriteria: { batch } };

// Cannot continue if the imported resource is not bank account transactions.
if (importFile.resource !== 'UncategorizedCashflowTransaction') return;

await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
}

/**
* Transformes the import params before storing them.
* Transforms the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/services/Import/ImportFileMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class ImportFileMapping {
*/
public async mapping(
tenantId: number,
importId: number,
importId: string,
maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> {
const importFile = await Import.query()
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/services/Import/ImportFilePreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class ImportFilePreview {
*/
public async preview(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/services/Import/ImportFileProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ImportFileProcess {
*/
public async import(
tenantId: number,
importId: number,
importId: string,
trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ImportFileProcessCommit {
*/
public async commit(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class ImportResourceApplication {
*/
public async mapping(
tenantId: number,
importId: number,
importId: string,
maps: ImportMappingAttr[]
) {
return this.importMappingService.mapping(tenantId, importId, maps);
Expand All @@ -67,7 +67,7 @@ export class ImportResourceApplication {
* @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(tenantId: number, importId: number) {
public async preview(tenantId: number, importId: string) {
return this.ImportFilePreviewService.preview(tenantId, importId);
}

Expand All @@ -77,7 +77,7 @@ export class ImportResourceApplication {
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async process(tenantId: number, importId: number) {
public async process(tenantId: number, importId: string) {
return this.importProcessCommit.commit(tenantId, importId);
}

Expand Down
6 changes: 4 additions & 2 deletions packages/server/src/services/Items/ItemsEntriesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models';
import { entriesAmountDiff } from 'utils';
import { Knex } from 'knex';

const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
Expand Down Expand Up @@ -58,13 +59,14 @@ export default class ItemsEntriesService {
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[]
entries: IItemEntry[],
trx?: Knex.Transaction
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);

// Retrieve entries inventory items.
const inventoryItems = await Item.query()
const inventoryItems = await Item.query(trx)
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');

Expand Down
Loading

0 comments on commit 07740a5

Please sign in to comment.