'use client'; import { useCallback, useEffect, useState } from 'react'; import { Loader2 } from '@/components/icons'; import { SettingsSection, SettingItem, ToggleSwitch } from '@/components/settings/settings-section'; import { ConfigNote, RevertButton, SourceBadge } from '@/components/admin/config-items'; import { AdminLink } from '@/components/admin/admin-link'; import { apiFetch } from '@/lib/browser-navigation'; import type { OutgoingHost } from '@/lib/admin/outgoing-hosts-registry'; import type { ReachableHost } from '@/lib/admin/reachable-hosts'; import type { OrgPolicyDraft } from '../use-org-policy-draft'; interface OutgoingHostsState { noThirdPartyTraffic: boolean; source: 'env' | 'admin' | 'default'; readOnly: boolean; envLocked: boolean; builtIn: OutgoingHost[]; reachable: ReachableHost[]; networkPlugins: { id: string; name: string }[]; } const SIDE_LABEL: Record = { server: 'this server', browser: 'the browser', both: 'server and browser', }; /** * Organisation > Security > Third-party traffic (#903). The switch is config, * not policy: it is saved through /api/admin/config like the Server page's * keys, applies to the whole server whatever scope the scope bar shows, and * is read-only when the environment or a read-only config sets it. */ export function ThirdPartySection({ draft }: { draft: OrgPolicyDraft }) { const [state, setState] = useState(null); const [failed, setFailed] = useState(false); const [saving, setSaving] = useState(false); const { setMessage } = draft; const load = useCallback(async () => { try { const res = await apiFetch('/api/admin/outgoing-hosts'); if (!res.ok) throw new Error(); setState(await res.json()); setFailed(false); } catch { setFailed(true); } }, []); useEffect(() => { void load(); }, [load]); async function save(on: boolean) { setSaving(true); try { const res = await apiFetch('/api/admin/config', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ noThirdPartyTraffic: on }), }); if (res.ok) { setMessage({ type: 'success', text: on ? 'Third-party traffic is off. The server stops at once; browsers follow the next time Bulwark loads.' : 'Third-party traffic is allowed again.', }); } else { const data = await res.json().catch(() => ({})); setMessage({ type: 'error', text: data.error || 'Could not save the setting' }); } } catch { setMessage({ type: 'error', text: 'Could not save the setting' }); } finally { await load(); setSaving(false); } } async function revert() { setSaving(true); try { const res = await apiFetch('/api/admin/config', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: 'noThirdPartyTraffic' }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setMessage({ type: 'error', text: data.error || 'Could not revert the setting' }); } } catch { setMessage({ type: 'error', text: 'Could not revert the setting' }); } finally { await load(); setSaving(false); } } const on = !!state?.noThirdPartyTraffic; const locked = !!state && (state.readOnly || state.envLocked); const relays = state?.reachable.filter((h) => h.kind === 'Push relay') ?? []; return (

This switch is for the whole server, not per domain or group: most of what it stops, such as telemetry and the version check, runs for no particular user.

{!state && !failed && (
Loading
)} {failed &&

Could not load the outgoing hosts.

} {state && ( <>
void save(checked)} /> {state.source === 'admin' && !locked && void revert()} />}
{state.envLocked && ( Set by NO_THIRD_PARTY_TRAFFIC in the environment. Change it there and restart Bulwark. )} {state.readOnly && !state.envLocked && ( The configuration is read-only. Set noThirdPartyTraffic in config.json, or NO_THIRD_PARTY_TRAFFIC in the environment. )} {on && relays.length === 0 && ( Push notifications now need a relay you run yourself. Add one under{' '} User policy > Push relays; until then users get new mail only while Bulwark is open. )} {on && state.networkPlugins.length > 0 && ( Not running because they declare network access: {state.networkPlugins.map((p) => p.name).join(', ')}. )}

{on ? 'Still reachable' : 'Stays reachable with the switch on'}

Hosts you configured yourself, and what users enter themselves.

{state.reachable.length === 0 ? (

None configured.

) : (
    {state.reachable.map((h) => (
  • {h.kind} {h.host} {h.setIn} ยท {SIDE_LABEL[h.side]} {h.note && {h.note}}
  • ))}
)}
Built-in outside hosts ({state.builtIn.length}){on ? ', all off' : ''}
    {state.builtIn.map((h) => (
  • {h.host} {h.side === 'server' ? 'this server' : 'the browser'}

    {h.usedBy}

    {on ? h.inMode : `Without the switch: ${h.off}`}

  • ))}
)}
); }