diff --git a/src/registry/routes/component.ts b/src/registry/routes/component.ts index 064413112..1d2b1fb85 100644 --- a/src/registry/routes/component.ts +++ b/src/registry/routes/component.ts @@ -1,6 +1,6 @@ import { Readable } from 'node:stream'; import { encode } from '@rdevis/turbo-stream'; -import type { Request, RequestHandler, Response } from 'express'; +import type { CookieOptions, Request, RequestHandler, Response } from 'express'; import { serializeError } from 'serialize-error'; import strings from '../../resources'; import type { Config } from '../../types'; @@ -49,6 +49,14 @@ export default function component( res.set(result.headers); } + if (Array.isArray(result.cookies) && result.cookies.length > 0) { + for (const cookie of result.cookies) { + const opts: CookieOptions = (cookie.options ?? + {}) as CookieOptions; + res.cookie(cookie.name, cookie.value as any, opts); + } + } + const streamEnabled = !!result.response.data?.component?.props?.[stream]; if (streamEnabled) { diff --git a/src/registry/routes/components.ts b/src/registry/routes/components.ts index f12013e80..17c3a37ba 100644 --- a/src/registry/routes/components.ts +++ b/src/registry/routes/components.ts @@ -1,6 +1,6 @@ import async from 'async'; -import type { Request, RequestHandler, Response } from 'express'; +import type { CookieOptions, Request, RequestHandler, Response } from 'express'; import strings from '../../resources'; import type { Config } from '../../types'; import type { Repository } from '../domain/repository'; @@ -30,6 +30,23 @@ export default function components( res.set(results[0].headers); }; + const setCookies = ( + results: GetComponentResult[] | undefined, + res: Response + ) => { + if (!results || results.length !== 1 || !results[0] || !res.cookie) { + return; + } + const cookies = results[0].cookies; + if (Array.isArray(cookies) && cookies.length > 0) { + for (const cookie of cookies) { + const opts: CookieOptions = (cookie.options ?? {}) as CookieOptions; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + res.cookie(cookie.name, cookie.value as any, opts); + } + } + }; + return (req: Request, res: Response) => { const components = req.body.components as Component[]; const registryErrors = strings.errors.registry; @@ -85,6 +102,7 @@ export default function components( (_err: any, results: GetComponentResult[]) => { try { setHeaders(results, res); + setCookies(results, res); res.status(200).json(results); } catch (e) { // @ts-ignore I think this will never reach (how can setHeaders throw?) diff --git a/src/registry/routes/helpers/get-component.ts b/src/registry/routes/helpers/get-component.ts index 1805fee90..1f1dae56a 100644 --- a/src/registry/routes/helpers/get-component.ts +++ b/src/registry/routes/helpers/get-component.ts @@ -3,6 +3,7 @@ import Domain from 'node:domain'; import type { IncomingHttpHeaders } from 'node:http'; import vm from 'node:vm'; import acceptLanguageParser from 'accept-language-parser'; +import type { CookieOptions } from 'express'; import Cache from 'nice-cache'; import Client from 'oc-client'; import emptyResponseHandler from 'oc-empty-response-handler'; @@ -43,6 +44,11 @@ export interface RendererOptions { export interface GetComponentResult { status: number; headers?: Record; + cookies?: Array<{ + name: string; + value: any; + options?: CookieOptions; + }>; response: { data?: any; type?: string; @@ -143,6 +149,11 @@ export default function getComponent(conf: Config, repository: Repository) { const nestedRenderer = NestedRenderer(renderer, options.conf); const retrievingInfo = GetComponentRetrievingInfo(options); let responseHeaders: Record = {}; + const responseCookies: Array<{ + name: string; + value: any; + options?: CookieOptions; + }> = []; const getLanguage = () => { const paramOverride = @@ -151,6 +162,9 @@ export default function getComponent(conf: Config, repository: Repository) { }; const callback = (result: GetComponentResult) => { + if (responseCookies.length > 0 && !result.cookies) { + result.cookies = responseCookies; + } if (result.response.error) { retrievingInfo.extend(result.response); } @@ -578,6 +592,17 @@ export default function getComponent(conf: Config, repository: Repository) { responseHeaders[header.toLowerCase()] = value; } }, + setCookie: ( + name?: string, + value?: any, + options?: CookieOptions + ) => { + if (typeof name !== 'string') { + throw strings.errors.registry + .COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID; + } + responseCookies.push({ name, value, options }); + }, templates: repository.getTemplatesInfo(), streamSymbol: stream }; diff --git a/src/resources/index.ts b/src/resources/index.ts index 57c14c095..d3f70be80 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -98,6 +98,8 @@ export default { COMPONENT_VERSION_NOT_VALID_CODE: 'version_not_valid', COMPONENT_SET_HEADER_PARAMETERS_NOT_VALID: 'context.setHeader parameters must be strings', + COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID: + 'context.setCookie parameters are not valid', CONFIGURATION_DEPENDENCIES_MUST_BE_ARRAY: 'Registry configuration is not valid: dependencies must be an array', CONFIGURATION_EMPTY: 'Registry configuration is empty', diff --git a/test/acceptance/registry.js b/test/acceptance/registry.js index 65b236944..c78acc75a 100644 --- a/test/acceptance/registry.js +++ b/test/acceptance/registry.js @@ -153,6 +153,27 @@ describe('registry', () => { }); }); + describe('GET /hello-world-custom-cookies', () => { + describe('with the default configuration and strong version 1.0.0', () => { + before((done) => { + next( + request('http://localhost:3030/hello-world-custom-cookies/1.0.0'), + done + ); + }); + + it('should return the component and set cookies', () => { + expect(result.version).to.equal('1.0.0'); + expect(result.name).to.equal('hello-world-custom-cookies'); + expect(result.headers).to.be.undefined; + const setCookie = headers['set-cookie']; + expect(setCookie).to.be.an('array'); + expect(setCookie.join(';')).to.contain('Test-Cookie=Cookie-Value'); + expect(setCookie.join(';')).to.contain('Another-Cookie=Another-Value'); + }); + }); + }); + describe('POST /hello-world-custom-headers', () => { describe('with the default configuration (no customHeadersToSkipOnWeakVersion defined) and strong version 1.0.0', () => { before((done) => { @@ -351,6 +372,43 @@ describe('registry', () => { }); }); + describe('POST /hello-world-custom-cookies', () => { + describe('with the default configuration and strong version 1.0.0', () => { + before((done) => { + next( + request('http://localhost:3030', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + components: [ + { + name: 'hello-world-custom-cookies', + version: '1.0.0' + } + ] + }) + }), + done + ); + }); + + it('should set HTTP cookies', () => { + const setCookie = headers['set-cookie']; + expect(setCookie).to.be.an('array'); + expect(setCookie.join(';')).to.contain('Test-Cookie=Cookie-Value'); + expect(setCookie.join(';')).to.contain('Another-Cookie=Another-Value'); + }); + + it('should return the component and keep headers in body the same', () => { + expect(result[0].response.version).to.equal('1.0.0'); + expect(result[0].response.name).to.equal('hello-world-custom-cookies'); + expect(result[0].headers).to.be.deep.equal({}); + }); + }); + }); + describe('GET /', () => { before((done) => { next(request('http://localhost:3030'), done); @@ -368,6 +426,7 @@ describe('registry', () => { 'http://localhost:3030/empty', 'http://localhost:3030/handlebars3-component', 'http://localhost:3030/hello-world', + 'http://localhost:3030/hello-world-custom-cookies', 'http://localhost:3030/hello-world-custom-headers', 'http://localhost:3030/jade-filters', 'http://localhost:3030/language', diff --git a/test/fixtures/components/hello-world-custom-cookies/_package/package.json b/test/fixtures/components/hello-world-custom-cookies/_package/package.json new file mode 100644 index 000000000..e14b93f74 --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/_package/package.json @@ -0,0 +1,27 @@ +{ + "name": "hello-world-custom-cookies", + "description": "", + "version": "1.0.0", + "repository": "", + "oc": { + "files": { + "template": { + "type": "handlebars", + "hashKey": "dbf1f0cb2ae6a52f402b26dff36d5bd696519933", + "src": "template.js", + "version": "6.0.25", + "size": 187 + }, + "dataProvider": { + "type": "node.js", + "hashKey": "ef8a15013fac7073b79cc9b21113f1d699ee83ae", + "src": "server.js", + "size": 334 + }, + "static": [] + }, + "version": "0.50.27", + "packaged": true, + "date": 1756543269656 + } +} diff --git a/test/fixtures/components/hello-world-custom-cookies/_package/server.js b/test/fixtures/components/hello-world-custom-cookies/_package/server.js new file mode 100644 index 000000000..9bfbff313 --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/_package/server.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={896:e=>{e.exports.data=function(e,o){e.setCookie("Test-Cookie","Cookie-Value"),e.setCookie("Another-Cookie","Another-Value",{httpOnly:!0}),o(null,{})}}},o={};var t=function t(r){var s=o[r];if(void 0!==s)return s.exports;var i=o[r]={exports:{}};return e[r](i,i.exports,t),i.exports}(896);module.exports=t})(); \ No newline at end of file diff --git a/test/fixtures/components/hello-world-custom-cookies/_package/template.js b/test/fixtures/components/hello-world-custom-cookies/_package/template.js new file mode 100644 index 000000000..2b367959d --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/_package/template.js @@ -0,0 +1 @@ +var oc=oc||{};oc.components=oc.components||{},oc.components.dbf1f0cb2ae6a52f402b26dff36d5bd696519933={compiler:[8,">= 4.3.0"],main:function(o,c,n,e,a){return"Hello world!\n"},useData:!0}; \ No newline at end of file diff --git a/test/fixtures/components/hello-world-custom-cookies/package.json b/test/fixtures/components/hello-world-custom-cookies/package.json new file mode 100644 index 000000000..19edde1a6 --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/package.json @@ -0,0 +1,17 @@ +{ + "name": "hello-world-custom-cookies", + "description": "", + "version": "1.0.0", + "repository": "", + "oc": { + "files": { + "data": "server.js", + "template": { + "src": "template.html", + "type": "handlebars" + } + } + } +} + + diff --git a/test/fixtures/components/hello-world-custom-cookies/server.js b/test/fixtures/components/hello-world-custom-cookies/server.js new file mode 100644 index 000000000..d95021ccf --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/server.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports.data = function (context, callback) { + context.setCookie('Test-Cookie', 'Cookie-Value'); + context.setCookie('Another-Cookie', 'Another-Value', { httpOnly: true }); + callback(null, {}); +}; + + diff --git a/test/fixtures/components/hello-world-custom-cookies/template.html b/test/fixtures/components/hello-world-custom-cookies/template.html new file mode 100644 index 000000000..cd0875583 --- /dev/null +++ b/test/fixtures/components/hello-world-custom-cookies/template.html @@ -0,0 +1 @@ +Hello world! diff --git a/test/unit/registry-domain-repository.js b/test/unit/registry-domain-repository.js index 9bc80dfe7..d0e9cf834 100644 --- a/test/unit/registry-domain-repository.js +++ b/test/unit/registry-domain-repository.js @@ -498,6 +498,7 @@ describe('registry : domain : repository', () => { 'empty', 'handlebars3-component', 'hello-world', + 'hello-world-custom-cookies', 'hello-world-custom-headers', 'jade-filters', 'language',