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,23 @@
import { Request, Response } from 'express';
const asyncRender = (app) => (path: string, attributes = {}) =>
new Promise((resolve, reject) => {
app.render(path, attributes, (error, data) => {
if (error) { reject(error); }
resolve(data);
});
});
/**
* Injects `asyncRender` method to response object.
* @param {Request} req Express req Object
* @param {Response} res Express res Object
* @param {NextFunction} next Express next Function
*/
const asyncRenderMiddleware = (req: Request, res: Response, next: Function) => {
res.asyncRender = asyncRender(req.app);
next();
};
export default asyncRenderMiddleware;

View File

@@ -0,0 +1,39 @@
import { Container } from 'typedi';
import { Request, Response } from 'express';
/**
* Attach user to req.currentUser
* @param {Request} req Express req Object
* @param {Response} res Express res Object
* @param {NextFunction} next Express next Function
*/
const attachCurrentUser = async (req: Request, res: Response, next: Function) => {
const Logger = Container.get('logger');
const { systemUserRepository } = Container.get('repositories');
try {
Logger.info('[attach_user_middleware] finding system user by id.');
const user = await systemUserRepository.findOneById(req.token.id);
if (!user) {
Logger.info('[attach_user_middleware] the system user not found.');
return res.boom.unauthorized();
}
if (!user.active) {
Logger.info('[attach_user_middleware] the system user not found.');
return res.boom.badRequest(
'The authorized user is inactivated.',
{ errors: [{ type: 'USER_INACTIVE', code: 100, }] },
);
}
// Delete password property from user object.
Reflect.deleteProperty(user, 'password');
req.user = user;
return next();
} catch (e) {
Logger.error('[attach_user_middleware] error attaching user to req: %o', e);
return next(e);
}
};
export default attachCurrentUser;

View File

@@ -0,0 +1,92 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Ability } from '@casl/ability';
import LruCache from 'lru-cache';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { IRole, IRolePremission, ISystemUser } from '@/interfaces';
// store abilities of 1000 most active users
export const ABILITIES_CACHE = new LruCache(1000);
/**
* Retrieve ability for the given role.
* @param {} role
* @returns
*/
function getAbilityForRole(role) {
const rules = getAbilitiesRolesConds(role);
return new Ability(rules);
}
/**
* Retrieve abilities of the given role.
* @param {IRole} role
* @returns {}
*/
function getAbilitiesRolesConds(role: IRole) {
switch (role.slug) {
case 'admin': // predefined role.
return getSuperAdminRules();
default:
return getRulesFromRolePermissions(role.permissions || []);
}
}
/**
* Retrieve the super admin rules.
* @returns {}
*/
function getSuperAdminRules() {
return [{ action: 'manage', subject: 'all' }];
}
/**
* Retrieve CASL rules from role permissions.
* @param {IRolePremission[]} permissions -
* @returns {}
*/
function getRulesFromRolePermissions(permissions: IRolePremission[]) {
return permissions
.filter((permission: IRolePremission) => permission.value)
.map((permission: IRolePremission) => {
return {
action: permission.ability,
subject: permission.subject,
};
});
}
/**
* Retrieve ability for user.
* @param {ISystemUser} user
* @param {number} tenantId
* @returns {}
*/
async function getAbilityForUser(user: ISystemUser, tenantId: number) {
const tenancy = Container.get(HasTenancyService);
const { User } = tenancy.models(tenantId);
const tenantUser = await User.query()
.findOne('systemUserId', user.id)
.withGraphFetched('role.permissions');
return getAbilityForRole(tenantUser.role);
}
/**
*
* @param {Request} request -
* @param {Response} response -
* @param {NextFunction} next -
*/
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId, user } = req;
if (ABILITIES_CACHE.has(req.user.id)) {
req.ability = ABILITIES_CACHE.get(req.user.id);
} else {
req.ability = await getAbilityForUser(req.user, tenantId);
ABILITIES_CACHE.set(req.user.id, req.ability);
}
next();
};

View File

@@ -0,0 +1,18 @@
import { Request, Response, NextFunction } from 'express';
import { ForbiddenError } from '@casl/ability';
/**
*
*/
export default (ability: string, subject: string) =>
(req: Request, res: Response, next: NextFunction) => {
try {
ForbiddenError.from(req.ability).throwUnlessCan(ability, subject);
} catch (error) {
return res.status(403).send({
type: 'USER_PERMISSIONS_FORBIDDEN',
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
});
}
next();
};

View File

@@ -0,0 +1,13 @@
import { Request, Response, NextFunction } from 'express';
import deepMap from 'deep-map';
import { convertEmptyStringToNull } from 'utils';
function convertEmptyStringsToNull(data) {
return deepMap(data, (value) => convertEmptyStringToNull(value));
}
export default (req: Request, res: Response, next: NextFunction) => {
const transfomedBody = convertEmptyStringsToNull(req.body);
req.body = transfomedBody;
next();
};

View File

