Skip to content

Commit

Permalink
Merge pull request #659 from uploadcare/feat/new-resolvers
Browse files Browse the repository at this point in the history
Add `secureDeliveryProxyUrlResolver` and `secureUploadsTokenResolver` options
  • Loading branch information
nd0ut authored May 24, 2024
2 parents 6168e4e + 0d9205d commit 6333bcf
Show file tree
Hide file tree
Showing 17 changed files with 395 additions and 40 deletions.
30 changes: 21 additions & 9 deletions abstract/Block.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @ts-check
import { BaseComponent, Data } from '@symbiotejs/symbiote';
import { initialConfig } from '../blocks/Config/initialConfig.js';
import { EventEmitter } from '../blocks/UploadCtxProvider/EventEmitter.js';
import { WindowHeightTracker } from '../utils/WindowHeightTracker.js';
import { extractFilename, extractCdnUrlModifiers, extractUuid } from '../utils/cdn-utils.js';
import { getLocaleDirection } from '../utils/getLocaleDirection.js';
import { getPluralForm } from '../utils/getPluralForm.js';
import { applyTemplateData, getPluralObjects } from '../utils/template-utils.js';
Expand All @@ -10,7 +12,6 @@ import { blockCtx } from './CTX.js';
import { LocaleManager, localeStateKey } from './LocaleManager.js';
import { l10nProcessor } from './l10nProcessor.js';
import { sharedConfigKey } from './sharedConfigKey.js';
import { initialConfig } from '../blocks/Config/initialConfig.js';

const TAG_PREFIX = 'lr-';

