From 1905e81442b0d2dcedbea03a302d2a9f39478220 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Tue, 7 Jul 2026 14:10:56 +0800 Subject: [PATCH] feat: add tabbed admin pages and web auth --- config.py | 7 + front/src/App.vue | 171 ++++++++------ front/src/style.css | 17 ++ web/api.py | 132 ++++++++++- web/static/assets/index-BkB3pxMb.js | 328 +++++++++++++++++++++++++++ web/static/assets/index-DEuQ3oiJ.css | 1 + web/static/index.html | 4 +- 7 files changed, 584 insertions(+), 76 deletions(-) create mode 100644 web/static/assets/index-BkB3pxMb.js create mode 100644 web/static/assets/index-DEuQ3oiJ.css diff --git a/config.py b/config.py index 0119451..975079c 100644 --- a/config.py +++ b/config.py @@ -47,3 +47,10 @@ NEW_MEMBER_RESTRICT_MINUTES = int(os.getenv("NEW_MEMBER_RESTRICT_MINUTES", "10") # 防刷屏 RATE_LIMIT_MESSAGES = int(os.getenv("RATE_LIMIT_MESSAGES", "5")) RATE_LIMIT_WINDOW = int(os.getenv("RATE_LIMIT_WINDOW", "30")) + +# Web 管理认证 +WEB_AUTH_ENABLED = os.getenv("WEB_AUTH_ENABLED", "true").lower() not in {"0", "false", "no"} +WEB_AUTH_USERNAME = os.getenv("WEB_AUTH_USERNAME", "admin") +WEB_AUTH_PASSWORD = os.getenv("WEB_AUTH_PASSWORD", "admin") +WEB_SESSION_SECRET = os.getenv("WEB_SESSION_SECRET", "tg-spam-guard-change-me") +WEB_TRUST_SAME_SUBNET = os.getenv("WEB_TRUST_SAME_SUBNET", "true").lower() not in {"0", "false", "no"} diff --git a/front/src/App.vue b/front/src/App.vue index de40fc6..f7c6d47 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -21,10 +21,14 @@ const lotteries = ref([]); const lotteryParticipants = ref[]>([]); const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true }); const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' }); -const activeModal = ref<'shop' | 'lottery' | 'bans' | null>(null); +const activePage = ref<'dashboard' | 'chat' | 'logs' | 'shop' | 'lottery' | 'bans'>('dashboard'); const activeTab = reactive({ shop: 'items', lottery: 'events' }); const toast = ref(''); const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) }); +const authReady = ref(false); +const authenticated = ref(false); +const loginForm = reactive({ username: '', password: '' }); +const loggingIn = ref(false); const newMessage = ref(''); const sending = ref(false); const restarting = ref(false); @@ -37,6 +41,20 @@ let statTimer: number | undefined; const chatCount = computed(() => chatLogs.value.length); const selectedQuery = computed(() => selectedChatId.value ? `?chat_id=${selectedChatId.value}` : ''); +const pageTitle = computed(() => ({ + dashboard: '群组安全总览', chat: '聊天记录', logs: '运行日志', shop: '商城管理', lottery: '抽奖管理', bans: '封禁记录' +}[activePage.value])); +const pageDesc = computed(() => ({ + dashboard: '实时审计聊天、封禁、日志和运营活动', chat: '群消息实时审计与手动处置', logs: '服务运行状态与错误排查', shop: '商品 CRUD 与兑换交易记录', lottery: '抽奖项目、开奖条件与参与记录', bans: '封禁审计与手动解封' +}[activePage.value])); +function goPage(page: typeof activePage.value) { + activePage.value = page; + if (page === 'shop') { loadShopItems(); loadShopTransactions(); } + if (page === 'lottery') loadLotteries(); + if (page === 'bans') loadBans(); +} + + function formatTime(value?: string) { if (!value) return '-'; const text = String(value); @@ -88,6 +106,32 @@ function closeConfirm(value: boolean) { confirmState.resolver = null; } +async function checkAuth() { + try { + const data = await fetchJson('/api/auth/status'); + authenticated.value = Boolean(data.authenticated); + } catch { + authenticated.value = false; + } finally { + authReady.value = true; + } +} +async function login() { + if (!loginForm.username || !loginForm.password) return showToast('请输入账号和密码'); + loggingIn.value = true; + try { + await fetchJson('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(loginForm) }); + authenticated.value = true; + await bootstrapData(); + } catch (e: any) { + showToast(e.message || '登录失败'); + } finally { loggingIn.value = false; } +} +async function logout() { + await fetchJson('/api/auth/logout', { method: 'POST' }).catch(() => {}); + authenticated.value = false; +} + async function fetchJson(url: string, options?: RequestInit) { const res = await fetch(url, options); const data = await res.json().catch(() => ({})); @@ -299,8 +343,8 @@ function copyLogs() { navigator.clipboard?.writeText(text); } -onMounted(() => { - loadGroups().then(reloadByGroup); +async function bootstrapData() { + await loadGroups().then(reloadByGroup); loadChatHistory(); loadBans(); loadStats(); @@ -308,7 +352,11 @@ onMounted(() => { loadLotteries(); loadLogs(); connectSSE(); - statTimer = window.setInterval(loadStats, 30000); + if (!statTimer) statTimer = window.setInterval(loadStats, 30000); +} +onMounted(async () => { + await checkAuth(); + if (authenticated.value) await bootstrapData(); }); onBeforeUnmount(() => { if (es) es.close(); @@ -318,7 +366,9 @@ onBeforeUnmount(() => {