feat: policy-controlled push relay URL with optional user lock
This commit is contained in:
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-border rounded-lg">
|
||||
<div className="px-4 py-3 border-b border-border bg-muted/30">
|
||||
<h2 className="text-sm font-medium text-foreground">Push Relay</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Override the Web Push relay URL shown in user notification settings. Leave empty to use the built-in default.</p>
|
||||
</div>
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
<input
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={policy.pushRelayUrl ?? ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!policy.pushRelayUrlLocked}
|
||||
onChange={togglePushRelayLocked}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
<Lock className="w-3 h-3" /> Lock - users cannot change this URL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{categories.map(category => (
|
||||
<div key={category} className="border border-border rounded-lg">
|
||||
<div className="px-4 py-3 border-b border-border bg-muted/30">
|
||||
|
||||
@@ -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<PushStatus>(
|
||||
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() {
|
||||
<SettingsSection title={t('push.title')} description={t('push.description')}>
|
||||
<div className="rounded-md border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-sm font-medium" htmlFor="push-relay-url">
|
||||
<label className="text-sm font-medium inline-flex items-center gap-1.5" htmlFor="push-relay-url">
|
||||
{t('push.relay_label')}
|
||||
{pushRelayLocked && (
|
||||
<Lock className="w-3 h-3 text-muted-foreground" aria-label={t('push.relay_locked')} />
|
||||
)}
|
||||
</label>
|
||||
<PushStatusBadge status={pushStatus} t={t} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('push.relay_desc')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pushRelayLocked ? t('push.relay_locked_desc') : t('push.relay_desc')}
|
||||
</p>
|
||||
<input
|
||||
id="push-relay-url"
|
||||
type="url"
|
||||
@@ -140,7 +159,8 @@ export function NotificationSettings() {
|
||||
value={relayUrl}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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…",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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…",
|
||||
|
||||
Reference in New Issue
Block a user