refactor(discord): einheitliche Berechtigung über verknüpfte Konten

Nutzerfrage: warum gibt es neben der Kontoverknüpfung noch eine
separate "Erlaubte Nutzer-IDs"-Liste, und woher weiß der Bot, ob
jemand Kunde oder Personal ist? Antwort: das waren zwei parallele,
verwirrende Mechanismen. /kc-status und /kc-kunde prüften bisher eine
manuell gepflegte Discord-ID-Liste (aus der Zeit vor der OAuth-
Verknüpfung), während /zuweisen und /schliessen bereits das verknüpfte
Personal-Konto (users.discord_user_id, kind='staff') prüften.

Jetzt einheitlich: alle vier Personal-Befehle verlangen ein verknüpftes
Personal-Konto, genau wie /zuweisen und /schliessen. Die Liste entfällt
vollständig (Migration 029, Spalte staff_user_ids entfernt) – wer ein
Personal-Konto im Kundencenter hat und es unter "Mein Konto" mit
Discord verknüpft, darf automatisch alle vier Befehle nutzen, ohne
zusätzliche manuelle Pflege einer ID-Liste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kundencenter 2026-09-29 00:12:10 +02:00
parent 8243cdb111
commit 078b1357fb
4 changed files with 20 additions and 20 deletions

View file

@ -4,12 +4,12 @@ import { pool } from '@kc/platform/db';
import { decrypt } from '@kc/platform/crypto';
import { enqueue } from '@kc/platform/jobs';
interface DiscordSettings { enabled: boolean; token: string | null; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null; staffUserIds: string[] }
interface DiscordSettings { enabled: boolean; token: string | null; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null }
async function loadSettings(): Promise<DiscordSettings> {
const [rows] = await pool.query('SELECT * FROM discord_settings WHERE id = 1') as any;
const s = rows[0];
const token = s?.token_enc ? (JSON.parse(decrypt(s.token_enc)).token as string) : null;
return { enabled: !!s?.enabled, token, guildId: s?.guild_id ?? null, adminChannelId: s?.admin_channel_id ?? null, ticketChannelId: s?.ticket_channel_id ?? null, staffUserIds: String(s?.staff_user_ids ?? '').split(',').map((x: string) => x.trim()).filter(Boolean) };
return { enabled: !!s?.enabled, token, guildId: s?.guild_id ?? null, adminChannelId: s?.admin_channel_id ?? null, ticketChannelId: s?.ticket_channel_id ?? null };
}
const notify = (event: string, extra: Record<string, unknown>, key: string) => enqueue('discord.notify', { event, ...extra }, { idempotencyKey: key });
async function nextTicketNumber(): Promise<string> {
@ -85,9 +85,8 @@ async function handle(i: ChatInputCommandInteraction): Promise<void> {
if (i.commandName === 'ticket') return handleTicketCreate(i);
if (i.commandName === 'zuweisen') return handleAssign(i);
if (i.commandName === 'schliessen') return handleClose(i);
// Alle Antworten ephemeral: nie vertrauliche Daten in Kanälen sichtbar machen.
const s = await loadSettings();
if (!s.staffUserIds.includes(i.user.id)) { await i.reply({ content: 'Keine Berechtigung.', ephemeral: true }); return; }
// Alle Antworten ephemeral: nie vertrauliche Daten in Kanälen sichtbar machen. Berechtigung = verknüpftes Personal-Konto (wie /zuweisen, /schliessen).
if (!(await linkedUser(i.user.id, 'staff'))) { await i.reply({ content: 'Nur für verknüpfte Personal-Konten.', ephemeral: true }); return; }
if (i.commandName === 'kc-status') {
const [rows] = await pool.query('SELECT status, COUNT(*) n FROM jobs GROUP BY status') as any;
const jobs = rows.length ? rows.map((r: any) => `${r.status}: ${r.n}`).join(', ') : 'keine';