Expand Down Expand Up @@ -242,15 +243,26 @@ export class Block extends BaseComponent {
* @returns {String}
*/
proxyUrl(url) {
let previewProxy = this.cfg.secureDeliveryProxy;
if (!previewProxy) {
return url;
if (this.cfg.secureDeliveryProxy && this.cfg.secureDeliveryProxyUrlResolver) {
console.warn(
'Both secureDeliveryProxy and secureDeliveryProxyUrlResolver are set. The secureDeliveryProxyUrlResolver will be used.',
);
}
return applyTemplateData(
previewProxy,
{ previewUrl: url },
{ transform: (value) => window.encodeURIComponent(value) },
);
if (this.cfg.secureDeliveryProxyUrlResolver) {
return this.cfg.secureDeliveryProxyUrlResolver(url, {
uuid: extractUuid(url),
cdnUrlModifiers: extractCdnUrlModifiers(url),
fileName: extractFilename(url),
});
}
if (this.cfg.secureDeliveryProxy) {
return applyTemplateData(
this.cfg.secureDeliveryProxy,
{ previewUrl: url },
{ transform: (value) => window.encodeURIComponent(value) },
);
}
return url;
}

/** @returns {import('../types').ConfigType} } */
Expand Down
2 changes: 2 additions & 0 deletions abstract/CTX.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ export const uploaderBlockCtx = (fnCtx) => ({
'*groupInfo': null,
/** @type {Set<string>} */
'*uploadTrigger': new Set(),
/** @type {import('./SecureUploadsManager.js').SecureUploadsManager | null} */
'*secureUploadsManager': null,
});
87 changes: 87 additions & 0 deletions abstract/SecureUploadsManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @ts-check

import { isSecureTokenExpired } from '../utils/isSecureTokenExpired.js';

export class SecureUploadsManager {
/**
* @private
* @type {import('./UploaderBlock.js').UploaderBlock}
*/
_block;
/**
* @private
* @type {import('../types').SecureUploadsSignatureAndExpire | null}
*/
_secureToken = null;

/** @param {import('./UploaderBlock.js').UploaderBlock} block */
constructor(block) {
this._block = block;
}

/**
* @private
* @param {unknown[]} args
*/
_debugPrint(...args) {
this._block.debugPrint('[secure-uploads]', ...args);
}

/** @returns {Promise<import('../types').SecureUploadsSignatureAndExpire | null>} */
async getSecureToken() {
const { secureSignature, secureExpire, secureUploadsSignatureResolver } = this._block.cfg;

if ((secureSignature || secureExpire) && secureUploadsSignatureResolver) {
console.warn(
'Both secureSignature/secureExpire and secureUploadsSignatureResolver are set. secureUploadsSignatureResolver will be used.',
);
}

if (secureUploadsSignatureResolver) {
if (
!this._secureToken ||
isSecureTokenExpired(this._secureToken, { threshold: this._block.cfg.secureUploadsExpireThreshold })
) {
if (!this._secureToken) {
this._debugPrint('Secure signature is not set yet.');
} else {
this._debugPrint('Secure signature is expired. Resolving a new one...');
}
try {
const result = await secureUploadsSignatureResolver();
if (!result) {
this._debugPrint('Secure signature resolver returned nothing.');
this._secureToken = null;
} else if (!result.secureSignature || !result.secureExpire) {
console.error('Secure signature resolver returned an invalid result:', result);
} else {
this._debugPrint('Secure signature resolved:', result);
this._debugPrint(
'Secure signature will expire in',
new Date(Number(result.secureExpire) * 1000).toISOString(),
);
this._secureToken = result;
}
} catch (err) {
console.error('Secure signature resolving failed. Falling back to the previous one.', err);
}
}

return this._secureToken;
}

if (secureSignature && secureExpire) {
this._debugPrint('Secure signature and expire are set. Using them...', {
secureSignature,
secureExpire,
});

return {
secureSignature,
secureExpire,
};
}

return null;
}
}
19 changes: 14 additions & 5 deletions abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { TypedCollection } from './TypedCollection.js';
import { buildOutputCollectionState } from './buildOutputCollectionState.js';
import { uploadEntrySchema } from './uploadEntrySchema.js';
import { parseCdnUrl } from '../utils/parseCdnUrl.js';
import { SecureUploadsManager } from './SecureUploadsManager.js';
export class UploaderBlock extends ActivityBlock {
couldBeCtxOwner = false;
isCtxOwner = false;
Expand Down Expand Up @@ -184,6 +185,10 @@ export class UploaderBlock extends ActivityBlock {
if (this.__initialUploadMetadata) {
this.$['*uploadMetadata'] = this.__initialUploadMetadata;
}

if (!this.$['*secureUploadsManager']) {
this.$['*secureUploadsManager'] = new SecureUploadsManager(this);
}
}

// TODO: Probably we should not allow user to override `source` property
Expand Down Expand Up @@ -582,7 +587,7 @@ export class UploaderBlock extends ActivityBlock {
* @param {import('../types').OutputCollectionState} collectionState
*/
async _createGroup(collectionState) {
const uploadClientOptions = this.getUploadClientOptions();
const uploadClientOptions = await this.getUploadClientOptions();
const uuidList = collectionState.allEntries.map((entry) => {
return entry.uuid + (entry.cdnUrlModifiers ? `/${entry.cdnUrlModifiers}` : '');
});
Expand Down Expand Up @@ -822,17 +827,21 @@ export class UploaderBlock extends ActivityBlock {
return configValue;
}

/** @returns {import('@uploadcare/upload-client').FileFromOptions} */
getUploadClientOptions() {
/** @returns {Promise<import('@uploadcare/upload-client').FileFromOptions>} */
async getUploadClientOptions() {
/** @type {SecureUploadsManager} */
const secureUploadsManager = this.$['*secureUploadsManager'];
const secureToken = await secureUploadsManager.getSecureToken().catch(() => null);

let options = {
store: this.cfg.store,
publicKey: this.cfg.pubkey,
baseCDN: this.cfg.cdnCname,
baseURL: this.cfg.baseUrl,
userAgent: customUserAgent,
integration: this.cfg.userAgentIntegration,
secureSignature: this.cfg.secureSignature,
secureExpire: this.cfg.secureExpire,
secureSignature: secureToken?.secureSignature,
secureExpire: secureToken?.secureExpire,
retryThrottledRequestMaxTimes: this.cfg.retryThrottledRequestMaxTimes,
multipartMinFileSize: this.cfg.multipartMinFileSize,
multipartChunkSize: this.cfg.multipartChunkSize,
Expand Down
16 changes: 14 additions & 2 deletions blocks/Config/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@ const allConfigKeys = /** @type {(keyof import('../../types').ConfigType)[]} */
/**
* Config keys that can't be passed as attribute (because they are object or function)
*
* @type {(keyof import('../../types').ConfigComplexType)[]}
* @type {[
* 'metadata',
* 'localeDefinitionOverride',
* 'secureUploadsSignatureResolver',
* 'secureDeliveryProxyUrlResolver',
* 'iconHrefResolver',
* ]}
*/
const complexConfigKeys = ['metadata', 'localeDefinitionOverride', 'iconHrefResolver'];
export const complexConfigKeys = [
'metadata',
'localeDefinitionOverride',
'secureUploadsSignatureResolver',
'secureDeliveryProxyUrlResolver',
'iconHrefResolver',
];

/** @type {(key: keyof import('../../types').ConfigType) => key is keyof import('../../types').ConfigComplexType} */
const isComplexKey = (key) => complexConfigKeys.includes(key);
Expand Down
3 changes: 3 additions & 0 deletions blocks/Config/initialConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@ export const initialConfig = {
metadata: null,
localeName: 'en',
localeDefinitionOverride: null,
secureUploadsExpireThreshold: 10 * 60 * 1000,
secureUploadsSignatureResolver: null,
secureDeliveryProxyUrlResolver: null,
iconHrefResolver: null,
};
44 changes: 27 additions & 17 deletions blocks/Config/normalizeConfigValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,6 @@ export const asBoolean = (value) => {
if (value === 'false') return false;
throw new Error(`Invalid boolean: "${value}"`);
};
/**
* @template {Function} T
* @param {unknown} value
* @returns {T}
*/
export const asFunction = (value) => {
if (typeof value === 'function') {
return /** @type {T} */ (value);
}

throw new Error('Invalid function value. Must be a function.');
};
/** @param {unknown} value */
const asStore = (value) => (value === 'auto' ? value : asBoolean(value));

Expand All @@ -60,13 +48,30 @@ const asMetadata = (value) => {
throw new Error('Invalid metadata value. Must be an object or function.');
};

/** @param {unknown} value */
const asLocaleDefinitionOverride = (value) => {
/**
* @template {{}} T
* @param {unknown} value
* @returns {T}
*/
const asObject = (value) => {
if (typeof value === 'object') {
return /** @type {import('../../types').LocaleDefinitionOverride} */ (value);
return /** @type {T} */ (value);
}

throw new Error('Invalid value. Must be an object.');
};

/**
* @template {Function} T
* @param {unknown} value
* @returns {T}
*/
const asFunction = (value) => {
if (typeof value === 'function') {
return /** @type {T} */ (value);
}

throw new Error('Invalid localeDefinitionOverride value. Must be an object.');
throw new Error('Invalid value. Must be a function.');
};

/**
Expand Down Expand Up @@ -128,7 +133,12 @@ const mapping = {
localeName: asString,

metadata: asMetadata,
localeDefinitionOverride: asLocaleDefinitionOverride,
secureUploadsExpireThreshold: asNumber,
localeDefinitionOverride: /** @type {typeof asObject<import('../../types').LocaleDefinitionOverride>} */ (asObject),
secureUploadsSignatureResolver:
/** @type {typeof asFunction<import('../../types').SecureUploadsSignatureResolver>} */ (asFunction),
secureDeliveryProxyUrlResolver:
/** @type {typeof asFunction<import('../../types').SecureDeliveryProxyUrlResolver>} */ (asFunction),
iconHrefResolver: /** @type {typeof asFunction<import('../../types').IconHrefResolver>} */ (asFunction),
};

Expand Down
2 changes: 1 addition & 1 deletion blocks/FileItem/FileItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export class FileItem extends UploaderBlock {
}

const fileInput = file || entry.getValue('externalUrl') || entry.getValue('uuid');
const baseUploadClientOptions = this.getUploadClientOptions();
const baseUploadClientOptions = await this.getUploadClientOptions();
/** @type {import('@uploadcare/upload-client').FileFromOptions} */
const uploadClientOptions = {
...baseUploadClientOptions,
Expand Down
35 changes: 35 additions & 0 deletions demo/preview-proxy/secure-delivery-proxy-url-resolver.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!doctype html>
<base href="../../" />

<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="./solutions/file-uploader/regular/index.css" />
<script
async=""
src="https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=raw,min/dist/es-module-shims.js"
></script>
<script type="importmap">
{
"imports": {
"@symbiotejs/symbiote": "./node_modules/@symbiotejs/symbiote/build/symbiote.js",
"@uploadcare/upload-client": "./node_modules/@uploadcare/upload-client/dist/esm/index.browser.mjs",
"@uploadcare/image-shrink": "./node_modules/@uploadcare/image-shrink/dist/esm/index.browser.mjs"
}
}
</script>
<script src="./index.js" type="module"></script>
<script type="module">
import * as LR from './index.js';

LR.registerBlocks(LR);

const config = document.querySelector('lr-config');
config.secureDeliveryProxyUrlResolver = (previewUrl) => {
return `http://localhost:3000/preview?url=${encodeURIComponent(previewUrl)}`
};
</script>
</head>

<lr-file-uploader-regular ctx-name="my-uploader"></lr-file-uploader-regular>
<lr-config ctx-name="my-uploader" pubkey="demopublickey"></lr-config>
<lr-upload-ctx-provider ctx-name="my-uploader"></lr-upload-ctx-provider>
34 changes: 34 additions & 0 deletions demo/preview-proxy/secure-delivery-proxy-url-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<base href="../../" />

<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="./solutions/file-uploader/regular/index.css" />
<script
async=""
src="https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=raw,min/dist/es-module-shims.js"
></script>
<script type="importmap">
{
"imports": {
"@symbiotejs/symbiote": "./node_modules/@symbiotejs/symbiote/build/symbiote.js",
"@uploadcare/upload-client": "./node_modules/@uploadcare/upload-client/dist/esm/index.browser.mjs",
"@uploadcare/image-shrink": "./node_modules/@uploadcare/image-shrink/dist/esm/index.browser.mjs"
}
}
</script>
<script src="./index.js" type="module"></script>
<script type="module">
import * as LR from './index.js';

LR.registerBlocks(LR);
</script>
</head>

<lr-file-uploader-regular ctx-name="my-uploader"></lr-file-uploader-regular>
<lr-config
ctx-name="my-uploader"
pubkey="demopublickey"
secure-delivery-proxy="http://localhost:3000/preview?url={{previewUrl}}"
></lr-config>
<lr-upload-ctx-provider ctx-name="my-uploader"></lr-upload-ctx-provider>
File renamed without changes.
Loading

0 comments on commit 6333bcf

Please sign in to comment.