diff --git a/public/locales/en/app.json b/public/locales/en/app.json index 9bd5a1339..1a52ff7d1 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -48,6 +48,10 @@ "publicGatewayForm": { "placeholder": "Enter a URL (https://ipfs.io)" }, + "localGatewayForm": { + "placeholder": "Enter a URL (https://ipfs.example.com)", + "description": "Set this to your gateway URL if accessing WebUI through a reverse proxy or from a different host. Leave empty to use the gateway URL from Kubo config." + }, "publicSubdomainGatewayForm": { "placeholder": "Enter a URL (https://dweb.link)" }, @@ -87,6 +91,7 @@ "pinStatus": "Pin Status", "publicKey": "Public key", "publicGateway": "Public Gateway", + "localGateway": "Local Gateway", "rateIn": "Rate in", "rateOut": "Rate out", "repo": "Repo", diff --git a/src/bundles/config.js b/src/bundles/config.js index e4902fc7c..1b9bfa803 100644 --- a/src/bundles/config.js +++ b/src/bundles/config.js @@ -66,7 +66,13 @@ bundle.reactIsSameOriginToBridge = createSelector( bundle.selectGatewayUrl = createSelector( 'selectConfigObject', 'selectPublicGateway', - (config, publicGateway) => getURLFromAddress('Gateway', config) || publicGateway + 'selectLocalGateway', + (config, publicGateway, localGateway) => { + // Priority: 1) User-configured local gateway, 2) Kubo config, 3) Public gateway + const url = localGateway || getURLFromAddress('Gateway', config) || publicGateway + // Normalize: remove trailing slashes to avoid double slashes when constructing paths + return url.replace(/\/+$/, '') + } ) bundle.selectAvailableGatewayUrl = createSelector( diff --git a/src/bundles/gateway.js b/src/bundles/gateway.js index fa8019bf1..990ac8be7 100644 --- a/src/bundles/gateway.js +++ b/src/bundles/gateway.js @@ -19,6 +19,13 @@ const readPublicGatewaySetting = () => { return setting || DEFAULT_PATH_GATEWAY } +const readLocalGatewaySetting = () => { + const setting = readSetting('ipfsLocalGateway') + // Return empty string if not set, so we can distinguish between + // "not configured" and "configured to empty" + return setting || '' +} + const readPublicSubdomainGatewaySetting = () => { const setting = readSetting('ipfsPublicSubdomainGateway') return setting || DEFAULT_SUBDOMAIN_GATEWAY @@ -33,7 +40,8 @@ const init = () => ({ availableGateway: null, publicGateway: readPublicGatewaySetting(), publicSubdomainGateway: readPublicSubdomainGatewaySetting(), - ipfsCheckUrl: readIpfsCheckUrlSetting() + ipfsCheckUrl: readIpfsCheckUrlSetting(), + localGateway: readLocalGatewaySetting() }) /** @@ -207,6 +215,10 @@ const bundle = { return { ...state, ipfsCheckUrl: action.payload } } + if (action.type === 'SET_LOCAL_GATEWAY') { + return { ...state, localGateway: action.payload } + } + return state }, @@ -243,6 +255,35 @@ const bundle = { dispatch({ type: 'SET_IPFS_CHECK_URL', payload: url }) }, + /** + * @param {string} address + * @returns {function({dispatch: Function}): Promise} + */ + doUpdateLocalGateway: (address) => async ({ dispatch }) => { + // Normalize: remove trailing slashes + const normalizedAddress = address.replace(/\/+$/, '') + await writeSetting('ipfsLocalGateway', normalizedAddress) + dispatch({ type: 'SET_LOCAL_GATEWAY', payload: normalizedAddress }) + + // Sync to kuboGateway for Helia/Explore components + if (normalizedAddress) { + try { + const url = new URL(normalizedAddress) + const host = url.hostname + const port = url.port || (url.protocol === 'https:' ? '443' : '80') + const protocol = url.protocol.replace(':', '') + await writeSetting('kuboGateway', { + host, + port, + protocol, + trustlessBlockBrokerConfig: { init: { allowLocal: true, allowInsecure: protocol === 'http' } } + }) + } catch (e) { + console.error('Error syncing ipfsLocalGateway to kuboGateway:', e) + } + } + }, + /** * @param {any} state * @returns {string|null} @@ -265,7 +306,13 @@ const bundle = { * @param {any} state * @returns {string} */ - selectIpfsCheckUrl: (state) => state?.gateway?.ipfsCheckUrl + selectIpfsCheckUrl: (state) => state?.gateway?.ipfsCheckUrl, + + /** + * @param {any} state + * @returns {string} + */ + selectLocalGateway: (state) => state?.gateway?.localGateway } export default bundle diff --git a/src/bundles/ipfs-provider.js b/src/bundles/ipfs-provider.js index 7b4791087..314a4f2a6 100644 --- a/src/bundles/ipfs-provider.js +++ b/src/bundles/ipfs-provider.js @@ -332,7 +332,25 @@ const actions = { } const kuboGateway = readSetting('kuboGateway') - if (kuboGateway === null || typeof kuboGateway === 'string' || typeof kuboGateway === 'boolean' || typeof kuboGateway === 'number') { + const localGateway = readSetting('ipfsLocalGateway') + + if (localGateway) { + // User has configured a custom local gateway, sync it to kuboGateway for Helia/Explore + try { + const url = new URL(localGateway) + const host = url.hostname + const port = url.port || (url.protocol === 'https:' ? '443' : '80') + const protocol = url.protocol.replace(':', '') + await writeSetting('kuboGateway', { + host, + port, + protocol, + trustlessBlockBrokerConfig: { init: { allowLocal: true, allowInsecure: protocol === 'http' } } + }) + } catch (e) { + console.error('Error parsing ipfsLocalGateway for kuboGateway:', e) + } + } else if (kuboGateway === null || typeof kuboGateway === 'string' || typeof kuboGateway === 'boolean' || typeof kuboGateway === 'number') { // empty or invalid, set defaults await writeSetting('kuboGateway', { trustlessBlockBrokerConfig: { init: { allowLocal: true, allowInsecure: false } } }) } else if (/** @type {Record} */(kuboGateway).trustlessBlockBrokerConfig == null) { diff --git a/src/components/local-gateway-form/LocalGatewayForm.js b/src/components/local-gateway-form/LocalGatewayForm.js new file mode 100644 index 000000000..412b7b8c7 --- /dev/null +++ b/src/components/local-gateway-form/LocalGatewayForm.js @@ -0,0 +1,82 @@ +import React, { useState, useEffect } from 'react' +import { connect } from 'redux-bundler-react' +import { withTranslation } from 'react-i18next' +import Button from '../button/button.tsx' +import { checkValidHttpUrl } from '../../bundles/gateway.js' + +const LocalGatewayForm = ({ t, doUpdateLocalGateway, localGateway }) => { + const [value, setValue] = useState(localGateway) + const [isValid, setIsValid] = useState(true) + + useEffect(() => { + // Empty value is valid (means "use default from Kubo config") + setIsValid(value === '' || checkValidHttpUrl(value)) + }, [value]) + + const onChange = (event) => setValue(event.target.value) + + const onSubmit = async (event) => { + event.preventDefault() + if (isValid) { + doUpdateLocalGateway(value) + } + } + + const onClear = async (event) => { + event.preventDefault() + setValue('') + doUpdateLocalGateway('') + } + + const onKeyPress = (event) => { + if (event.key === 'Enter') { + onSubmit(event) + } + } + + const hasChanges = value !== localGateway + + return ( +
+ +
+ + +
+

+ {t('localGatewayForm.description', 'Set this to your gateway URL if accessing WebUI through a reverse proxy or from a different host. Leave empty to use the gateway address from Kubo config.')} +

+
+ ) +} + +export default connect( + 'doUpdateLocalGateway', + 'selectLocalGateway', + withTranslation('app')(LocalGatewayForm) +) diff --git a/src/settings/SettingsPage.js b/src/settings/SettingsPage.js index a7c4e48b6..ccb05230f 100644 --- a/src/settings/SettingsPage.js +++ b/src/settings/SettingsPage.js @@ -18,6 +18,7 @@ import AnalyticsToggle from '../components/analytics-toggle/AnalyticsToggle.js' import ApiAddressForm from '../components/api-address-form/api-address-form' import PublicGatewayForm from '../components/public-gateway-form/PublicGatewayForm.js' import PublicSubdomainGatewayForm from '../components/public-subdomain-gateway-form/PublicSubdomainGatewayForm.js' +import LocalGatewayForm from '../components/local-gateway-form/LocalGatewayForm.js' import IpfsCheckForm from '../components/ipfs-check-form/IpfsCheckForm.js' import { JsonEditor } from './editor/JsonEditor.js' import Experiments from '../components/experiments/ExperimentsPanel.js' @@ -67,19 +68,23 @@ export const SettingsPage = ({
+ {t('app:terms.localGateway')} + +
+
{t('app:terms.publicGateway')} - -

Select a default Subdomain Gateway for generating shareable links.

-
- -
-
- -

Select a fallback Path Gateway for generating shareable links for CIDs that exceed the 63-character DNS limit.

-
- -
-
+ +

Select a default Subdomain Gateway for generating shareable links.

+
+ + +
+ +

Select a fallback Path Gateway for generating shareable links for CIDs that exceed the 63-character DNS limit.

+
+ +
+ {t('ipnsPublishingKeys.title')}