diff --git a/app/(main)/admin/_tabs/policy.tsx b/app/(main)/admin/_tabs/policy.tsx index 78755819..f6e12307 100644 --- a/app/(main)/admin/_tabs/policy.tsx +++ b/app/(main)/admin/_tabs/policy.tsx @@ -74,6 +74,18 @@ export function PolicyTab() { setMessage(null); } + function setPushRelayUrl(value: string) { + setPolicy(prev => ({ ...prev, pushRelayUrl: value })); + setDirty(true); + setMessage(null); + } + + function togglePushRelayLocked() { + setPolicy(prev => ({ ...prev, pushRelayUrlLocked: !prev.pushRelayUrlLocked })); + setDirty(true); + setMessage(null); + } + function toggleLocked(settingKey: string) { setPolicy(prev => { const existing = prev.restrictions[settingKey] || {}; @@ -183,6 +195,34 @@ export function PolicyTab() { +
+
+

Push Relay

+

Override the Web Push relay URL shown in user notification settings. Leave empty to use the built-in default.

+
+
+ setPushRelayUrl(e.target.value)} + placeholder="https://notifications.relay.example.com" + className="w-full rounded border border-input bg-background px-3 py-2 text-sm" + /> + +
+
+ {categories.map(category => (
diff --git a/components/settings/notification-settings.tsx b/components/settings/notification-settings.tsx index 56c83c3e..f4874804 100644 --- a/components/settings/notification-settings.tsx +++ b/components/settings/notification-settings.tsx @@ -7,7 +7,7 @@ import { SettingsSection, SettingItem, ToggleSwitch, Select } from './settings-s import { playNotificationSound, NOTIFICATION_SOUNDS } from '@/lib/notification-sound'; import type { NotificationSoundChoice } from '@/lib/notification-sound'; import { Button } from '@/components/ui/button'; -import { CheckCircle2, Volume2, XCircle } from 'lucide-react'; +import { CheckCircle2, Lock, Volume2, XCircle } from 'lucide-react'; import { usePolicyStore } from '@/stores/policy-store'; import { useAuthStore } from '@/stores/auth-store'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; @@ -40,16 +40,30 @@ export function NotificationSettings() { updateSetting, } = useSettingsStore(); const { isSettingLocked, isSettingHidden } = usePolicyStore(); + const adminPushRelayUrl = usePolicyStore((s) => s.policy.pushRelayUrl); + const pushRelayLocked = usePolicyStore((s) => s.policy.pushRelayUrlLocked) === true; const client = useAuthStore((s) => s.client); const username = useAuthStore((s) => s.username); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); const supported = typeof window !== 'undefined' && isWebPushSupported(); - const [relayUrl, setRelayUrl] = useState(DEFAULT_RELAY_BASE_URL); + const adminUrl = (adminPushRelayUrl ?? '').trim(); + const [relayUrl, setRelayUrl] = useState(adminUrl || DEFAULT_RELAY_BASE_URL); const [pushStatus, setPushStatus] = useState( supported ? { kind: 'idle' } : { kind: 'unsupported' }, ); + // Pull the admin-configured URL into local state when policy loads/changes. + // When locked, the admin value always wins; when only set (not locked), use + // it as the initial default but let the user override. + useEffect(() => { + if (pushRelayLocked && adminUrl) { + setRelayUrl(adminUrl); + } else if (adminUrl) { + setRelayUrl((current) => (current === DEFAULT_RELAY_BASE_URL ? adminUrl : current)); + } + }, [adminUrl, pushRelayLocked]); + useEffect(() => { if (!supported) return; if (!client) return; @@ -125,12 +139,17 @@ export function NotificationSettings() {
-
-

{t('push.relay_desc')}

+

+ {pushRelayLocked ? t('push.relay_locked_desc') : t('push.relay_desc')} +

setRelayUrl(e.target.value)} placeholder={t('push.relay_placeholder')} - disabled={busy || pushStatus.kind === 'unsupported'} + disabled={busy || pushStatus.kind === 'unsupported' || pushRelayLocked} + readOnly={pushRelayLocked} className="w-full rounded border bg-background px-3 py-2 text-sm disabled:opacity-50" />
diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 57e8535b..09d496c9 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -106,6 +106,10 @@ export interface SettingsPolicy { approvedPlugins: string[]; /** Theme IDs that are force-enabled (users cannot deactivate) */ forceEnabledThemes: string[]; + /** Web Push relay base URL shown to users. Empty means the built-in default. */ + pushRelayUrl?: string; + /** When true, users cannot change pushRelayUrl in notification settings. */ + pushRelayUrlLocked?: boolean; } export const DEFAULT_POLICY: SettingsPolicy = { @@ -116,6 +120,8 @@ export const DEFAULT_POLICY: SettingsPolicy = { forceEnabledPlugins: [], approvedPlugins: [], forceEnabledThemes: [], + pushRelayUrl: '', + pushRelayUrlLocked: false, }; export interface AuditEntry { diff --git a/locales/cs/common.json b/locales/cs/common.json index 6a989409..7a9eb55d 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -955,6 +955,8 @@ "reenable": "Znovu zaregistrovat", "relay_desc": "Výchozí je hostovaný relay Bulwark. Změňte pouze pokud používáte vlastní hosting.", "relay_label": "Push relay", + "relay_locked": "Nastaveno administrátorem", + "relay_locked_desc": "URL push relay byla nastavena administrátorem a nelze ji změnit.", "relay_placeholder": "https://notifications.relay.example.com", "status_active": "Aktivní na tomto zařízení", "status_busy": "Pracuji…", diff --git a/locales/en/common.json b/locales/en/common.json index a7d3a38c..1dbba593 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -927,6 +927,8 @@ "description": "Receive system notifications for new mail when this site is closed. Delivered via the Bulwark push relay; the relay never sees mail content.", "relay_label": "Push relay", "relay_desc": "Defaults to the hosted Bulwark relay. Change only if you self-host.", + "relay_locked": "Set by administrator", + "relay_locked_desc": "The push relay URL has been set by your administrator and cannot be changed.", "relay_placeholder": "https://notifications.relay.example.com", "status_active": "Active on this device", "status_inactive": "Not enabled on this device", diff --git a/locales/nl/common.json b/locales/nl/common.json index 351b56d7..43679c43 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -955,6 +955,8 @@ "reenable": "Opnieuw registreren", "relay_desc": "Gebruikt standaard de gehoste Bulwark-relay. Wijzig alleen als je zelf hostt.", "relay_label": "Push-relay", + "relay_locked": "Ingesteld door beheerder", + "relay_locked_desc": "De push-relay-URL is ingesteld door je beheerder en kan niet worden gewijzigd.", "relay_placeholder": "https://notifications.relay.example.com", "status_active": "Actief op dit apparaat", "status_busy": "Bezig…",