@@ -0,0 +1,21 @@
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.initializedAt) {
Logger.info('[ensure_tenant_initialized_middleware] tenant database not initalized.');
return res.boom.badRequest(
'Tenant database is not migrated with application schema yut.',
{ errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }] },
);
}
next();
};

View File

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,18 @@
import { Request, Response } from 'express';
import { Features } from '@/interfaces';
export const FeatureActivationGuard =
(feature: Features) => (req: Request, res: Response, next: Function) => {
const { settings } = req;
const isActivated = settings.get({ group: 'features', key: feature });
if (!isActivated) {
return res.status(400).send({
errors: [
{ type: 'FEATURE_NOT_ACTIVATED', code: 20, payload: { feature } },
],
});
}
next();
};

View File

@@ -0,0 +1,37 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { injectI18nUtils } from './TenantDependencyInjection';
/**
* I18n from organization settings.
*/
export default (req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger');
const I18n = Container.get('i18n');
const { tenantId, tenant } = req;
if (!req.user) {
throw new Error('Should load this middleware after `JWTAuth`.');
}
if (!req.settings) {
throw new Error('Should load this middleware after `SettingsMiddleware`.');
}
// Get the organization language from settings.
const { language } = tenant.metadata;
if (language) {
I18n.setLocale(req, language);
}
Logger.info('[i18n_authenticated_middleware] set locale language to i18n.', {
language,
user: req.user,
});
const tenantServices = Container.get(HasTenancyService);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils(req));
next();
};

View File

@@ -0,0 +1,22 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import { lowerCase } from 'lodash';
/**
* Set the language from request `accept-language` header
* or default application language.
*/
export default (req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger');
const I18n = Container.get('i18n');
// Parses the accepted language from request object.
const language = lowerCase(req.headers['accept-language']) || 'en';
Logger.info('[i18n_middleware] set locale language to i18n.', {
language,
user: req.user,
});
// Initialise the global localization.
I18n.init(req, res, next);
};

View File

@@ -0,0 +1,37 @@
import { snakeCase } from 'lodash';
import { mapKeysDeep } from 'utils';
/**
* Express middleware for intercepting and transforming json responses
*
* @param {function} [condition] - takes the req and res and returns a boolean indicating whether to run the transform on this response
* @param {function} transform - takes an object passed to res.json and returns a replacement object
* @return {function} the middleware
*/
export function JSONResponseTransformer(transform: Function) {
const replaceJson = (res) => {
var origJson = res.json;
res.json = function (val) {
const json = JSON.parse(JSON.stringify(val));
return origJson.call(res, transform(json));
};
};
return function (req, res, next) {
replaceJson(res);
next();
};
}
/**
* Transformes the given response keys to snake case.
* @param response
* @returns
*/
export const snakecaseResponseTransformer = (response) => {
return mapKeysDeep(response, (value, key) => {
return snakeCase(key);
});
};

View File

@@ -0,0 +1,11 @@
import { NextFunction, Request } from 'express';
import { Container } from 'typedi';
function loggerMiddleware(request: Request, response: Response, next: NextFunction) {
const Logger = Container.get('logger');
Logger.info(`[routes] ${request.method} ${request.path}`);
next();
}
export default loggerMiddleware;

View File

@@ -0,0 +1,24 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import config from '@/config';
const MAX_CONSECUTIVE_FAILS = config.throttler.login.points;
export default async (req: Request, res: Response, next: NextFunction) => {
const { crediential } = req.body;
const loginThrottler = Container.get('rateLimiter.login');
// Retrieve the rate limiter response of the given crediential.
const emailRateRes = await loginThrottler.get(crediential);
if (emailRateRes !== null && emailRateRes.consumedPoints >= MAX_CONSECUTIVE_FAILS) {
const retrySecs = Math.round(emailRateRes.msBeforeNext / 1000) || 1;
res.set('Retry-After', retrySecs);
res.status(429).send({
errors: [{ type: 'LOGIN_TO_MANY_ATTEMPTS', code: 400 }],
});
} else {
next();
}
}

View File

@@ -0,0 +1,113 @@
import { Request, Response, NextFunction } from 'express';
import {
ValidationError,
NotFoundError,
DBError,
UniqueViolationError,
NotNullViolationError,
ForeignKeyViolationError,
CheckViolationError,
DataError,
} from 'objection';
// In this example `res` is an express response object.
export default function ObjectionErrorHandlerMiddleware(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof ValidationError) {
switch (err.type) {
case 'ModelValidation':
return res.status(400).send({
message: err.message,
type: err.type,
data: err.data,
});
case 'RelationExpression':
return res.status(400).send({
message: err.message,
type: 'RelationExpression',
data: {},
});
case 'UnallowedRelation':
return res.status(400).send({
message: err.message,
type: err.type,
data: {},
});
case 'InvalidGraph':
return res.status(400).send({
message: err.message,
type: err.type,
data: {},
});
default:
return res.status(400).send({
message: err.message,
type: 'UnknownValidationError',
data: {},
});
}
} else if (err instanceof NotFoundError) {
return res.status(404).send({
message: err.message,
type: 'NotFound',
data: {},
});
} else if (err instanceof UniqueViolationError) {
return res.status(409).send({
message: err.message,
type: 'UniqueViolation',
data: {
columns: err.columns,
table: err.table,
constraint: err.constraint,
},
});
} else if (err instanceof NotNullViolationError) {
return res.status(400).send({
message: err.message,
type: 'NotNullViolation',
data: {
column: err.column,
table: err.table,
},
});
} else if (err instanceof ForeignKeyViolationError) {
return res.status(409).send({
message: err.message,
type: 'ForeignKeyViolation',
data: {
table: err.table,
constraint: err.constraint,
},
});
} else if (err instanceof CheckViolationError) {
return res.status(400).send({
message: err.message,
type: 'CheckViolation',
data: {
table: err.table,
constraint: err.constraint,
},
});
} else if (err instanceof DataError) {
return res.status(400).send({
message: err.message,
type: 'InvalidData',
data: {},
});
} else if (err instanceof DBError) {
return res.status(500).send({
message: err.message,
type: 'UnknownDatabaseError',
data: {},
});
}
next(err);
}

