2026-06-03 23:13:33 -04:00
|
|
|
import React, { useCallback, useRef, useState } from 'react';
|
2026-06-19 18:12:25 -04:00
|
|
|
import {
|
|
|
|
|
Box,
|
|
|
|
|
Button,
|
|
|
|
|
Checkbox,
|
2026-07-02 21:40:07 -04:00
|
|
|
Dialog,
|
|
|
|
|
Header,
|
2026-06-19 18:12:25 -04:00
|
|
|
Icon,
|
|
|
|
|
IconButton,
|
|
|
|
|
Icons,
|
|
|
|
|
Input,
|
2026-07-02 21:40:07 -04:00
|
|
|
Overlay,
|
|
|
|
|
OverlayBackdrop,
|
|
|
|
|
OverlayCenter,
|
2026-06-19 18:12:25 -04:00
|
|
|
Scroll,
|
|
|
|
|
Spinner,
|
|
|
|
|
Text,
|
|
|
|
|
color,
|
|
|
|
|
config,
|
|
|
|
|
} from 'folds';
|
2026-07-02 21:40:07 -04:00
|
|
|
import FocusTrap from 'focus-trap-react';
|
2026-06-03 23:13:33 -04:00
|
|
|
import { Page, PageContent, PageHeader } from '../../components/page';
|
|
|
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
|
|
|
import { useRoom } from '../../hooks/useRoom';
|
|
|
|
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
|
|
|
|
import { StateEvent } from '../../../types/matrix/room';
|
|
|
|
|
import { usePowerLevels, readPowerLevel } from '../../hooks/usePowerLevels';
|
|
|
|
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
|
|
|
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
|
|
|
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
|
|
|
|
import { SequenceCard } from '../../components/sequence-card';
|
|
|
|
|
import { SequenceCardStyle } from '../common-settings/styles.css';
|
2026-07-02 21:40:07 -04:00
|
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
|
|
|
import { useModalStyle } from '../../hooks/useModalStyle';
|
2026-06-03 23:13:33 -04:00
|
|
|
|
|
|
|
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type ServerAclContent = {
|
|
|
|
|
allow: string[];
|
|
|
|
|
deny: string[];
|
|
|
|
|
allow_ip_literals: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DEFAULT_ACL: ServerAclContent = {
|
|
|
|
|
allow: ['*'],
|
|
|
|
|
deny: [],
|
|
|
|
|
allow_ip_literals: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Validation ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-02 21:40:07 -04:00
|
|
|
* Validate a server-name glob for an ACL entry.
|
|
|
|
|
*
|
|
|
|
|
* Matrix ACL `allow`/`deny` entries are globs where `*` (any run of chars) and
|
|
|
|
|
* `?` (single char) may appear ANYWHERE — e.g. `*`, `*.example.com`,
|
|
|
|
|
* `1.2.3.*`, `10.0.0.?`, `*.evil.*`, `*bad*`. We therefore validate the *glob*
|
|
|
|
|
* rather than a concrete hostname:
|
|
|
|
|
* - reject empty / whitespace-only
|
|
|
|
|
* - allow only hostname/IP chars plus the wildcards `*` and `?`
|
|
|
|
|
* (letters, digits, dots, hyphens, colons for ports/IPv6 — NO underscore)
|
|
|
|
|
* - reject consecutive/leading/trailing dots (`...`, `.foo`, `foo.`)
|
|
|
|
|
* - reject entries with no alphanumeric or wildcard char (bare `-`, lone `:`)
|
2026-06-03 23:13:33 -04:00
|
|
|
*/
|
|
|
|
|
function isValidServerPattern(value: string): boolean {
|
2026-07-02 21:40:07 -04:00
|
|
|
const v = value.trim();
|
|
|
|
|
if (!v) return false;
|
|
|
|
|
// Only hostname/IP glob chars — wildcards may appear at any position.
|
|
|
|
|
if (!/^[A-Za-z0-9.:*?-]+$/.test(v)) return false;
|
|
|
|
|
// Structural rules for the dotted parts.
|
|
|
|
|
if (v.startsWith('.') || v.endsWith('.') || v.includes('..')) return false;
|
|
|
|
|
// Must carry actual signal — reject pure punctuation like `-`, `:` or `-.-`.
|
|
|
|
|
if (!/[A-Za-z0-9*?]/.test(v)) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert an ACL glob (`*` = any run, `?` = single char) to an anchored RegExp,
|
|
|
|
|
* escaping every other regex metacharacter. Used only for local self-ban
|
|
|
|
|
* detection — never sent to the server.
|
|
|
|
|
*/
|
|
|
|
|
function globToRegExp(glob: string): RegExp {
|
|
|
|
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
|
|
|
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
|
|
|
// Case-INsensitive: Synapse's glob_to_regex uses IGNORECASE and hostnames are
|
|
|
|
|
// case-insensitive, so a deny like `MATRIX.foo.org` must still be detected as
|
|
|
|
|
// self-banning `matrix.foo.org` (otherwise the warning is a false negative).
|
|
|
|
|
return new RegExp(`^${pattern}$`, 'i');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchesAnyGlob(domain: string, globs: string[]): boolean {
|
|
|
|
|
return globs.some((glob) => {
|
|
|
|
|
try {
|
|
|
|
|
return globToRegExp(glob).test(domain);
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-03 23:13:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Server list sub-component ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type ServerListProps = {
|
|
|
|
|
label: string;
|
|
|
|
|
entries: string[];
|
|
|
|
|
canEdit: boolean;
|
|
|
|
|
onAdd: (value: string) => void;
|
|
|
|
|
onRemove: (index: number) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function ServerList({ label, entries, canEdit, onAdd, onRemove }: ServerListProps) {
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [error, setError] = useState<string | undefined>();
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
const raw = inputRef.current?.value ?? '';
|
|
|
|
|
const value = raw.trim();
|
|
|
|
|
if (!value) return;
|
|
|
|
|
|
|
|
|
|
if (!isValidServerPattern(value)) {
|
2026-07-02 21:40:07 -04:00
|
|
|
setError('Invalid pattern. Use a hostname, IP, or glob (e.g. *.evil.com, 1.2.3.*, 10.0.0.?)');
|
2026-06-03 23:13:33 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setError(undefined);
|
|
|
|
|
onAdd(value);
|
|
|
|
|
if (inputRef.current) inputRef.current.value = '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
|
|
|
if (e.key === 'Enter') handleAdd();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box direction="Column" gap="100">
|
|
|
|
|
<Box alignItems="Center" justifyContent="SpaceBetween">
|
|
|
|
|
<Text size="L400">{label}</Text>
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<Button
|
|
|
|
|
size="300"
|
|
|
|
|
variant="Secondary"
|
|
|
|
|
fill="Soft"
|
|
|
|
|
radii="300"
|
|
|
|
|
before={<Icon src={Icons.Plus} size="100" />}
|
|
|
|
|
onClick={handleAdd}
|
|
|
|
|
>
|
|
|
|
|
<Text size="B300">Add</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<Box direction="Column" gap="100">
|
2026-06-19 18:12:25 -04:00
|
|
|
<Input
|
2026-06-03 23:13:33 -04:00
|
|
|
ref={inputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g. *.example.com or badserver.org"
|
|
|
|
|
onKeyDown={handleKeyDown}
|
2026-06-19 18:12:25 -04:00
|
|
|
variant={error ? 'Critical' : 'Secondary'}
|
|
|
|
|
size="400"
|
|
|
|
|
radii="300"
|
2026-06-03 23:13:33 -04:00
|
|
|
/>
|
|
|
|
|
{error && (
|
|
|
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
|
|
|
{error}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<SequenceCard
|
|
|
|
|
className={SequenceCardStyle}
|
|
|
|
|
variant="SurfaceVariant"
|
|
|
|
|
direction="Column"
|
|
|
|
|
gap="0"
|
|
|
|
|
>
|
|
|
|
|
{entries.length === 0 ? (
|
|
|
|
|
<Text size="T300" priority="300" style={{ padding: `${config.space.S100} 0` }}>
|
|
|
|
|
(empty)
|
|
|
|
|
</Text>
|
|
|
|
|
) : (
|
|
|
|
|
entries.map((entry, i) => (
|
|
|
|
|
<Box
|
|
|
|
|
key={`${entry}-${i}`}
|
|
|
|
|
alignItems="Center"
|
|
|
|
|
justifyContent="SpaceBetween"
|
|
|
|
|
style={{
|
|
|
|
|
padding: `${config.space.S100} 0`,
|
|
|
|
|
borderBottom:
|
|
|
|
|
i < entries.length - 1 ? `1px solid ${color.Surface.ContainerLine}` : undefined,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text size="T300" style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>
|
|
|
|
|
{entry}
|
|
|
|
|
</Text>
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<IconButton
|
|
|
|
|
size="300"
|
|
|
|
|
variant="Background"
|
|
|
|
|
radii="300"
|
|
|
|
|
aria-label={`Remove ${entry}`}
|
|
|
|
|
onClick={() => onRemove(i)}
|
|
|
|
|
style={{ flexShrink: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Cross} size="100" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</SequenceCard>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Main component ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type RoomServerACLProps = {
|
|
|
|
|
requestClose: () => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function RoomServerACL({ requestClose }: RoomServerACLProps) {
|
|
|
|
|
const mx = useMatrixClient();
|
|
|
|
|
const room = useRoom();
|
2026-07-02 21:40:07 -04:00
|
|
|
const modalStyle = useModalStyle(480);
|
2026-06-03 23:13:33 -04:00
|
|
|
|
|
|
|
|
// Power level checks
|
|
|
|
|
const powerLevels = usePowerLevels(room);
|
|
|
|
|
const creators = useRoomCreators(room);
|
|
|
|
|
const permissions = useRoomPermissions(creators, powerLevels);
|
|
|
|
|
const myUserId = mx.getSafeUserId();
|
|
|
|
|
const canEdit = permissions.stateEvent(StateEvent.RoomServerAcl, myUserId);
|
|
|
|
|
|
|
|
|
|
// Read current ACL from room state
|
|
|
|
|
const aclEvent = useStateEvent(room, StateEvent.RoomServerAcl);
|
|
|
|
|
const currentAcl = aclEvent?.getContent<ServerAclContent>() ?? DEFAULT_ACL;
|
|
|
|
|
|
|
|
|
|
// Local draft state — initialised from current ACL
|
|
|
|
|
const [allowList, setAllowList] = useState<string[]>(() => currentAcl.allow ?? ['*']);
|
|
|
|
|
const [denyList, setDenyList] = useState<string[]>(() => currentAcl.deny ?? []);
|
|
|
|
|
const [allowIpLiterals, setAllowIpLiterals] = useState<boolean>(
|
|
|
|
|
() => currentAcl.allow_ip_literals ?? false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Track whether there are unsaved changes
|
|
|
|
|
const isDirty =
|
|
|
|
|
JSON.stringify(allowList) !== JSON.stringify(currentAcl.allow ?? ['*']) ||
|
|
|
|
|
JSON.stringify(denyList) !== JSON.stringify(currentAcl.deny ?? []) ||
|
|
|
|
|
allowIpLiterals !== (currentAcl.allow_ip_literals ?? false);
|
|
|
|
|
|
|
|
|
|
// Save handler
|
|
|
|
|
const [saveState, save] = useAsyncCallback(
|
|
|
|
|
useCallback(async () => {
|
|
|
|
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomServerAcl as any, {
|
|
|
|
|
allow: allowList,
|
|
|
|
|
deny: denyList,
|
|
|
|
|
allow_ip_literals: allowIpLiterals,
|
|
|
|
|
});
|
|
|
|
|
}, [mx, room.roomId, allowList, denyList, allowIpLiterals]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const saving = saveState.status === AsyncStatus.Loading;
|
|
|
|
|
const saveError =
|
|
|
|
|
saveState.status === AsyncStatus.Error ? 'Failed to save ACL. Please try again.' : undefined;
|
|
|
|
|
|
2026-07-02 21:40:07 -04:00
|
|
|
// ── Save guards ───────────────────────────────────────────────────────────
|
|
|
|
|
// #1 Empty allow list denies EVERY server (allow: [] is not "allow all") and
|
|
|
|
|
// partitions the room from all federation irreversibly — block the save.
|
|
|
|
|
const emptyAllow = allowList.length === 0;
|
|
|
|
|
|
|
|
|
|
// #2 Self-ban: the local homeserver must match at least one allow glob and no
|
|
|
|
|
// deny glob, otherwise applying this ACL removes our own server from the room.
|
|
|
|
|
const localDomain = mx.getDomain() ?? '';
|
|
|
|
|
const selfBanned =
|
|
|
|
|
localDomain.length > 0 &&
|
|
|
|
|
(!matchesAnyGlob(localDomain, allowList) || matchesAnyGlob(localDomain, denyList));
|
|
|
|
|
|
|
|
|
|
// #4 Gate the destructive write behind a confirmation dialog.
|
|
|
|
|
const [prompt, setPrompt] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleConfirmSave = () => {
|
|
|
|
|
setPrompt(false);
|
|
|
|
|
save();
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-03 23:13:33 -04:00
|
|
|
// Required power level for this state event
|
|
|
|
|
const requiredPL = readPowerLevel.state(powerLevels, StateEvent.RoomServerAcl);
|
|
|
|
|
const myPL = readPowerLevel.user(powerLevels, myUserId);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Page>
|
|
|
|
|
<PageHeader outlined={false}>
|
|
|
|
|
<Box grow="Yes" gap="200">
|
|
|
|
|
<Box grow="Yes" alignItems="Center" gap="200">
|
|
|
|
|
<Icon src={Icons.Shield} size="200" />
|
|
|
|
|
<Text as="h2" size="H3" truncate>
|
|
|
|
|
Server ACL
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box shrink="No" gap="200" alignItems="Center">
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<Button
|
|
|
|
|
size="400"
|
|
|
|
|
variant="Primary"
|
|
|
|
|
fill="Solid"
|
|
|
|
|
radii="300"
|
2026-07-02 21:40:07 -04:00
|
|
|
disabled={saving || !isDirty || emptyAllow}
|
|
|
|
|
onClick={() => setPrompt(true)}
|
2026-06-03 23:13:33 -04:00
|
|
|
before={saving ? <Spinner size="200" /> : <Icon src={Icons.Check} size="100" />}
|
|
|
|
|
>
|
|
|
|
|
<Text size="B400">{saving ? 'Saving…' : 'Save Changes'}</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
|
|
|
|
<Icon src={Icons.Cross} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</PageHeader>
|
|
|
|
|
|
|
|
|
|
<Box grow="Yes">
|
|
|
|
|
<Scroll hideTrack visibility="Hover">
|
|
|
|
|
<PageContent>
|
|
|
|
|
<Box direction="Column" gap="700">
|
|
|
|
|
{/* Info banner */}
|
|
|
|
|
<SequenceCard
|
|
|
|
|
className={SequenceCardStyle}
|
|
|
|
|
variant="SurfaceVariant"
|
|
|
|
|
direction="Column"
|
|
|
|
|
gap="200"
|
|
|
|
|
>
|
|
|
|
|
<Box gap="200" alignItems="Start">
|
|
|
|
|
<Icon src={Icons.Warning} size="200" style={{ flexShrink: 0, marginTop: 2 }} />
|
|
|
|
|
<Box direction="Column" gap="100">
|
|
|
|
|
<Text size="T300">
|
|
|
|
|
Server ACL controls which servers can participate in this room. Changes take
|
|
|
|
|
effect immediately.
|
|
|
|
|
</Text>
|
|
|
|
|
{!canEdit && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
|
|
|
|
You need power level {requiredPL} to edit the ACL (your level: {myPL}).
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</SequenceCard>
|
|
|
|
|
|
|
|
|
|
{/* Save error */}
|
|
|
|
|
{saveError && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
|
|
|
|
{saveError}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-07-02 21:40:07 -04:00
|
|
|
{/* #1 Empty allow list guard — blocks save */}
|
|
|
|
|
{canEdit && emptyAllow && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
|
|
|
|
The allow list is empty. An empty allow list denies every server and partitions
|
|
|
|
|
this room from all federation permanently. Add at least one entry (use
|
|
|
|
|
"*" to allow all servers).
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* #2 Self-ban warning — save allowed but confirmation required */}
|
|
|
|
|
{canEdit && !emptyAllow && selfBanned && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Warning.Main }}>
|
|
|
|
|
Warning: your own homeserver ({localDomain}) is not permitted by this ACL.
|
|
|
|
|
Applying it will remove your server from the room and you may lose the ability to
|
|
|
|
|
moderate it.
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-06-03 23:13:33 -04:00
|
|
|
{/* Allow IP literals toggle */}
|
|
|
|
|
<Box direction="Column" gap="100">
|
|
|
|
|
<Text size="L400">IP Address Access</Text>
|
|
|
|
|
<SequenceCard
|
|
|
|
|
className={SequenceCardStyle}
|
|
|
|
|
variant="SurfaceVariant"
|
|
|
|
|
direction="Column"
|
|
|
|
|
gap="200"
|
|
|
|
|
>
|
|
|
|
|
<Box alignItems="Center" gap="300">
|
2026-06-19 18:12:25 -04:00
|
|
|
<Checkbox
|
2026-06-03 23:13:33 -04:00
|
|
|
id="allow-ip-literals"
|
|
|
|
|
checked={allowIpLiterals}
|
|
|
|
|
disabled={!canEdit}
|
2026-06-19 18:12:25 -04:00
|
|
|
onClick={() => setAllowIpLiterals(!allowIpLiterals)}
|
|
|
|
|
size="300"
|
|
|
|
|
variant="Primary"
|
2026-06-03 23:13:33 -04:00
|
|
|
/>
|
|
|
|
|
<Box direction="Column" gap="0">
|
|
|
|
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="allow-ip-literals"
|
|
|
|
|
style={{ cursor: canEdit ? 'pointer' : 'default' }}
|
|
|
|
|
>
|
|
|
|
|
<Text size="T300">Allow IP literal addresses</Text>
|
|
|
|
|
</label>
|
|
|
|
|
<Text size="T200" priority="300">
|
|
|
|
|
When disabled, clients connecting via raw IP addresses (e.g. 1.2.3.4) cannot
|
|
|
|
|
participate.
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</SequenceCard>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* Allowed servers */}
|
|
|
|
|
<ServerList
|
|
|
|
|
label="Allowed Servers"
|
|
|
|
|
entries={allowList}
|
|
|
|
|
canEdit={canEdit}
|
|
|
|
|
onAdd={(value) => setAllowList((prev) => [...prev, value])}
|
|
|
|
|
onRemove={(index) => setAllowList((prev) => prev.filter((_, i) => i !== index))}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Denied servers */}
|
|
|
|
|
<ServerList
|
|
|
|
|
label="Denied Servers"
|
|
|
|
|
entries={denyList}
|
|
|
|
|
canEdit={canEdit}
|
|
|
|
|
onAdd={(value) => setDenyList((prev) => [...prev, value])}
|
|
|
|
|
onRemove={(index) => setDenyList((prev) => prev.filter((_, i) => i !== index))}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Note about defaults */}
|
|
|
|
|
<Text size="T200" priority="300">
|
|
|
|
|
Tip: The default ACL allows all servers (allow: ["*"], deny: []). Adding
|
|
|
|
|
"*" to the allow list permits all servers not explicitly denied.
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
</PageContent>
|
|
|
|
|
</Scroll>
|
|
|
|
|
</Box>
|
2026-07-02 21:40:07 -04:00
|
|
|
|
|
|
|
|
{/* #4 Confirmation dialog — surfaces the empty-allow (#1) and self-ban (#2)
|
|
|
|
|
warnings and keeps a safe save one extra click. */}
|
|
|
|
|
{prompt && (
|
|
|
|
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
|
|
|
<OverlayCenter>
|
|
|
|
|
<FocusTrap
|
|
|
|
|
focusTrapOptions={{
|
|
|
|
|
initialFocus: false,
|
|
|
|
|
onDeactivate: () => setPrompt(false),
|
|
|
|
|
clickOutsideDeactivates: true,
|
|
|
|
|
escapeDeactivates: stopPropagation,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Dialog
|
|
|
|
|
variant="Surface"
|
|
|
|
|
aria-labelledby="server-acl-confirm-title"
|
|
|
|
|
style={modalStyle}
|
|
|
|
|
>
|
|
|
|
|
<Header
|
|
|
|
|
style={{
|
|
|
|
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
|
|
|
|
borderBottomWidth: config.borderWidth.B300,
|
|
|
|
|
}}
|
|
|
|
|
variant="Surface"
|
|
|
|
|
size="500"
|
|
|
|
|
>
|
|
|
|
|
<Box grow="Yes">
|
|
|
|
|
<Text as="h2" size="H4" id="server-acl-confirm-title">
|
|
|
|
|
Apply Server ACL
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="300"
|
|
|
|
|
onClick={() => setPrompt(false)}
|
|
|
|
|
radii="300"
|
|
|
|
|
aria-label="Cancel"
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Cross} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Header>
|
|
|
|
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
|
|
|
|
<Box direction="Column" gap="200">
|
|
|
|
|
<Text priority="400">
|
|
|
|
|
Server ACL changes take effect immediately and control which servers can
|
|
|
|
|
participate in this room. This cannot be undone by other servers once they are
|
|
|
|
|
removed.
|
|
|
|
|
</Text>
|
|
|
|
|
{emptyAllow && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
|
|
|
|
The allow list is empty — this would deny every server and partition the
|
|
|
|
|
room from all federation permanently.
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
{!emptyAllow && selfBanned && (
|
|
|
|
|
<Text size="T300" style={{ color: color.Warning.Main }}>
|
|
|
|
|
Warning: your own homeserver ({localDomain}) is not permitted by this ACL.
|
|
|
|
|
Applying it will remove your server from the room and you may lose the
|
|
|
|
|
ability to moderate it.
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant={selfBanned ? 'Critical' : 'Primary'}
|
|
|
|
|
onClick={handleConfirmSave}
|
|
|
|
|
disabled={emptyAllow}
|
|
|
|
|
>
|
|
|
|
|
<Text size="B400">Apply ACL</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</FocusTrap>
|
|
|
|
|
</OverlayCenter>
|
|
|
|
|
</Overlay>
|
|
|
|
|
)}
|
2026-06-03 23:13:33 -04:00
|
|
|
</Page>
|
|
|
|
|
);
|
|
|
|
|
}
|