diff --git a/ilc/common/wrapWithCache.js b/ilc/common/wrapWithCache.js index 06f0c44a..ed1213e7 100644 --- a/ilc/common/wrapWithCache.js +++ b/ilc/common/wrapWithCache.js @@ -6,13 +6,14 @@ errors.WrapWithCacheError = extendError('WrapWithCacheError'); const wrapWithCache = (localStorage, logger, createHash = hashFn) => (fn, cacheParams = {}) => { const { cacheForSeconds = 60, + name = '', // "hash" of returned value is based only on arguments, so with the help "name" we can add prefix to hash } = cacheParams; const cacheResolutionPromise = {}; return (...args) => { const now = Math.floor(Date.now() / 1000); - const hash = args.length > 0 ? createHash(JSON.stringify(args)) : '__null__'; + const hash = `${name ? name + '__' : ''}${args.length > 0 ? createHash(JSON.stringify(args)) : '__null__'}`; if (localStorage.getItem(hash) === null || JSON.parse(localStorage.getItem(hash)).cachedAt < now - cacheForSeconds) { if (cacheResolutionPromise[hash] !== undefined) { diff --git a/ilc/server/app.js b/ilc/server/app.js index 800caf14..e7318f49 100644 --- a/ilc/server/app.js +++ b/ilc/server/app.js @@ -43,7 +43,8 @@ module.exports = (registryService, pluginManager) => { app.register(require('./ping')); app.get('/_ilc/api/v1/registry/template/:templateName', async (req, res) => { - const data = await registryService.getTemplate(req.params.templateName); + const currentDomain = req.hostname; + const data = await registryService.getTemplate(req.params.templateName, currentDomain); res.status(200).send(data.data.content); }); diff --git a/ilc/server/errorHandler/ErrorHandler.js b/ilc/server/errorHandler/ErrorHandler.js index faf8c0cb..461526e4 100644 --- a/ilc/server/errorHandler/ErrorHandler.js +++ b/ilc/server/errorHandler/ErrorHandler.js @@ -51,7 +51,8 @@ module.exports = class ErrorHandler { errorId }); - let data = await this.#registryService.getTemplate('500'); + const currentDomain = req.hostname; + let data = await this.#registryService.getTemplate('500', currentDomain); data = data.data.content.replace('%ERRORID%', `Error ID: ${errorId}`); nres.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); diff --git a/ilc/server/registry/Registry.js b/ilc/server/registry/Registry.js index c6c1a03c..9e878e37 100644 --- a/ilc/server/registry/Registry.js +++ b/ilc/server/registry/Registry.js @@ -13,6 +13,7 @@ module.exports = class Registry { #cacheHeated = { config: false, template: false, + routerDomains: false, }; /** @@ -30,6 +31,7 @@ module.exports = class Registry { const getConfigMemo = wrapFetchWithCache(this.#getConfig, { cacheForSeconds: 5, + name: 'registry_getConfig', }); this.getConfig = async (options) => { @@ -38,13 +40,28 @@ module.exports = class Registry { return res; }; - this.getTemplate = wrapFetchWithCache(this.#getTemplate, { + this.getRouterDomains = wrapFetchWithCache(this.#getRouterDomains, { cacheForSeconds: 30, + name: 'registry_routerDomains', }); + + const getTemplateMemo = wrapFetchWithCache(this.#getTemplate, { + cacheForSeconds: 30, + }); + + this.getTemplate = async (templateName, forDomain) => { + if (templateName === '500' && forDomain) { + const routerDomains = await this.getRouterDomains(); + const redefined500 = routerDomains.data.find(item => item.domainName === forDomain)?.template500; + templateName = redefined500 || templateName; + } + + return await getTemplateMemo(templateName); + }; } async preheat() { - if (this.#cacheHeated.template && this.#cacheHeated.config) { + if (this.#cacheHeated.template && this.#cacheHeated.config && this.#cacheHeated.routerDomains) { return; } @@ -53,6 +70,7 @@ module.exports = class Registry { await Promise.all([ this.getConfig(), this.getTemplate('500'), + this.getRouterDomains(), ]); this.#logger.info('Registry preheated successfully!'); @@ -102,6 +120,27 @@ module.exports = class Registry { return res.data; }; + #getRouterDomains = async () => { + this.#logger.debug('Calling get routerDomains registry endpoint...'); + + const url = urljoin(this.#address, 'api/v1/router_domains'); + let res; + try { + res = await axios.get(url, { responseType: 'json' }); + } catch (e) { + throw new errors.RegistryError({ + message: `Error while requesting routerDomains from registry`, + cause: e, + data: { + requestedUrl: url + } + }); + } + + this.#cacheHeated.routerDomains = true; + return res.data; + }; + #filterConfig = (config, filter) => { if (!filter || !Object.keys(filter).length) { return config; diff --git a/registry/client/src/routerDomains/Edit.js b/registry/client/src/routerDomains/Edit.js index cd0124b7..196ae939 100644 --- a/registry/client/src/routerDomains/Edit.js +++ b/registry/client/src/routerDomains/Edit.js @@ -6,6 +6,8 @@ import { TextInput, required, TextField, + ReferenceInput, + SelectInput, } from 'react-admin'; // eslint-disable-line import/no-unresolved const Title = ({ record }) => { @@ -19,7 +21,12 @@ const InputForm = ({ mode = 'edit', ...props }) => { ? : null} - + + + + ); }; diff --git a/registry/client/src/routerDomains/List.js b/registry/client/src/routerDomains/List.js index 377ee4f0..9a7e7652 100644 --- a/registry/client/src/routerDomains/List.js +++ b/registry/client/src/routerDomains/List.js @@ -51,6 +51,7 @@ const PostList = props => { + diff --git a/registry/server/app.ts b/registry/server/app.ts index fa70b3ae..cfca063b 100644 --- a/registry/server/app.ts +++ b/registry/server/app.ts @@ -40,7 +40,7 @@ export default (withAuth: boolean = true) => { app.use('/api/v1/auth_entities', authMw, routes.authEntities); app.use('/api/v1/versioning', authMw, routes.versioning); app.use('/api/v1/settings', routes.settings(authMw)); - app.use('/api/v1/router_domains', authMw, routes.routerDomains); + app.use('/api/v1/router_domains', routes.routerDomains); app.use(errorHandler); diff --git a/registry/server/migrations/20210405164831_router_domains.ts b/registry/server/migrations/20210405164831_router_domains.ts index 45d73f12..6817ae88 100644 --- a/registry/server/migrations/20210405164831_router_domains.ts +++ b/registry/server/migrations/20210405164831_router_domains.ts @@ -5,6 +5,7 @@ export async function up(knex: Knex): Promise { return knex.schema.createTable('router_domains', table => { table.increments('id'); table.string('domainName', 255).notNullable(); + table.string('template500', 50).nullable().references('templates.name') }); } diff --git a/registry/server/routerDomains/interfaces/index.ts b/registry/server/routerDomains/interfaces/index.ts index 5a1866e3..0e6c28f3 100644 --- a/registry/server/routerDomains/interfaces/index.ts +++ b/registry/server/routerDomains/interfaces/index.ts @@ -1,18 +1,24 @@ import Joi from 'joi'; +import { templateNameSchema } from '../../templates/interfaces'; export default interface RouterDomains { id: number, domainName: string, + template500?: string, }; export const routerDomainIdSchema = Joi.string().trim().required(); -const routerDomainNameSchema = Joi.string().trim().min(1); +const commonRouterDomainsSchema = { + domainName: Joi.string().trim().min(1), + template500: templateNameSchema.allow(null), +}; export const partialRouterDomainsSchema = Joi.object({ - domainName: routerDomainNameSchema, + ...commonRouterDomainsSchema, }); export const routerDomainsSchema = Joi.object({ - domainName: routerDomainNameSchema.required(), + ...commonRouterDomainsSchema, + domainName: commonRouterDomainsSchema.domainName.required(), }); diff --git a/registry/tests/routerDomains.spec.ts b/registry/tests/routerDomains.spec.ts index 3c332fc4..80cc1343 100644 --- a/registry/tests/routerDomains.spec.ts +++ b/registry/tests/routerDomains.spec.ts @@ -29,6 +29,17 @@ describe(`Tests ${example.url}`, () => { .expect(422, '"domainName" must be a string'); }); + it('should not create record with non-existed template500', async () => { + const response = await request.post(example.url) + .send({ + ...example.correct, + template500: 'nonExistedTemplate', + }) + .expect(500); + + expect(response.text).to.include('Internal server error occurred.'); + }); + it('should successfully create record', async () => { let routerDomainsId; @@ -56,12 +67,44 @@ describe(`Tests ${example.url}`, () => { } }); - describe('Authentication / Authorization', () => { - it('should deny access w/o authentication', async () => { - await requestWithAuth.post(example.url) - .send(example.correct) - .expect(401); - }); + it('should successfully create record with template for 500', async () => { + const templateName = 'testTemplate500'; + let routerDomainsId; + + const exampleWithTemplate500 = { + ...example.correct, + template500: templateName, + }; + + try { + await request.post('/api/v1/template/') + .send({ + name: templateName, + content: 'ncTestTemplateContent' + }); + + const responseCreation = await request.post(example.url) + .send(exampleWithTemplate500) + .expect(200) + + routerDomainsId = responseCreation.body.id; + + expect(responseCreation.body).deep.equal({ + id: routerDomainsId, + ...exampleWithTemplate500, + }); + + const responseFetching = await request.get(example.url + routerDomainsId) + .expect(200); + + expect(responseFetching.body).deep.equal({ + id: routerDomainsId, + ...exampleWithTemplate500, + }); + } finally { + routerDomainsId && await request.delete(example.url + routerDomainsId); + await request.delete('/api/v1/template/' + templateName); + } }); }); @@ -156,16 +199,6 @@ describe(`Tests ${example.url}`, () => { await Promise.all(routerDomainsList.map(item => request.delete(example.url + item.id))); } }); - - describe('Authentication / Authorization', () => { - it('should deny access w/o authentication', async () => { - await requestWithAuth.get(example.url) - .expect(401); - - await requestWithAuth.get(example.url + 123) - .expect(401); - }); - }); }); describe('Update', () => { @@ -213,14 +246,6 @@ describe(`Tests ${example.url}`, () => { routerDomainsId && await request.delete(example.url + routerDomainsId); } }); - - describe('Authentication / Authorization', () => { - it('should deny access w/o authentication', async () => { - await requestWithAuth.put(example.url + 123) - .send(example.updated) - .expect(401); - }); - }); }); describe('Delete', () => { @@ -286,12 +311,5 @@ describe(`Tests ${example.url}`, () => { await request.delete(example.url + response.body.id) .expect(204, ''); }); - - describe('Authentication / Authorization', () => { - it('should deny access w/o authentication', async () => { - await requestWithAuth.delete(example.url + 123) - .expect(401); - }); - }); }); }); diff --git a/registry/tests/templates.spec.ts b/registry/tests/templates.spec.ts index 5ff56ae9..d58e81dc 100644 --- a/registry/tests/templates.spec.ts +++ b/registry/tests/templates.spec.ts @@ -292,6 +292,38 @@ describe(`Tests ${example.url}`, () => { expect(response.body).deep.equal({}); }); + it('should successfully delete record if doesn\'t have any reference from foreign (routerDomains -> template500) to current primary key', async () => { + let routerDomainsId; + + try { + await request.post(example.url).send(example.correct).expect(200); + + const responseRouterDomains = await request.post('/api/v1/router_domains/') + .send({ + domainName: 'domainNameCorrect', + template500: example.correct.name, + }) + .expect(200) + + routerDomainsId = responseRouterDomains.body.id; + + const response = await request.delete(example.url + example.correct.name) + .expect(500); + expect(response.text).to.include('Internal server error occurred.'); + + await request.delete('/api/v1/router_domains/' + routerDomainsId); + + await request.delete(example.url + example.correct.name) + .expect(204, ''); + + await request.get(example.url + example.correct.name) + .expect(404, 'Not found'); + } finally { + routerDomainsId && await request.delete('/api/v1/router_domains/' + routerDomainsId); + await request.delete(example.url + example.correct.name) + } + }); + it('should successfully delete record', async () => { await request.post(example.url).send(example.correct).expect(200);