@controleonline/ui-common 1.2.102 → 1.2.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.102",
3
+ "version": "1.2.103",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
@@ -89,6 +89,60 @@ const resolveDownloadHost = ({company = null, appDomain = ''} = {}) => {
89
89
  * Keep app-domain header only; do not path-inject environment hosts.
90
90
  * app-community#432
91
91
  */
92
+ const resolveApiEntrypointHost = () => {
93
+ const entry = normalizeText(APP_ENV?.API_ENTRYPOINT);
94
+ if (!entry) {
95
+ return '';
96
+ }
97
+
98
+ try {
99
+ const withProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(entry)
100
+ ? entry
101
+ : `https://${entry}`;
102
+ return new URL(withProtocol).host.toLowerCase();
103
+ } catch (error) {
104
+ return entry
105
+ .replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, '')
106
+ .replace(/\/.*$/, '')
107
+ .toLowerCase();
108
+ }
109
+ };
110
+
111
+ /**
112
+ * API host must never be used as /{host}/files/{id}/download segment.
113
+ * Backend File.domain often stores the API host (api.controleonline.com);
114
+ * using it produces broken URLs like:
115
+ * https://api.controleonline.com/api.controleonline.com/files/8408/download
116
+ * Tenant white-labels need:
117
+ * https://api.controleonline.com/erp.jaguncos.com.br/files/8408/download
118
+ * app-community#1160
119
+ */
120
+ const isApiEntrypointHost = host => {
121
+ const normalized = normalizeText(host).toLowerCase();
122
+ if (!normalized) {
123
+ return false;
124
+ }
125
+
126
+ const apiHost = resolveApiEntrypointHost();
127
+ if (apiHost && normalized === apiHost) {
128
+ return true;
129
+ }
130
+
131
+ if (
132
+ normalized === 'api.controleonline.com' ||
133
+ normalized === 'api.controleonline.com.br'
134
+ ) {
135
+ return true;
136
+ }
137
+
138
+ // Generic api.* hosts are infrastructure, not tenant app-domains.
139
+ if (/^api\./i.test(normalized)) {
140
+ return true;
141
+ }
142
+
143
+ return false;
144
+ };
145
+
92
146
  const isEnvironmentLikeHost = host => {
93
147
  const normalized = normalizeText(host).toLowerCase();
94
148
  if (!normalized) return false;
@@ -120,6 +174,29 @@ const isEnvironmentLikeHost = host => {
120
174
  return false;
121
175
  };
122
176
 
177
+ /**
178
+ * Prefer File.domain only when it is a real tenant host.
179
+ * Ignore API entrypoint and environment/platform hosts (app-community#1160).
180
+ */
181
+ const pickDownloadHost = (fileDomain, fallbackHost) => {
182
+ const candidate = normalizeText(fileDomain);
183
+ if (
184
+ candidate &&
185
+ !isApiEntrypointHost(candidate) &&
186
+ !isEnvironmentLikeHost(candidate)
187
+ ) {
188
+ return candidate;
189
+ }
190
+
191
+ const fallback = normalizeText(fallbackHost);
192
+ if (fallback && !isApiEntrypointHost(fallback)) {
193
+ return fallback;
194
+ }
195
+
196
+ // Last resort: runtime host (web multi-tenant) or configured DOMAIN.
197
+ return resolveAppDomain(APP_ENV?.DOMAIN) || fallback;
198
+ };
199
+
123
200
  const buildBackendDownloadUrl = (fileId, options = {}) => {
124
201
  const normalizedId = String(fileId || '').trim();
125
202
  if (!normalizedId) {
@@ -139,13 +216,11 @@ const buildTenantDownloadUrl = (url, host) => {
139
216
  return absoluteUrl;
140
217
  }
141
218
 
142
- // File download ops are registered as /{appDomain}/files/{id}/download.
143
- // Skipping path inject for environment hosts left SPA on /files/{id}/download
144
- // which 404s until/unless a non-prefixed route is deployed (app-community#796).
145
- const isFileDownloadPath = /\/files\/[^/?#]+\/download/i.test(absoluteUrl);
146
-
147
- // Do not inject environment hosts into non-download paths (app-community#432).
148
- if (isEnvironmentLikeHost(normalizedHost) && !isFileDownloadPath) {
219
+ // Do not path-inject environment/platform hosts (app-community#432).
220
+ // Bare /files/{id}/download remains for staging/manager/dev; tenant hosts
221
+ // (erp.jaguncos.com.br, loja..., etc.) still get /{appDomain}/files/... .
222
+ // Never path-inject the API entrypoint host (app-community#1160).
223
+ if (isEnvironmentLikeHost(normalizedHost) || isApiEntrypointHost(normalizedHost)) {
149
224
  return absoluteUrl;
150
225
  }
151
226
 
@@ -310,9 +385,9 @@ const resolveFileSource = (
310
385
  return null;
311
386
  }
312
387
 
313
- // Prefer FileService domain (companies/my icon/logo payload) for path inject.
314
- const fileDomain = normalizeText(normalizedFile?.domain);
315
- const downloadHost = fileDomain || host;
388
+ // Prefer FileService domain only when it is a tenant host, never the API host.
389
+ // file.domain often comes as api.controleonline.com (app-community#1160).
390
+ const downloadHost = pickDownloadHost(normalizedFile?.domain, host);
316
391
 
317
392
  const uri = isBackendDownload
318
393
  ? buildTenantDownloadUrl(uriBase, downloadHost)
@@ -114,4 +114,63 @@ describe('file authentication origin boundary', () => {
114
114
  const source = resolveDefaultFileSource(1, {headers: {'API-TOKEN': 'session-secret'}});
115
115
  expect(source.headers['API-TOKEN']).toBe('session-secret');
116
116
  });
117
+
118
+ it('ignores file.domain when it is the API host and uses tenant app-domain (app-community#1160)', () => {
119
+ const company = {domain: 'loja.jaguncos.com.br'};
120
+
121
+ const source = resolveDefaultFileSource(
122
+ {
123
+ id: 8408,
124
+ domain: 'api.controleonline.com',
125
+ url: '/api.controleonline.com/files/8408/download',
126
+ },
127
+ {
128
+ company,
129
+ headers: {Authorization: 'Bearer token'},
130
+ },
131
+ );
132
+
133
+ expect(source.uri).toBe(
134
+ 'https://api.controleonline.com/loja.jaguncos.com.br/files/8408/download',
135
+ );
136
+ expect(source.uri).not.toMatch(/api\.controleonline\.com\/files/);
137
+ expect(source.headers['app-domain']).toBe('loja.jaguncos.com.br');
138
+ });
139
+
140
+ it('ignores API host as company domain and falls back to explicit appDomain', () => {
141
+ const source = resolveDefaultFileSource(
142
+ {
143
+ id: 8408,
144
+ domain: 'api.controleonline.com',
145
+ },
146
+ {
147
+ company: {domain: 'api.controleonline.com'},
148
+ appDomain: 'erp.jaguncos.com.br',
149
+ headers: {},
150
+ },
151
+ );
152
+
153
+ expect(source.uri).toBe(
154
+ 'https://api.controleonline.com/erp.jaguncos.com.br/files/8408/download',
155
+ );
156
+ expect(source.headers['app-domain']).toBe('erp.jaguncos.com.br');
157
+ });
158
+
159
+ it('still prefers a real tenant file.domain over company when not API host', () => {
160
+ const source = resolveDefaultFileSource(
161
+ {
162
+ id: 12,
163
+ domain: 'cardapio.jaguncos.com.br',
164
+ url: '/files/12/download',
165
+ },
166
+ {
167
+ company: {domain: 'erp.jaguncos.com.br'},
168
+ },
169
+ );
170
+
171
+ expect(source.uri).toBe(
172
+ 'https://api.controleonline.com/cardapio.jaguncos.com.br/files/12/download',
173
+ );
174
+ expect(source.headers['app-domain']).toBe('cardapio.jaguncos.com.br');
175
+ });
117
176
  });