Threema Gateway
All checks were successful
Build and Push / build (push) Successful in 1m30s

This commit is contained in:
2026-05-16 22:00:27 +02:00
parent 726151d90b
commit 85c4302f7a
8 changed files with 914 additions and 8 deletions

View File

@@ -8,12 +8,26 @@ import { useRouter } from "next/navigation";
const CHANNEL_ID_HELP: Record<string, string> = {
telegram: "telegramIdHelp",
discord: "discordIdHelp",
threema: "threemaIdHelp",
// email entry dropped in the Phase A rework — IMAP/SMTP is handled by
// the `mail` skill (category=skill, not channel), so it never appears
// in `enabledChannels`. If a future channel is added to the catalog,
// give it an entry here so the help blurb renders.
};
/**
* Channels whose user list is managed through a dedicated endpoint
* instead of the generic PATCH /api/tenants/:name flow.
*
* Threema is the only one today — adding/removing a Threema ID has to
* synchronise with the central pieced-threema-gateway relay's `routes`
* table (uniqueness enforced there, not in K8s). The
* /api/tenants/:name/threema/routes endpoint owns that two-step
* coordination (relay first, then K8s, with compensation on K8s
* failure). Other channels just patch the K8s spec directly.
*/
const RELAY_MANAGED_CHANNELS = new Set(["threema"]);
interface ChannelUsersProps {
tenantName: string;
/** Currently enabled channel packages (e.g. ["telegram", "discord"]) */
@@ -63,6 +77,70 @@ export function ChannelUsers({
[tenantName, router]
);
/**
* Threema (and any future relay-managed channel) uses a dedicated
* endpoint that synchronises the central relay's routes table with
* the K8s spec atomically. We call it per-ID rather than sending the
* whole array because uniqueness is enforced ID-by-ID at the relay,
* and the error UX of "this ID is taken" is per-add.
*/
const addToRelayChannel = useCallback(
async (channel: string, threemaId: string) => {
setSaving(true);
setError("");
try {
const res = await fetch(
`/api/tenants/${tenantName}/${channel}/routes`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ threemaId }),
}
);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Add failed (HTTP ${res.status})`);
}
setChannelUsers((prev) => {
const current = prev[channel] ?? [];
if (current.includes(threemaId)) return prev;
return { ...prev, [channel]: [...current, threemaId] };
});
router.refresh();
} catch (e: any) {
setError(e.message);
} finally {
setSaving(false);
}
},
[tenantName, router]
);
const removeFromRelayChannel = useCallback(
async (channel: string, threemaId: string) => {
setSaving(true);
setError("");
try {
const url = `/api/tenants/${tenantName}/${channel}/routes?threemaId=${encodeURIComponent(threemaId)}`;
const res = await fetch(url, { method: "DELETE" });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Remove failed (HTTP ${res.status})`);
}
setChannelUsers((prev) => {
const current = prev[channel] ?? [];
return { ...prev, [channel]: current.filter((id) => id !== threemaId) };
});
router.refresh();
} catch (e: any) {
setError(e.message);
} finally {
setSaving(false);
}
},
[tenantName, router]
);
const handleAdd = useCallback(
(channel: string) => {
const userId = inputValues[channel]?.trim();
@@ -74,18 +152,27 @@ export function ChannelUsers({
return;
}
const updated = {
...channelUsers,
[channel]: [...current, userId],
};
setInputValues((prev) => ({ ...prev, [channel]: "" }));
updateChannelUsers(updated);
if (RELAY_MANAGED_CHANNELS.has(channel)) {
void addToRelayChannel(channel, userId);
} else {
const updated = {
...channelUsers,
[channel]: [...current, userId],
};
updateChannelUsers(updated);
}
},
[channelUsers, inputValues, updateChannelUsers, t]
[channelUsers, inputValues, updateChannelUsers, addToRelayChannel, t]
);
const handleRemove = useCallback(
(channel: string, userId: string) => {
if (RELAY_MANAGED_CHANNELS.has(channel)) {
void removeFromRelayChannel(channel, userId);
return;
}
const current = channelUsers[channel] || [];
const updated = {
...channelUsers,
@@ -93,7 +180,7 @@ export function ChannelUsers({
};
updateChannelUsers(updated);
},
[channelUsers, updateChannelUsers]
[channelUsers, updateChannelUsers, removeFromRelayChannel]
);
if (enabledChannels.length === 0) return null;