View File

@@ -0,0 +1,16 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
/**
* Rate limiter middleware.
*/
export default (req: Request, res: Response, next: NextFunction) => {
const requestRateLimiter = Container.get('rateLimiter.request');
requestRateLimiter.attempt(req.ip).then(() => {
next();
})
.catch(() => {
res.status(429).send('Too Many Requests');
});
}

View File

@@ -0,0 +1,27 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
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 tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
const { settingRepository } = tenantContainer.get('repositories');
const settings = new SettingsStore(settingRepository);
tenantContainer.set('settings', settings);
}
const settings = tenantContainer.get('settings');
await settings.load();
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}

View File

@@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
export default (subscriptionSlug = 'main') => async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenant, tenantId } = req;
const Logger = Container.get('logger');
const { subscriptionRepository } = Container.get('repositories');
if (!tenant) {
throw new Error('Should load `TenancyMiddlware` before this middleware.');
}
Logger.info('[subscription_middleware] trying get tenant main subscription.');
const subscription = await subscriptionRepository.getBySlugInTenant(
subscriptionSlug,
tenantId
);
// Validate in case there is no any already subscription.
if (!subscription) {
Logger.info('[subscription_middleware] tenant has no subscription.', {
tenantId,
});
return res.boom.badRequest('Tenant has no subscription.', {
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
Logger.info(
'[subscription_middleware] tenant main subscription is expired.',
{ tenantId }
);
return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});
}
next();
};

View File

@@ -0,0 +1,36 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import tenantDependencyInjection from '@/api/middleware/TenantDependencyInjection';
import { Tenant } from '@/system/models';
export default async (req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger');
const organizationId =
req.headers['organization-id'] || req.query.organization;
const notFoundOrganization = () => {
Logger.info('[tenancy_middleware] organization id not found.');
return res.boom.unauthorized('Organization identication not found.', {
errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }],
});
};
// In case the given organization not found.
if (!organizationId) {
return notFoundOrganization();
}
const tenant = await Tenant.query()
.findOne({ organizationId })
.withGraphFetched('metadata');
// When the given organization id not found on the system storage.
if (!tenant) {
return notFoundOrganization();
}
// When user tenant not match the given organization id.
if (tenant.id !== req.user.tenantId) {
Logger.info('[tenancy_middleware] authorized user not match org. tenant.');
return res.boom.unauthorized();
}
tenantDependencyInjection(req, tenant);
next();
};

View File

@@ -0,0 +1,46 @@
import { Container } from 'typedi';
import { ITenant } from '@/interfaces';
import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant;
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(req));
const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId);
const repositories = tenantServices.repositories(tenantId);
const cacheInstance = tenantServices.cache(tenantId);
req.knex = knexInstance;
req.organizationId = organizationId;
req.tenant = tenant;
req.tenantId = tenant.id;
req.models = models;
req.repositories = repositories;
req.cache = cacheInstance;
};
export const injectI18nUtils = (req) => {
const locale = req.getLocale();
const direction = rtlDetect.getLangDir(locale);
return {
locale,
__: req.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
};
};

View File

@@ -0,0 +1,14 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
export default (
fn: (rq: Request, rs: Response, next?: NextFunction) => {}) =>
(req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger');
Promise.resolve(fn(req, res, next))
.catch((error) => {
Logger.error('[async_middleware] error.', { error });
next(error);
});
};

View File

@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import jwt from 'jsonwebtoken';
import config from '@/config';
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger');
const token = req.headers['x-access-token'] || req.query.token;
const onError = () => {
Logger.info('[auth_middleware] jwt verify error.');
res.boom.unauthorized();
};
const onSuccess = (decoded) => {
req.token = decoded;
Logger.info('[auth_middleware] jwt verify success.');
next();
};
if (!token) { return onError(); }
const verify = new Promise((resolve, reject) => {
jwt.verify(token, config.jwtSecret, async (error, decoded) => {
if (error) {
reject(error);
} else {
resolve(decoded);
}
});
});
verify.then(onSuccess).catch(onError);
};
export default authMiddleware;