Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
},
Expand Down Expand Up @@ -87,6 +91,7 @@
"pinStatus": "Pin Status",
"publicKey": "Public key",
"publicGateway": "Public Gateway",
"localGateway": "Local Gateway",
"rateIn": "Rate in",
"rateOut": "Rate out",
"repo": "Repo",
Expand Down
8 changes: 7 additions & 1 deletion src/bundles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
51 changes: 49 additions & 2 deletions src/bundles/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +40,8 @@ const init = () => ({
availableGateway: null,
publicGateway: readPublicGatewaySetting(),
publicSubdomainGateway: readPublicSubdomainGatewaySetting(),
ipfsCheckUrl: readIpfsCheckUrlSetting()
ipfsCheckUrl: readIpfsCheckUrlSetting(),
localGateway: readLocalGatewaySetting()
})

/**
Expand Down Expand Up @@ -207,6 +215,10 @@ const bundle = {
return { ...state, ipfsCheckUrl: action.payload }
}

if (action.type === 'SET_LOCAL_GATEWAY') {
return { ...state, localGateway: action.payload }
}

return state
},

Expand Down Expand Up @@ -243,6 +255,35 @@ const bundle = {
dispatch({ type: 'SET_IPFS_CHECK_URL', payload: url })
},

/**
* @param {string} address
* @returns {function({dispatch: Function}): Promise<void>}
*/
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}
Expand All @@ -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
20 changes: 19 additions & 1 deletion src/bundles/ipfs-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,25 @@
}

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)

Check failure on line 340 in src/bundles/ipfs-provider.js

View workflow job for this annotation

GitHub Actions / lint

Argument of type 'string | object' is not assignable to parameter of type 'string | URL'.

Check failure on line 340 in src/bundles/ipfs-provider.js

View workflow job for this annotation

GitHub Actions / lint

Argument of type 'string | object' is not assignable to parameter of type 'string | URL'. Type 'object' is not assignable to type 'string | URL'.
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<string, any>} */(kuboGateway).trustlessBlockBrokerConfig == null) {
Expand Down
82 changes: 82 additions & 0 deletions src/components/local-gateway-form/LocalGatewayForm.js
Original file line number Diff line number Diff line change
@@ -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 (
<form onSubmit={onSubmit}>
<input
id='local-gateway'
aria-label={t('terms.localGateway')}
placeholder={t('localGatewayForm.placeholder', 'e.g., https://ipfs.example.com')}
type='text'
className={`w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 ${!isValid ? 'focus-outline-red b--red-muted' : 'focus-outline-green b--green-muted'}`}
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
/>
<div className='tr'>
<Button
id='local-gateway-clear-button'
minWidth={100}
height={40}
bg='bg-charcoal'
className='tc'
disabled={value === ''}
onClick={onClear}>
{t('app:actions.clear')}
</Button>
<Button
id='local-gateway-submit-button'
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
disabled={!isValid || !hasChanges}>
{t('actions.submit')}
</Button>
</div>
<p className='f6 charcoal-muted mt2 mb0'>
{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.')}
</p>
</form>
)
}

export default connect(
'doUpdateLocalGateway',
'selectLocalGateway',
withTranslation('app')(LocalGatewayForm)
)
29 changes: 17 additions & 12 deletions src/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -67,19 +68,23 @@ export const SettingsPage = ({

<Box className='mb3 pa4-l pa2'>
<div className='lh-copy charcoal'>
<Title>{t('app:terms.localGateway')}</Title>
<LocalGatewayForm/>
</div>
<div className='lh-copy charcoal mt4'>
<Title>{t('app:terms.publicGateway')}</Title>
<Trans i18nKey='publicSubdomainGatewayDescription' t={t}>
<p>Select a default <a className='link blue' href='https://docs.ipfs.tech/concepts/ipfs-gateway/#subdomain' target='_blank' rel='noopener noreferrer'>Subdomain Gateway</a> for generating shareable links.</p>
</Trans>
<PublicSubdomainGatewayForm/>
</div>
<div className='lh-copy charcoal'>
<Trans i18nKey='publicPathGatewayDescription' t={t}>
<p>Select a fallback <a className='link blue' href='https://docs.ipfs.tech/concepts/ipfs-gateway/#path' target='_blank' rel='noopener noreferrer'>Path Gateway</a> for generating shareable links for CIDs that exceed the 63-character DNS limit.</p>
</Trans>
<PublicGatewayForm/>
</div>
</Box>
<Trans i18nKey='publicSubdomainGatewayDescription' t={t}>
<p>Select a default <a className='link blue' href='https://docs.ipfs.tech/concepts/ipfs-gateway/#subdomain' target='_blank' rel='noopener noreferrer'>Subdomain Gateway</a> for generating shareable links.</p>
</Trans>
<PublicSubdomainGatewayForm/>
</div>
<div className='lh-copy charcoal'>
<Trans i18nKey='publicPathGatewayDescription' t={t}>
<p>Select a fallback <a className='link blue' href='https://docs.ipfs.tech/concepts/ipfs-gateway/#path' target='_blank' rel='noopener noreferrer'>Path Gateway</a> for generating shareable links for CIDs that exceed the 63-character DNS limit.</p>
</Trans>
<PublicGatewayForm/>
</div>
</Box>

<Box className='mb3 pa4-l pa2'>
<Title>{t('ipnsPublishingKeys.title')}</Title>
Expand Down
Loading