add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
import {
ReportsAction,
ISubjectAbilitiesSchema,
ISubjectAbilitySchema,
AbilitySubject,
ManualJournalAction,
AccountAction,
SaleInvoiceAction,
ItemAction,
VendorAction,
CustomerAction,
ExpenseAction,
PaymentReceiveAction,
InventoryAdjustmentAction,
SaleEstimateAction,
BillAction,
SaleReceiptAction,
IPaymentMadeAction,
CashflowAction,
PreferencesAction,
CreditNoteAction,
VendorCreditAction,
} from '@/interfaces';
export const AbilitySchema: ISubjectAbilitiesSchema[] = [
{
subject: AbilitySubject.Account,
subjectLabel: 'ability.accounts',
abilities: [
{ key: AccountAction.VIEW, label: 'ability.view' },
{ key: AccountAction.CREATE, label: 'ability.create' },
{ key: AccountAction.EDIT, label: 'ability.edit' },
{ key: AccountAction.DELETE, label: 'ability.delete' },
],
extraAbilities: [
{
key: AccountAction.TransactionsLocking,
label: 'ability.transactions_locking',
},
],
},
{
subject: AbilitySubject.ManualJournal,
subjectLabel: 'ability.manual_journal',
abilities: [
{ key: ManualJournalAction.View, label: 'ability.view' },
{ key: ManualJournalAction.Create, label: 'ability.create' },
{ key: ManualJournalAction.Edit, label: 'ability.edit' },
{ key: ManualJournalAction.Delete, label: 'ability.delete' },
],
},
{
subject: AbilitySubject.Cashflow,
subjectLabel: 'ability.cashflow',
abilities: [
{ key: CashflowAction.View, label: 'ability.view' },
{ key: CashflowAction.Create, label: 'ability.create' },
{ key: CashflowAction.Delete, label: 'ability.delete' },
],
},
{
subject: AbilitySubject.Item,
subjectLabel: 'ability.items',
abilities: [
{ key: ItemAction.VIEW, label: 'ability.view', default: true },
{ key: ItemAction.CREATE, label: 'ability.create', default: true },
{ key: ItemAction.EDIT, label: 'ability.edit', default: true },
{ key: ItemAction.DELETE, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.InventoryAdjustment,
subjectLabel: 'ability.inventory_adjustment',
abilities: [
{
key: InventoryAdjustmentAction.VIEW,
label: 'ability.view',
default: true,
},
{
key: InventoryAdjustmentAction.CREATE,
label: 'ability.create',
default: true,
},
{
key: InventoryAdjustmentAction.EDIT,
label: 'ability.edit',
default: true,
},
{ key: InventoryAdjustmentAction.DELETE, label: 'ability.delete' },
],
},
{
subject: AbilitySubject.Customer,
subjectLabel: 'ability.customers',
// description: 'Description is here',
abilities: [
{ key: CustomerAction.View, label: 'ability.view', default: true },
{ key: CustomerAction.Create, label: 'ability.create', default: true },
{ key: CustomerAction.Edit, label: 'ability.edit', default: true },
{ key: CustomerAction.Delete, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.Vendor,
subjectLabel: 'ability.vendors',
abilities: [
{ key: VendorAction.View, label: 'ability.view', default: true },
{ key: VendorAction.Create, label: 'ability.create', default: true },
{ key: VendorAction.Edit, label: 'ability.edit', default: true },
{ key: VendorAction.Delete, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.SaleEstimate,
subjectLabel: 'ability.sale_estimates',
abilities: [
{ key: SaleEstimateAction.View, label: 'ability.view', default: true },
{
key: SaleEstimateAction.Create,
label: 'ability.create',
default: true,
},
{ key: SaleEstimateAction.Edit, label: 'ability.edit', default: true },
{
key: SaleEstimateAction.Delete,
label: 'ability.delete',
default: true,
},
],
},
{
subject: AbilitySubject.SaleInvoice,
subjectLabel: 'ability.sale_invoices',
abilities: [
{ key: SaleInvoiceAction.View, label: 'ability.view', default: true },
{ key: SaleInvoiceAction.Create, label: 'ability.create', default: true },
{ key: SaleInvoiceAction.Edit, label: 'ability.edit', default: true },
{ key: SaleInvoiceAction.Delete, label: 'ability.delete', default: true },
],
extraAbilities: [{ key: 'bad-debt', label: 'Due amount to bad debit' }],
},
{
subject: AbilitySubject.SaleReceipt,
subjectLabel: 'ability.sale_receipts',
abilities: [
{ key: SaleReceiptAction.View, label: 'ability.view', default: true },
{ key: SaleReceiptAction.Create, label: 'ability.create', default: true },
{ key: SaleReceiptAction.Edit, label: 'ability.edit', default: true },
{ key: SaleReceiptAction.Delete, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.CreditNote,
subjectLabel: 'ability.credit_note',
abilities: [
{ key: CreditNoteAction.View, label: 'ability.view', default: true },
{ key: CreditNoteAction.Create, label: 'ability.create', default: true },
{ key: CreditNoteAction.Edit, label: 'ability.edit', default: true },
{ key: CreditNoteAction.Delete, label: 'ability.delete', default: true },
{ key: CreditNoteAction.Refund, label: 'ability.refund', default: true },
],
},
{
subject: AbilitySubject.PaymentReceive,
subjectLabel: 'ability.payments_receive',
abilities: [
{ key: PaymentReceiveAction.View, label: 'ability.view', default: true },
{
key: PaymentReceiveAction.Create,
label: 'ability.create',
default: true,
},
{ key: PaymentReceiveAction.Edit, label: 'ability.edit', default: true },
{
key: PaymentReceiveAction.Delete,
label: 'ability.delete',
default: true,
},
],
},
{
subject: AbilitySubject.Bill,
subjectLabel: 'ability.purchase_invoices',
abilities: [
{ key: BillAction.View, label: 'ability.view', default: true },
{ key: BillAction.Create, label: 'ability.create', default: true },
{ key: BillAction.Edit, label: 'ability.edit', default: true },
{ key: BillAction.Delete, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.VendorCredit,
subjectLabel: 'ability.vendor_credit',
abilities: [
{ key: VendorCreditAction.View, label: 'ability.view', default: true },
{
key: VendorCreditAction.Create,
label: 'ability.create',
default: true,
},
{ key: VendorCreditAction.Edit, label: 'ability.edit', default: true },
{
key: VendorCreditAction.Delete,
label: 'ability.delete',
default: true,
},
{
key: VendorCreditAction.Refund,
label: 'ability.refund',
default: true,
},
],
},
{
subject: AbilitySubject.PaymentMade,
subjectLabel: 'ability.payments_made',
abilities: [
{ key: IPaymentMadeAction.View, label: 'ability.view', default: true },
{
key: IPaymentMadeAction.Create,
label: 'ability.create',
default: true,
},
{ key: IPaymentMadeAction.Edit, label: 'ability.edit', default: true },
{
key: IPaymentMadeAction.Delete,
label: 'ability.delete',
default: true,
},
],
},
{
subject: AbilitySubject.Expense,
subjectLabel: 'ability.expenses',
abilities: [
{ key: ExpenseAction.View, label: 'ability.view', default: true },
{ key: ExpenseAction.Create, label: 'ability.create', default: true },
{ key: ExpenseAction.Edit, label: 'ability.edit', default: true },
{ key: ExpenseAction.Delete, label: 'ability.delete', default: true },
],
},
{
subject: AbilitySubject.Report,
subjectLabel: 'ability.all_reports',
extraAbilities: [
{
key: ReportsAction.READ_BALANCE_SHEET,
label: 'ability.balance_sheet_report',
},
{
key: ReportsAction.READ_PROFIT_LOSS,
label: 'ability.profit_loss_sheet',
},
{ key: ReportsAction.READ_JOURNAL, label: 'ability.journal' },
{
key: ReportsAction.READ_GENERAL_LEDGET,
label: 'ability.general_ledger',
},
{ key: ReportsAction.READ_CASHFLOW, label: 'ability.cashflow_report' },
{
key: ReportsAction.READ_AR_AGING_SUMMARY,
label: 'ability.AR_aging_summary_report',
},
{
key: ReportsAction.READ_AP_AGING_SUMMARY,
label: 'ability.AP_aging_summary_report',
},
{
key: ReportsAction.READ_PURCHASES_BY_ITEMS,
label: 'ability.purchases_by_items',
},
{
key: ReportsAction.READ_SALES_BY_ITEMS,
label: 'ability.sales_by_items_report',
},
{
key: ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
label: 'ability.customers_transactions_report',
},
{
key: ReportsAction.READ_VENDORS_TRANSACTIONS,
label: 'ability.vendors_transactions_report',
},
{
key: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
label: 'ability.customers_summary_balance_report',
},
{
key: ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
label: 'ability.vendors_summary_balance_report',
},
{
key: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
label: 'ability.inventory_valuation_summary',
},
{
key: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
label: 'ability.inventory_items_details',
},
],
},
{
subject: AbilitySubject.Preferences,
subjectLabel: 'ability.preferences',
extraAbilities: [
{
key: PreferencesAction.Mutate,
label: 'ability.mutate_system_preferences',
},
],
},
];
/**
* Retrieve the permissions subject.
* @param {string} key
* @returns {ISubjectAbilitiesSchema | null}
*/
export const getPermissionsSubject = (
key: string
): ISubjectAbilitiesSchema | null => {
return AbilitySchema.find((subject) => subject.subject === key);
};
/**
* Retrieve the permission subject ability.
* @param {String} subjectKey
* @param {string} abilityKey
* @returns
*/
export const getPermissionAbility = (
subjectKey: string,
abilityKey: string
): ISubjectAbilitySchema | null => {
const subject = getPermissionsSubject(subjectKey);
return subject?.abilities.find((ability) => ability.key === abilityKey);
};

View File

@@ -0,0 +1,22 @@
import { Service } from 'typedi';
import events from '@/subscribers/events';
import { ABILITIES_CACHE } from '../../api/middleware/AuthorizationMiddleware';
@Service()
export default class PurgeAuthorizedUserOnceRoleMutate {
/**
* Attaches events with handlers.
* @param bus
*/
attach(bus) {
bus.subscribe(events.roles.onEdited, this.purgeAuthedUserOnceRoleMutated);
bus.subscribe(events.roles.onDeleted, this.purgeAuthedUserOnceRoleMutated);
}
/**
* Purges authorized user once role edited or deleted.
*/
purgeAuthedUserOnceRoleMutated({}) {
ABILITIES_CACHE.reset();
}
}

View File

@@ -0,0 +1,31 @@
import { Service, Inject } from 'typedi';
import * as qim from 'qim';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AbilitySchema } from './AbilitySchema';
import I18nService from '@/services/I18n/I18nService';
@Service()
export default class RolePermissionsSchema {
@Inject()
tenancy: HasTenancyService;
@Inject()
i18nService: I18nService;
/**
* Retrieve the role permissions schema.
* @param {number} tenantId
*/
getRolePermissionsSchema(tenantId: number) {
const $abilities = (f) => (f.abilities ? f : undefined);
const $extraAbilities = (f) => (f.extraAbilities ? f : undefined);
const naviagations = [
[qim.$each, 'subjectLabel'],
[qim.$each, $abilities, 'abilities', qim.$each, 'label'],
[qim.$each, $extraAbilities, 'extraAbilities', qim.$each, 'label'],
];
return this.i18nService.i18nApply(naviagations, AbilitySchema, tenantId);
}
}

View File

@@ -0,0 +1,31 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class RoleTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['name', 'description'];
};
/**
*
* @param role
* @returns
*/
public name(role) {
return role.predefined ? this.context.i18n.__(role.name) : role.name;
}
/**
*
* @param role
* @returns
*/
public description(role) {
return role.predefined
? this.context.i18n.__(role.description)
: role.description;
}
}

View File

@@ -0,0 +1,291 @@
import { Service, Inject } from 'typedi';
import Knex from 'knex';
import {
ICreateRoleDTO,
ICreateRolePermissionDTO,
IEditRoleDTO,
IEditRolePermissionDTO,
IRole,
IRoleCreatedPayload,
IRoleDeletedPayload,
IRoleEditedPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { AbilitySchema } from './AbilitySchema';
import { getInvalidPermissions } from './utils';
import UnitOfWork from '@/services/UnitOfWork';
import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { RoleTransformer } from './RoleTransformer';
@Service()
export default class RolesService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private transformer: TransformerInjectable;
/**
* Creates a new role and store it to the storage.
* @param {number} tenantId
* @param {ICreateRoleDTO} createRoleDTO
* @returns
*/
public createRole = async (
tenantId: number,
createRoleDTO: ICreateRoleDTO
) => {
const { Role } = this.tenancy.models(tenantId);
// Validates the invalid permissions.
this.validateInvalidPermissions(createRoleDTO.permissions);
// Transformes the permissions DTO.
const permissions = this.tranaformPermissionsDTO(createRoleDTO.permissions);
// Creates a new role with associated entries under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Creates a new role to the storage.
const role = await Role.query(trx).upsertGraph({
name: createRoleDTO.roleName,
description: createRoleDTO.roleDescription,
permissions,
});
// Triggers `onRoleCreated` event.
await this.eventPublisher.emitAsync(events.roles.onCreated, {
tenantId,
createRoleDTO,
role,
trx,
} as IRoleCreatedPayload);
return role;
});
};
/**
* Edits details of the given role on the storage.
* @param {number} tenantId -
* @param {number} roleId -
* @param {IEditRoleDTO} editRoleDTO - Edit role DTO.
*/
public editRole = async (
tenantId: number,
roleId: number,
editRoleDTO: IEditRoleDTO
) => {
const { Role } = this.tenancy.models(tenantId);
// Validates the invalid permissions.
this.validateInvalidPermissions(editRoleDTO.permissions);
// Retrieve the given role or throw not found serice error.
const oldRole = await this.getRoleOrThrowError(tenantId, roleId);
const permissions = this.tranaformEditPermissionsDTO(
editRoleDTO.permissions
);
// Updates the role on the storage.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Updates the given role to the storage.
const role = await Role.query(trx).upsertGraph({
id: roleId,
name: editRoleDTO.roleName,
description: editRoleDTO.roleDescription,
permissions,
});
// Triggers `onRoleEdited` event.
await this.eventPublisher.emitAsync(events.roles.onEdited, {
editRoleDTO,
oldRole,
role,
trx,
} as IRoleEditedPayload);
return role;
});
};
/**
* Retrieve the role or throw not found service error.
* @param {number} tenantId
* @param {number} roleId
* @returns {Promise<IRole>}
*/
public getRoleOrThrowError = async (
tenantId: number,
roleId: number
): Promise<IRole> => {
const { Role } = this.tenancy.models(tenantId);
const role = await Role.query().findById(roleId);
this.throwRoleNotFound(role);
return role;
};
/**
* Throw role not found service error if the role is not found.
* @param {IRole|null} role
*/
private throwRoleNotFound(role: IRole | null) {
if (!role) {
throw new ServiceError(ERRORS.ROLE_NOT_FOUND);
}
}
/**
* Deletes the given role from the storage.
* @param {number} tenantId -
* @param {number} roleId - Role id.
*/
public deleteRole = async (
tenantId: number,
roleId: number
): Promise<void> => {
const { Role, RolePermission } = this.tenancy.models(tenantId);
// Retrieve the given role or throw not found serice error.
const oldRole = await this.getRoleOrThrowError(tenantId, roleId);
// Validate role is not predefined.
this.validateRoleNotPredefined(oldRole);
// Validates the given role is not associated to any user.
await this.validateRoleNotAssociatedToUser(tenantId, roleId);
// Deletes the given role and associated models under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Deletes the role associated permissions from the storage.
await RolePermission.query(trx).where('roleId', roleId).delete();
// Deletes the role object form the storage.
await Role.query(trx).findById(roleId).delete();
// Triggers `onRoleDeleted` event.
await this.eventPublisher.emitAsync(events.roles.onDeleted, {
oldRole,
roleId,
tenantId,
trx,
} as IRoleDeletedPayload);
});
};
/**
* Retrieve the roles list.
* @param {number} tenantId
* @param {Promise<IRole[]>}
*/
public listRoles = async (tenantId: number): Promise<IRole[]> => {
const { Role } = this.tenancy.models(tenantId);
const roles = await Role.query().withGraphFetched('permissions');
return this.transformer.transform(tenantId, roles, new RoleTransformer());
};
/**
* Retrieve the given role metadata.
* @param {number} tenantId
* @param {number} roleId - Role id.
* @returns {Promise<IRole>}
*/
public getRole = async (tenantId: number, roleId: number): Promise<IRole> => {
const { Role } = this.tenancy.models(tenantId);
const role = await Role.query()
.findById(roleId)
.withGraphFetched('permissions');
this.throwRoleNotFound(role);
return this.transformer.transform(tenantId, role, new RoleTransformer());
};
/**
* Valdiates role is not predefined.
* @param {IRole} role - Role object.
*/
private validateRoleNotPredefined(role: IRole) {
if (role.predefined) {
throw new ServiceError(ERRORS.ROLE_PREFINED);
}
}
/**
* Validates the invalid given permissions.
* @param {ICreateRolePermissionDTO[]} permissions -
*/
public validateInvalidPermissions = (
permissions: ICreateRolePermissionDTO[]
) => {
const invalidPerms = getInvalidPermissions(AbilitySchema, permissions);
if (invalidPerms.length > 0) {
throw new ServiceError(ERRORS.INVALIDATE_PERMISSIONS, null, {
invalidPermissions: invalidPerms,
});
}
};
/**
* Transformes new permissions DTO.
* @param {ICreateRolePermissionDTO[]} permissions
* @returns {ICreateRolePermissionDTO[]}
*/
private tranaformPermissionsDTO = (
permissions: ICreateRolePermissionDTO[]
) => {
return permissions.map((permission: ICreateRolePermissionDTO) => ({
subject: permission.subject,
ability: permission.ability,
value: permission.value,
}));
};
/**
* Transformes edit permissions DTO.
* @param {ICreateRolePermissionDTO[]} permissions
* @returns {IEditRolePermissionDTO[]}
*/
private tranaformEditPermissionsDTO = (
permissions: IEditRolePermissionDTO[]
) => {
return permissions.map((permission: IEditRolePermissionDTO) => ({
permissionId: permission.permissionId,
subject: permission.subject,
ability: permission.ability,
value: permission.value,
}));
};
/**
* Validates the given role is not associated to any tenant users.
* @param {number} tenantId
* @param {number} roleId
*/
private validateRoleNotAssociatedToUser = async (
tenantId: number,
roleId: number
) => {
const { User } = this.tenancy.models(tenantId);
const userAssociatedRole = await User.query().where('roleId', roleId);
// Throw service error if the role has associated users.
if (userAssociatedRole.length > 0) {
throw new ServiceError(ERRORS.CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS);
}
};
}

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
ROLE_NOT_FOUND: 'ROLE_NOT_FOUND',
ROLE_PREFINED: 'ROLE_PREFINED',
INVALIDATE_PERMISSIONS: 'INVALIDATE_PERMISSIONS',
CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS: 'CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS'
};

View File

@@ -0,0 +1,42 @@
import { keyBy } from 'lodash';
import { ISubjectAbilitiesSchema } from '@/interfaces';
/**
* Transformes ability schema to map.
*/
export function transformAbilitySchemaToMap(schema: ISubjectAbilitiesSchema[]) {
return keyBy(
schema.map((item) => ({
...item,
abilities: keyBy(item.abilities, 'key'),
extraAbilities: keyBy(item.extraAbilities, 'key'),
})),
'subject'
);
}
/**
* Retrieve the invalid permissions from the given defined schema.
* @param {ISubjectAbilitiesSchema[]} schema
* @param permissions
* @returns
*/
export function getInvalidPermissions(
schema: ISubjectAbilitiesSchema[],
permissions
) {
const schemaMap = transformAbilitySchemaToMap(schema);
return permissions.filter((permission) => {
const { subject, ability } = permission;
if (
!schemaMap[subject] ||
(!schemaMap[subject].abilities[ability] &&
!schemaMap[subject].extraAbilities[ability])
) {
return true;
}
return false;
});
}