From dbfa7560cf79f0fe6485b9e254384929a8aeee5d Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 12 Apr 2026 13:47:27 +0200 Subject: [PATCH] All the channel approval --- deploy/patch-i18n-channel-users.mjs | 66 ++++++ src/app/[locale]/tenants/[name]/page.tsx | 23 ++- src/app/api/tenants/[name]/route.ts | 2 + .../channel-users/channel-users.tsx | 192 ++++++++++++++++++ src/messages/de.json | 12 ++ src/messages/en.json | 12 ++ src/messages/fr.json | 12 ++ src/messages/it.json | 12 ++ src/types/index.ts | 1 + 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 deploy/patch-i18n-channel-users.mjs create mode 100644 src/components/channel-users/channel-users.tsx diff --git a/deploy/patch-i18n-channel-users.mjs b/deploy/patch-i18n-channel-users.mjs new file mode 100644 index 0000000..c894239 --- /dev/null +++ b/deploy/patch-i18n-channel-users.mjs @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * Run: node patch-i18n-channel-users.mjs + * Adds channelUsers i18n keys to all 4 message files. + * Run from the pieced-portal root. + */ +import { readFileSync, writeFileSync } from "fs"; + +const newKeys = { + en: { + title: "Authorized Users", + description: "Manage which users can interact with your assistant on each channel. Add their numeric user ID to authorize access.", + users: "users", + placeholder: "Enter numeric user ID…", + add: "Add", + remove: "Remove", + alreadyAdded: "This user ID is already authorized.", + telegramIdHelp: "To find your Telegram user ID:\n1. Open Telegram and message @userinfobot\n2. It instantly replies with your numeric ID\n3. Enter that number here", + discordIdHelp: "To find your Discord user ID:\n1. Enable Developer Mode in Discord settings (Advanced)\n2. Right-click your name → Copy User ID\n3. Enter that number here", + emailIdHelp: "Enter the email address that should be authorized to interact with the assistant.", + }, + de: { + title: "Autorisierte Benutzer", + description: "Verwalten Sie, welche Benutzer mit Ihrem Assistenten auf jedem Kanal interagieren können. Fügen Sie die numerische Benutzer-ID hinzu, um den Zugang zu autorisieren.", + users: "Benutzer", + placeholder: "Numerische Benutzer-ID eingeben…", + add: "Hinzufügen", + remove: "Entfernen", + alreadyAdded: "Diese Benutzer-ID ist bereits autorisiert.", + telegramIdHelp: "So finden Sie Ihre Telegram-Benutzer-ID:\n1. Öffnen Sie Telegram und schreiben Sie @userinfobot\n2. Der Bot antwortet sofort mit Ihrer numerischen ID\n3. Geben Sie diese Nummer hier ein", + discordIdHelp: "So finden Sie Ihre Discord-Benutzer-ID:\n1. Aktivieren Sie den Entwicklermodus in den Discord-Einstellungen (Erweitert)\n2. Rechtsklick auf Ihren Namen → Benutzer-ID kopieren\n3. Geben Sie diese Nummer hier ein", + emailIdHelp: "Geben Sie die E-Mail-Adresse ein, die zur Interaktion mit dem Assistenten autorisiert werden soll.", + }, + fr: { + title: "Utilisateurs autorisés", + description: "Gérez les utilisateurs pouvant interagir avec votre assistant sur chaque canal. Ajoutez leur identifiant numérique pour autoriser l'accès.", + users: "utilisateurs", + placeholder: "Entrez l'identifiant numérique…", + add: "Ajouter", + remove: "Supprimer", + alreadyAdded: "Cet identifiant est déjà autorisé.", + telegramIdHelp: "Pour trouver votre identifiant Telegram :\n1. Ouvrez Telegram et envoyez un message à @userinfobot\n2. Il répond instantanément avec votre identifiant numérique\n3. Entrez ce numéro ici", + discordIdHelp: "Pour trouver votre identifiant Discord :\n1. Activez le mode développeur dans les paramètres Discord (Avancé)\n2. Clic droit sur votre nom → Copier l'identifiant\n3. Entrez ce numéro ici", + emailIdHelp: "Entrez l'adresse e-mail qui doit être autorisée à interagir avec l'assistant.", + }, + it: { + title: "Utenti autorizzati", + description: "Gestisci quali utenti possono interagire con il tuo assistente su ogni canale. Aggiungi il loro ID numerico per autorizzare l'accesso.", + users: "utenti", + placeholder: "Inserisci l'ID numerico…", + add: "Aggiungi", + remove: "Rimuovi", + alreadyAdded: "Questo ID utente è già autorizzato.", + telegramIdHelp: "Per trovare il tuo ID Telegram:\n1. Apri Telegram e invia un messaggio a @userinfobot\n2. Risponde istantaneamente con il tuo ID numerico\n3. Inserisci quel numero qui", + discordIdHelp: "Per trovare il tuo ID Discord:\n1. Attiva la Modalità sviluppatore nelle impostazioni Discord (Avanzate)\n2. Clic destro sul tuo nome → Copia ID utente\n3. Inserisci quel numero qui", + emailIdHelp: "Inserisci l'indirizzo e-mail che deve essere autorizzato a interagire con l'assistente.", + }, +}; + +for (const [lang, keys] of Object.entries(newKeys)) { + const path = `src/messages/${lang}.json`; + const json = JSON.parse(readFileSync(path, "utf8")); + json.channelUsers = keys; + writeFileSync(path, JSON.stringify(json, null, 2) + "\n"); + console.log(`Patched ${path} — added channelUsers section`); +} diff --git a/src/app/[locale]/tenants/[name]/page.tsx b/src/app/[locale]/tenants/[name]/page.tsx index 8703053..20a2b81 100644 --- a/src/app/[locale]/tenants/[name]/page.tsx +++ b/src/app/[locale]/tenants/[name]/page.tsx @@ -6,6 +6,9 @@ import { StatusBadge } from "@/components/ui/status-badge"; import { UsageDisplay } from "@/components/dashboard/usage-display"; import { PackageList } from "@/components/packages/package-list"; import { WorkspaceEditor } from "@/components/packages/workspace-editor"; +import { ChannelUsers } from "@/components/channel-users/channel-users"; + +const CHANNEL_PACKAGES = ["telegram", "discord", "email"]; export default async function TenantDetailPage({ params, @@ -20,7 +23,6 @@ export default async function TenantDetailPage({ const tenant = await getTenant(name); if (!tenant) notFound(); - console.log("tenant spec:", JSON.stringify(tenant.spec)); // Scope check if ( @@ -32,6 +34,10 @@ export default async function TenantDetailPage({ const enabledPackages = tenant.spec.packages || []; const workspaceFiles = tenant.spec.workspaceFiles || {}; + const enabledChannels = enabledPackages.filter((pkg) => + CHANNEL_PACKAGES.includes(pkg) + ); + const channelUsers = tenant.spec.channelUsers || {}; return (
@@ -70,8 +76,19 @@ export default async function TenantDetailPage({ /> + {/* Channel Users (authorized users per channel) */} + {enabledChannels.length > 0 && ( +
+ +
+ )} + {/* Workspace files */} -
+

{t("workspaceFiles")}

@@ -79,4 +96,4 @@ export default async function TenantDetailPage({
); -} +} \ No newline at end of file diff --git a/src/app/api/tenants/[name]/route.ts b/src/app/api/tenants/[name]/route.ts index b3b8412..dad03b7 100644 --- a/src/app/api/tenants/[name]/route.ts +++ b/src/app/api/tenants/[name]/route.ts @@ -67,6 +67,8 @@ export async function PATCH( if (body.displayName !== undefined) specPatch.displayName = body.displayName; if (body.agentName !== undefined) specPatch.agentName = body.agentName; + if (body.channelUsers !== undefined) + specPatch.channelUsers = body.channelUsers; const updated = await patchTenantSpec(name, specPatch); return NextResponse.json(updated); diff --git a/src/components/channel-users/channel-users.tsx b/src/components/channel-users/channel-users.tsx new file mode 100644 index 0000000..a4ca580 --- /dev/null +++ b/src/components/channel-users/channel-users.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; + +/** Maps channel IDs to the instructions for finding the user ID. */ +const CHANNEL_ID_HELP: Record = { + telegram: "telegramIdHelp", + discord: "discordIdHelp", + email: "emailIdHelp", +}; + +interface ChannelUsersProps { + tenantName: string; + /** Currently enabled channel packages (e.g. ["telegram", "discord"]) */ + enabledChannels: string[]; + /** Current channelUsers from the PiecedTenant spec */ + initialChannelUsers: Record; +} + +export function ChannelUsers({ + tenantName, + enabledChannels, + initialChannelUsers, +}: ChannelUsersProps) { + const t = useTranslations("channelUsers"); + const router = useRouter(); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + const [inputValues, setInputValues] = useState>({}); + const [channelUsers, setChannelUsers] = + useState>(initialChannelUsers); + + const updateChannelUsers = useCallback( + async (updated: Record) => { + setSaving(true); + setError(""); + try { + const res = await fetch(`/api/tenants/${tenantName}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ channelUsers: updated }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Update failed"); + } + setChannelUsers(updated); + router.refresh(); + } catch (e: any) { + setError(e.message); + } finally { + setSaving(false); + } + }, + [tenantName, router] + ); + + const handleAdd = useCallback( + (channel: string) => { + const userId = inputValues[channel]?.trim(); + if (!userId) return; + + const current = channelUsers[channel] || []; + if (current.includes(userId)) { + setError(t("alreadyAdded")); + return; + } + + const updated = { + ...channelUsers, + [channel]: [...current, userId], + }; + setInputValues((prev) => ({ ...prev, [channel]: "" })); + updateChannelUsers(updated); + }, + [channelUsers, inputValues, updateChannelUsers, t] + ); + + const handleRemove = useCallback( + (channel: string, userId: string) => { + const current = channelUsers[channel] || []; + const updated = { + ...channelUsers, + [channel]: current.filter((id) => id !== userId), + }; + updateChannelUsers(updated); + }, + [channelUsers, updateChannelUsers] + ); + + if (enabledChannels.length === 0) return null; + + return ( +
+
+

+ {t("title")} +

+

{t("description")}

+
+ + {error && ( +
+ {error} + +
+ )} + + {enabledChannels.map((channel) => { + const users = channelUsers[channel] || []; + const helpKey = CHANNEL_ID_HELP[channel]; + + return ( +
+
+

+ {channel} +

+ + {users.length} {t("users")} + +
+ + {helpKey && ( +

+ {t(helpKey)} +

+ )} + + {/* Current users */} + {users.length > 0 && ( +
+ {users.map((userId) => ( + + {userId} + + + ))} +
+ )} + + {/* Add user */} +
+ + setInputValues((prev) => ({ + ...prev, + [channel]: e.target.value, + })) + } + onKeyDown={(e) => { + if (e.key === "Enter") handleAdd(channel); + }} + placeholder={t("placeholder")} + className="flex-1 px-3 py-2 bg-surface-1 border border-border rounded-lg text-sm text-text-primary font-mono placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors" + /> + +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/src/messages/de.json b/src/messages/de.json index 05f93d0..d60ba4e 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -237,5 +237,17 @@ "statusHealthy": "OK", "statusDown": "Ausgefallen", "spendChf": "Kosten (CHF)" + }, + "channelUsers": { + "title": "Autorisierte Benutzer", + "description": "Verwalten Sie, welche Benutzer mit Ihrem Assistenten auf jedem Kanal interagieren können. Fügen Sie die numerische Benutzer-ID hinzu, um den Zugang zu autorisieren.", + "users": "Benutzer", + "placeholder": "Numerische Benutzer-ID eingeben…", + "add": "Hinzufügen", + "remove": "Entfernen", + "alreadyAdded": "Diese Benutzer-ID ist bereits autorisiert.", + "telegramIdHelp": "So finden Sie Ihre Telegram-Benutzer-ID:\n1. Öffnen Sie Telegram und schreiben Sie @userinfobot\n2. Der Bot antwortet sofort mit Ihrer numerischen ID\n3. Geben Sie diese Nummer hier ein", + "discordIdHelp": "So finden Sie Ihre Discord-Benutzer-ID:\n1. Aktivieren Sie den Entwicklermodus in den Discord-Einstellungen (Erweitert)\n2. Rechtsklick auf Ihren Namen → Benutzer-ID kopieren\n3. Geben Sie diese Nummer hier ein", + "emailIdHelp": "Geben Sie die E-Mail-Adresse ein, die zur Interaktion mit dem Assistenten autorisiert werden soll." } } diff --git a/src/messages/en.json b/src/messages/en.json index 313649a..a7d8fe6 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -237,5 +237,17 @@ "statusHealthy": "Healthy", "statusDown": "Down", "spendChf": "Spend (CHF)" + }, + "channelUsers": { + "title": "Authorized Users", + "description": "Manage which users can interact with your assistant on each channel. Add their numeric user ID to authorize access.", + "users": "users", + "placeholder": "Enter numeric user ID…", + "add": "Add", + "remove": "Remove", + "alreadyAdded": "This user ID is already authorized.", + "telegramIdHelp": "To find your Telegram user ID:\n1. Open Telegram and message @userinfobot\n2. It instantly replies with your numeric ID\n3. Enter that number here", + "discordIdHelp": "To find your Discord user ID:\n1. Enable Developer Mode in Discord settings (Advanced)\n2. Right-click your name → Copy User ID\n3. Enter that number here", + "emailIdHelp": "Enter the email address that should be authorized to interact with the assistant." } } diff --git a/src/messages/fr.json b/src/messages/fr.json index a1b607e..de58763 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -237,5 +237,17 @@ "statusHealthy": "OK", "statusDown": "Hors service", "spendChf": "Coûts (CHF)" + }, + "channelUsers": { + "title": "Utilisateurs autorisés", + "description": "Gérez les utilisateurs pouvant interagir avec votre assistant sur chaque canal. Ajoutez leur identifiant numérique pour autoriser l'accès.", + "users": "utilisateurs", + "placeholder": "Entrez l'identifiant numérique…", + "add": "Ajouter", + "remove": "Supprimer", + "alreadyAdded": "Cet identifiant est déjà autorisé.", + "telegramIdHelp": "Pour trouver votre identifiant Telegram :\n1. Ouvrez Telegram et envoyez un message à @userinfobot\n2. Il répond instantanément avec votre identifiant numérique\n3. Entrez ce numéro ici", + "discordIdHelp": "Pour trouver votre identifiant Discord :\n1. Activez le mode développeur dans les paramètres Discord (Avancé)\n2. Clic droit sur votre nom → Copier l'identifiant\n3. Entrez ce numéro ici", + "emailIdHelp": "Entrez l'adresse e-mail qui doit être autorisée à interagir avec l'assistant." } } diff --git a/src/messages/it.json b/src/messages/it.json index d7c7d13..9cb87a0 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -237,5 +237,17 @@ "statusHealthy": "OK", "statusDown": "Non disponibile", "spendChf": "Costi (CHF)" + }, + "channelUsers": { + "title": "Utenti autorizzati", + "description": "Gestisci quali utenti possono interagire con il tuo assistente su ogni canale. Aggiungi il loro ID numerico per autorizzare l'accesso.", + "users": "utenti", + "placeholder": "Inserisci l'ID numerico…", + "add": "Aggiungi", + "remove": "Rimuovi", + "alreadyAdded": "Questo ID utente è già autorizzato.", + "telegramIdHelp": "Per trovare il tuo ID Telegram:\n1. Apri Telegram e invia un messaggio a @userinfobot\n2. Risponde istantaneamente con il tuo ID numerico\n3. Inserisci quel numero qui", + "discordIdHelp": "Per trovare il tuo ID Discord:\n1. Attiva la Modalità sviluppatore nelle impostazioni Discord (Avanzate)\n2. Clic destro sul tuo nome → Copia ID utente\n3. Inserisci quel numero qui", + "emailIdHelp": "Inserisci l'indirizzo e-mail che deve essere autorizzato a interagire con l'assistente." } } diff --git a/src/types/index.ts b/src/types/index.ts index 0ff9cab..8d675de 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,6 +29,7 @@ export interface PiecedTenantSpec { plan?: string; packages?: string[]; workspaceFiles?: Record; + channelUsers?: Record; suspend?: boolean; }