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:
parent
8243cdb111
commit
078b1357fb
4 changed files with 20 additions and 20 deletions
|
|
@ -11,7 +11,6 @@ import type { KcModule } from '../../core/module.js';
|
||||||
const ID = /^\d{15,25}$/; // Discord-Snowflake-IDs
|
const ID = /^\d{15,25}$/; // Discord-Snowflake-IDs
|
||||||
const settingsView = (s: any) => ({
|
const settingsView = (s: any) => ({
|
||||||
enabled: !!s.enabled, hasToken: !!s.token_enc, guildId: s.guild_id, adminChannelId: s.admin_channel_id,
|
enabled: !!s.enabled, hasToken: !!s.token_enc, guildId: s.guild_id, adminChannelId: s.admin_channel_id,
|
||||||
staffUserIds: String(s.staff_user_ids ?? '').split(',').map((x: string) => x.trim()).filter(Boolean),
|
|
||||||
clientId: s.client_id, hasClientSecret: !!s.client_secret_enc, ticketChannelId: s.ticket_channel_id,
|
clientId: s.client_id, hasClientSecret: !!s.client_secret_enc, ticketChannelId: s.ticket_channel_id,
|
||||||
configured: !!s.token_enc, lastConnectedAt: s.last_connected_at, lastError: s.last_error, updatedAt: s.updated_at,
|
configured: !!s.token_enc, lastConnectedAt: s.last_connected_at, lastError: s.last_error, updatedAt: s.updated_at,
|
||||||
});
|
});
|
||||||
|
|
@ -31,11 +30,11 @@ export const discordModule: KcModule = {
|
||||||
token: z.string().max(200).nullable().optional(), // undefined = unverändert lassen, null = löschen
|
token: z.string().max(200).nullable().optional(), // undefined = unverändert lassen, null = löschen
|
||||||
guildId: z.string().trim().regex(ID).nullable(), adminChannelId: z.string().trim().regex(ID).nullable(),
|
guildId: z.string().trim().regex(ID).nullable(), adminChannelId: z.string().trim().regex(ID).nullable(),
|
||||||
ticketChannelId: z.string().trim().regex(ID).nullable(),
|
ticketChannelId: z.string().trim().regex(ID).nullable(),
|
||||||
staffUserIds: z.array(z.string().trim().regex(ID)).max(50), enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
clientId: z.string().trim().regex(ID).nullable(), clientSecret: z.string().max(200).nullable().optional(),
|
clientId: z.string().trim().regex(ID).nullable(), clientSecret: z.string().max(200).nullable().optional(),
|
||||||
}).parse(req.body);
|
}).parse(req.body);
|
||||||
const sets = ['guild_id = ?', 'admin_channel_id = ?', 'ticket_channel_id = ?', 'staff_user_ids = ?', 'enabled = ?', 'client_id = ?', 'updated_by = ?'];
|
const sets = ['guild_id = ?', 'admin_channel_id = ?', 'ticket_channel_id = ?', 'enabled = ?', 'client_id = ?', 'updated_by = ?'];
|
||||||
const params: unknown[] = [b.guildId, b.adminChannelId, b.ticketChannelId, b.staffUserIds.join(','), b.enabled ? 1 : 0, b.clientId, a.user.id];
|
const params: unknown[] = [b.guildId, b.adminChannelId, b.ticketChannelId, b.enabled ? 1 : 0, b.clientId, a.user.id];
|
||||||
if (b.token !== undefined) { sets.push('token_enc = ?'); params.push(b.token ? encrypt(JSON.stringify({ token: b.token })) : null); }
|
if (b.token !== undefined) { sets.push('token_enc = ?'); params.push(b.token ? encrypt(JSON.stringify({ token: b.token })) : null); }
|
||||||
if (b.clientSecret !== undefined) { sets.push('client_secret_enc = ?'); params.push(b.clientSecret ? encrypt(JSON.stringify({ secret: b.clientSecret })) : null); }
|
if (b.clientSecret !== undefined) { sets.push('client_secret_enc = ?'); params.push(b.clientSecret ? encrypt(JSON.stringify({ secret: b.clientSecret })) : null); }
|
||||||
await run(`UPDATE discord_settings SET ${sets.join(', ')} WHERE id = 1`, params);
|
await run(`UPDATE discord_settings SET ${sets.join(', ')} WHERE id = 1`, params);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { api, errMsg } from '@/lib/api';
|
||||||
import { useSession } from '@/lib/session';
|
import { useSession } from '@/lib/session';
|
||||||
import { Alert, Field, fmt } from '@/components/ui';
|
import { Alert, Field, fmt } from '@/components/ui';
|
||||||
|
|
||||||
interface Settings { enabled: boolean; hasToken: boolean; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null; staffUserIds: string[]; clientId: string | null; hasClientSecret: boolean; configured: boolean; lastConnectedAt: string | null; lastError: string | null; updatedAt: string }
|
interface Settings { enabled: boolean; hasToken: boolean; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null; clientId: string | null; hasClientSecret: boolean; configured: boolean; lastConnectedAt: string | null; lastError: string | null; updatedAt: string }
|
||||||
|
|
||||||
/** Schritt-für-Schritt-Anleitung, damit auch ohne Discord-Vorwissen ein Bot eingerichtet werden kann. */
|
/** Schritt-für-Schritt-Anleitung, damit auch ohne Discord-Vorwissen ein Bot eingerichtet werden kann. */
|
||||||
function Guide({ redirectUri }: { redirectUri: string }) {
|
function Guide({ redirectUri }: { redirectUri: string }) {
|
||||||
|
|
@ -16,9 +16,9 @@ function Guide({ redirectUri }: { redirectUri: string }) {
|
||||||
<li><strong>Bot einladen:</strong> Im Reiter „OAuth2 → URL Generator“ die Scopes <code className="mono">bot</code> und <code className="mono">applications.commands</code> ankreuzen, bei den Bot-Berechtigungen „Send Messages“, „Create Private Threads“, „View Channels“ und „Read Message History“ auswählen. Die erzeugte URL öffnen und den Bot auf den gewünschten Server einladen.</li>
|
<li><strong>Bot einladen:</strong> Im Reiter „OAuth2 → URL Generator“ die Scopes <code className="mono">bot</code> und <code className="mono">applications.commands</code> ankreuzen, bei den Bot-Berechtigungen „Send Messages“, „Create Private Threads“, „View Channels“ und „Read Message History“ auswählen. Die erzeugte URL öffnen und den Bot auf den gewünschten Server einladen.</li>
|
||||||
<li><strong>Kunden-Anmeldung (OAuth) einrichten:</strong> Im Reiter „OAuth2 → General“ die <strong>Client ID</strong> und (unter „Reset Secret“) das <strong>Client Secret</strong> kopieren, unten eintragen. Unter „Redirects“ genau diese Adresse eintragen: <code className="mono">{redirectUri}</code></li>
|
<li><strong>Kunden-Anmeldung (OAuth) einrichten:</strong> Im Reiter „OAuth2 → General“ die <strong>Client ID</strong> und (unter „Reset Secret“) das <strong>Client Secret</strong> kopieren, unten eintragen. Unter „Redirects“ genau diese Adresse eintragen: <code className="mono">{redirectUri}</code></li>
|
||||||
<li><strong>IDs ermitteln:</strong> In Discord unter Einstellungen → Erweitert den „Entwicklermodus“ aktivieren. Danach mit Rechtsklick auf den Server, die gewünschten Kanäle und die eigene Person jeweils „ID kopieren“ wählen.</li>
|
<li><strong>IDs ermitteln:</strong> In Discord unter Einstellungen → Erweitert den „Entwicklermodus“ aktivieren. Danach mit Rechtsklick auf den Server, die gewünschten Kanäle und die eigene Person jeweils „ID kopieren“ wählen.</li>
|
||||||
<li><strong>Hier eintragen:</strong> Token, Server-ID, Kanal-ID für Systemmeldungen (Backup-Warnungen, neue Tickets) und Kanal-ID für Ticket-Threads unten speichern. Unter „Erlaubte Nutzer-IDs“ die Discord-IDs aller Personen eintragen, die <code className="mono">/kc-status</code> und <code className="mono">/kc-kunde</code> nutzen dürfen.</li>
|
<li><strong>Hier eintragen:</strong> Token, Server-ID, Kanal-ID für Systemmeldungen (Backup-Warnungen, neue Tickets) und Kanal-ID für Ticket-Threads unten speichern.</li>
|
||||||
<li><strong>Verfügbare Befehle:</strong> Kunden mit verknüpftem Konto können mit <code className="mono">/ticket</code> direkt ein neues Ticket eröffnen. Personal kann <em>innerhalb eines Ticket-Threads</em> mit <code className="mono">/zuweisen</code> und <code className="mono">/schliessen</code> das Ticket zuweisen bzw. schließen – dafür muss die jeweilige Person ihr eigenes Discord-Konto ebenfalls unter „Mein Konto“ verknüpft haben.</li>
|
<li><strong>Wer darf was:</strong> Es gibt keine separate Liste mehr – maßgeblich ist allein, ob und wie ein Discord-Konto unter „Mein Konto“ mit einem Kundencenter-Zugang verknüpft ist. Kunden mit verknüpftem Konto können mit <code className="mono">/ticket</code> ein neues Ticket eröffnen. Personal-Konten (Mitarbeiter, die sich ebenfalls unter „Mein Konto“ verknüpfen) dürfen zusätzlich <code className="mono">/kc-status</code>, <code className="mono">/kc-kunde</code> sowie <em>innerhalb eines Ticket-Threads</em> <code className="mono">/zuweisen</code> und <code className="mono">/schliessen</code> nutzen.</li>
|
||||||
<li><strong>Saubere Trennung:</strong> Kunden sehen und nutzen ausschließlich <code className="mono">/ticket</code> sowie ihren eigenen Ticket-Thread. Die Personal-Befehle sind zusätzlich so eingestellt, dass sie nur Discord-Mitgliedern mit der Server-Berechtigung „Threads verwalten“ überhaupt in der Befehlsliste angezeigt werden – vergeben Sie diese Berechtigung über eine eigene Rolle nur an Ihr Support-Team, nicht an die Kundenrolle.</li>
|
<li><strong>Saubere Trennung:</strong> Kunden sehen und nutzen ausschließlich <code className="mono">/ticket</code> sowie ihren eigenen Ticket-Thread. Die Personal-Befehle sind zusätzlich so eingestellt, dass sie nur Discord-Mitgliedern mit der Server-Berechtigung „Threads verwalten“ überhaupt in der Befehlsliste angezeigt werden (reine Sichtbarkeit – die eigentliche Prüfung ist immer das verknüpfte Personal-Konto). Vergeben Sie diese Berechtigung über eine eigene Rolle nur an Ihr Support-Team, nicht an die Kundenrolle.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</details>);
|
</details>);
|
||||||
}
|
}
|
||||||
|
|
@ -36,9 +36,8 @@ export default function DiscordSettings() {
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget;
|
||||||
const f = new FormData(form); const v = (k: string) => (String(f.get(k) ?? '').trim() || null);
|
const f = new FormData(form); const v = (k: string) => (String(f.get(k) ?? '').trim() || null);
|
||||||
const token = String(f.get('token') ?? ''); const clientSecret = String(f.get('clientSecret') ?? '');
|
const token = String(f.get('token') ?? ''); const clientSecret = String(f.get('clientSecret') ?? '');
|
||||||
const staffUserIds = String(f.get('staffUserIds') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
||||||
try {
|
try {
|
||||||
await api('PUT', '/admin/discord/settings', { guildId: v('guildId'), adminChannelId: v('adminChannelId'), ticketChannelId: v('ticketChannelId'), staffUserIds, enabled: f.get('enabled') === 'on', clientId: v('clientId'), ...(token ? { token } : {}), ...(clientSecret ? { clientSecret } : {}) });
|
await api('PUT', '/admin/discord/settings', { guildId: v('guildId'), adminChannelId: v('adminChannelId'), ticketChannelId: v('ticketChannelId'), enabled: f.get('enabled') === 'on', clientId: v('clientId'), ...(token ? { token } : {}), ...(clientSecret ? { clientSecret } : {}) });
|
||||||
setMsg({ k: 'ok', t: 'Gespeichert. Die Verbindung wird innerhalb einer Minute automatisch aufgebaut.' }); (form.elements.namedItem('token') as HTMLInputElement).value = ''; (form.elements.namedItem('clientSecret') as HTMLInputElement).value = ''; void load();
|
setMsg({ k: 'ok', t: 'Gespeichert. Die Verbindung wird innerhalb einer Minute automatisch aufgebaut.' }); (form.elements.namedItem('token') as HTMLInputElement).value = ''; (form.elements.namedItem('clientSecret') as HTMLInputElement).value = ''; void load();
|
||||||
} catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setBusy(false); }
|
} catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setBusy(false); }
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +61,7 @@ export default function DiscordSettings() {
|
||||||
<form onSubmit={save}>
|
<form onSubmit={save}>
|
||||||
<div className="card" style={{ marginTop: 16 }}>
|
<div className="card" style={{ marginTop: 16 }}>
|
||||||
<h2>Bot-Verbindung</h2>
|
<h2>Bot-Verbindung</h2>
|
||||||
<p className="muted small">Der Bot meldet Systemereignisse (neue Tickets, Rechnungen, Backup-Probleme) im gewählten Kanal und beantwortet die Befehle <code className="mono">/kc-status</code> und <code className="mono">/kc-kunde</code> für die unten eingetragenen Personen.</p>
|
<p className="muted small">Der Bot meldet Systemereignisse (neue Tickets, Rechnungen, Backup-Probleme) im gewählten Kanal. Wer <code className="mono">/kc-status</code>, <code className="mono">/kc-kunde</code>, <code className="mono">/zuweisen</code> und <code className="mono">/schliessen</code> nutzen darf, richtet sich allein nach der Discord-Verknüpfung des jeweiligen Personal-Kontos (siehe unten).</p>
|
||||||
{!s.configured && <Alert kind="warn">Noch kein Token gespeichert.</Alert>}
|
{!s.configured && <Alert kind="warn">Noch kein Token gespeichert.</Alert>}
|
||||||
{s.configured && !s.enabled && <Alert kind="info">Token gespeichert, aber deaktiviert – der Bot verbindet sich nicht.</Alert>}
|
{s.configured && !s.enabled && <Alert kind="info">Token gespeichert, aber deaktiviert – der Bot verbindet sich nicht.</Alert>}
|
||||||
{s.lastError && <Alert kind="err">Letzte Störung: {s.lastError}</Alert>}
|
{s.lastError && <Alert kind="err">Letzte Störung: {s.lastError}</Alert>}
|
||||||
|
|
@ -70,15 +69,14 @@ export default function DiscordSettings() {
|
||||||
<Field id="token" label="Bot-Token" hint={s.hasToken ? 'Ein Token ist gespeichert. Leer lassen, um ihn unverändert zu lassen.' : 'Noch kein Token gespeichert.'}><input id="token" name="token" type="password" autoComplete="new-password" disabled={!w} /></Field>
|
<Field id="token" label="Bot-Token" hint={s.hasToken ? 'Ein Token ist gespeichert. Leer lassen, um ihn unverändert zu lassen.' : 'Noch kein Token gespeichert.'}><input id="token" name="token" type="password" autoComplete="new-password" disabled={!w} /></Field>
|
||||||
<Field id="guildId" label="Server-ID (Guild-ID)"><input id="guildId" name="guildId" defaultValue={s.guildId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
<Field id="guildId" label="Server-ID (Guild-ID)"><input id="guildId" name="guildId" defaultValue={s.guildId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
||||||
<Field id="adminChannelId" label="Kanal-ID für Systemmeldungen"><input id="adminChannelId" name="adminChannelId" defaultValue={s.adminChannelId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
<Field id="adminChannelId" label="Kanal-ID für Systemmeldungen"><input id="adminChannelId" name="adminChannelId" defaultValue={s.adminChannelId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
||||||
<Field id="ticketChannelId" label="Kanal-ID für Ticket-Threads" hint="Wird für die kommende Ticket-Thread-Funktion benötigt."><input id="ticketChannelId" name="ticketChannelId" defaultValue={s.ticketChannelId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
<Field id="ticketChannelId" label="Kanal-ID für Ticket-Threads"><input id="ticketChannelId" name="ticketChannelId" defaultValue={s.ticketChannelId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
||||||
<Field id="staffUserIds" label="Erlaubte Nutzer-IDs (kommagetrennt)" hint="Dürfen /kc-status und /kc-kunde nutzen."><input id="staffUserIds" name="staffUserIds" defaultValue={s.staffUserIds.join(', ')} disabled={!w} /></Field>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="row small" style={{ marginBottom: 12 }}><input type="checkbox" name="enabled" defaultChecked={s.enabled} disabled={!w} style={{ width: 20, minHeight: 20 }} /> Bot aktiv</label>
|
<label className="row small" style={{ marginBottom: 12 }}><input type="checkbox" name="enabled" defaultChecked={s.enabled} disabled={!w} style={{ width: 20, minHeight: 20 }} /> Bot aktiv</label>
|
||||||
<p className="small muted">Zuletzt verbunden: {s.lastConnectedAt ? fmt(s.lastConnectedAt) : 'noch nie'}</p>
|
<p className="small muted">Zuletzt verbunden: {s.lastConnectedAt ? fmt(s.lastConnectedAt) : 'noch nie'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="card" style={{ marginTop: 16 }}>
|
<div className="card" style={{ marginTop: 16 }}>
|
||||||
<h2>Kunden-Anmeldung (OAuth)</h2>
|
<h2>Konto-Verknüpfung (OAuth)</h2>
|
||||||
<p className="muted small">Ermöglicht Kunden, ihr Discord-Konto unter „Mein Konto“ mit ihrem Kundencenter-Zugang zu verknüpfen. Client ID und Secret stehen in derselben Discord-Anwendung wie der Bot, im Reiter „OAuth2 → General“.</p>
|
<p className="muted small">Ermöglicht sowohl Kunden als auch Personal, das eigene Discord-Konto unter „Mein Konto“ mit dem Kundencenter-Zugang zu verknüpfen – davon hängt ab, welche Discord-Befehle jemand nutzen darf. Client ID und Secret stehen in derselben Discord-Anwendung wie der Bot, im Reiter „OAuth2 → General“.</p>
|
||||||
<div className="cols">
|
<div className="cols">
|
||||||
<Field id="clientId" label="Client ID"><input id="clientId" name="clientId" defaultValue={s.clientId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
<Field id="clientId" label="Client ID"><input id="clientId" name="clientId" defaultValue={s.clientId ?? ''} placeholder="123456789012345678" disabled={!w} /></Field>
|
||||||
<Field id="clientSecret" label="Client Secret" hint={s.hasClientSecret ? 'Gespeichert. Leer lassen, um es unverändert zu lassen.' : 'Noch nicht gespeichert.'}><input id="clientSecret" name="clientSecret" type="password" autoComplete="new-password" disabled={!w} /></Field>
|
<Field id="clientSecret" label="Client Secret" hint={s.hasClientSecret ? 'Gespeichert. Leer lassen, um es unverändert zu lassen.' : 'Noch nicht gespeichert.'}><input id="clientSecret" name="clientSecret" type="password" autoComplete="new-password" disabled={!w} /></Field>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import { pool } from '@kc/platform/db';
|
||||||
import { decrypt } from '@kc/platform/crypto';
|
import { decrypt } from '@kc/platform/crypto';
|
||||||
import { enqueue } from '@kc/platform/jobs';
|
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> {
|
async function loadSettings(): Promise<DiscordSettings> {
|
||||||
const [rows] = await pool.query('SELECT * FROM discord_settings WHERE id = 1') as any;
|
const [rows] = await pool.query('SELECT * FROM discord_settings WHERE id = 1') as any;
|
||||||
const s = rows[0];
|
const s = rows[0];
|
||||||
const token = s?.token_enc ? (JSON.parse(decrypt(s.token_enc)).token as string) : null;
|
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 });
|
const notify = (event: string, extra: Record<string, unknown>, key: string) => enqueue('discord.notify', { event, ...extra }, { idempotencyKey: key });
|
||||||
async function nextTicketNumber(): Promise<string> {
|
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 === 'ticket') return handleTicketCreate(i);
|
||||||
if (i.commandName === 'zuweisen') return handleAssign(i);
|
if (i.commandName === 'zuweisen') return handleAssign(i);
|
||||||
if (i.commandName === 'schliessen') return handleClose(i);
|
if (i.commandName === 'schliessen') return handleClose(i);
|
||||||
// Alle Antworten ephemeral: nie vertrauliche Daten in Kanälen sichtbar machen.
|
// Alle Antworten ephemeral: nie vertrauliche Daten in Kanälen sichtbar machen. Berechtigung = verknüpftes Personal-Konto (wie /zuweisen, /schliessen).
|
||||||
const s = await loadSettings();
|
if (!(await linkedUser(i.user.id, 'staff'))) { await i.reply({ content: 'Nur für verknüpfte Personal-Konten.', ephemeral: true }); return; }
|
||||||
if (!s.staffUserIds.includes(i.user.id)) { await i.reply({ content: 'Keine Berechtigung.', ephemeral: true }); return; }
|
|
||||||
if (i.commandName === 'kc-status') {
|
if (i.commandName === 'kc-status') {
|
||||||
const [rows] = await pool.query('SELECT status, COUNT(*) n FROM jobs GROUP BY status') as any;
|
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';
|
const jobs = rows.length ? rows.map((r: any) => `${r.status}: ${r.n}`).join(', ') : 'keine';
|
||||||
|
|
|
||||||
4
migrations/029_discord_drop_staff_ids.sql
Normal file
4
migrations/029_discord_drop_staff_ids.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- Die manuelle Freigabeliste (Discord-IDs) für /kc-status und /kc-kunde entfällt: alle Personal-Befehle prüfen
|
||||||
|
-- jetzt einheitlich ein per Discord-OAuth verknüpftes Personal-Konto (users.discord_user_id, kind='staff'),
|
||||||
|
-- wie schon /zuweisen und /schliessen. Vermeidet zwei parallele, leicht zu verwechselnde Berechtigungswege.
|
||||||
|
ALTER TABLE discord_settings DROP COLUMN staff_user_ids;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue