接入tgbot

This commit is contained in:
mtvpls
2026-06-29 16:40:03 +08:00
parent b1aad6a9bc
commit 5d605710f3
27 changed files with 2079 additions and 11 deletions
+252
View File
@@ -44,6 +44,7 @@ import {
Palette,
Plus,
Search,
Send,
Settings,
Smartphone,
Tablet,
@@ -13939,6 +13940,241 @@ const XiaoyaConfigComponent = ({
);
};
// Telegram Bot 配置组件
const TelegramConfigComponent = ({
config,
refreshConfig,
}: {
config: AdminConfig | null;
refreshConfig: () => Promise<void>;
}) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [enabled, setEnabled] = useState(false);
const [botToken, setBotToken] = useState('');
const [botUsername, setBotUsername] = useState('');
const [webhookSecret, setWebhookSecret] = useState('');
const [apiProxy, setApiProxy] = useState('');
const [apiBaseUrl, setApiBaseUrl] = useState('');
const [loginEnabled, setLoginEnabled] = useState(true);
const [bindingEnabled, setBindingEnabled] = useState(true);
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [defaultNotifications, setDefaultNotifications] = useState(true);
const [testChatId, setTestChatId] = useState('');
useEffect(() => {
const telegram = config?.TelegramConfig;
if (telegram) {
setEnabled(Boolean(telegram.enabled));
setBotToken(telegram.botToken || '');
setBotUsername(telegram.botUsername || '');
setWebhookSecret(telegram.webhookSecret || '');
setApiProxy(telegram.apiProxy || '');
setApiBaseUrl(telegram.apiBaseUrl || '');
setLoginEnabled(telegram.loginEnabled !== false);
setBindingEnabled(telegram.bindingEnabled !== false);
setNotificationsEnabled(telegram.notificationsEnabled !== false);
setDefaultNotifications(telegram.defaultNotifications !== false);
}
}, [config]);
const buildConfig = (): AdminConfig['TelegramConfig'] => ({
enabled,
botToken,
botUsername: botUsername.replace(/^@/, ''),
webhookSecret,
apiProxy,
apiBaseUrl,
loginEnabled,
bindingEnabled,
notificationsEnabled,
defaultNotifications,
});
const handleSave = async () => {
await withLoading('saveTelegram', async () => {
try {
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save', config: buildConfig() }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || '保存失败');
showSuccess('Telegram 配置保存成功', showAlert);
await refreshConfig();
} catch (error) {
showError(error instanceof Error ? error.message : '保存失败', showAlert);
throw error;
}
});
};
const handleSetWebhook = async () => {
await withLoading('setTelegramWebhook', async () => {
try {
if (!enabled || !botToken.trim() || !botUsername.trim() || !webhookSecret.trim()) {
throw new Error('请先填写 Bot Token、Bot 用户名 和 Webhook Secret');
}
const webhookUrlValue = webhookSecret === '******'
? ''
: `${window.location.origin}/api/telegram/webhook/${webhookSecret}`;
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'set_webhook',
config: buildConfig(),
webhookUrl: webhookUrlValue,
origin: window.location.origin,
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const telegramDetail = data.telegram
? `HTTP ${data.telegram.status || '-'},响应:${data.telegram.body || data.telegram.statusText || '-'}`
: '';
throw new Error(`${data.error || 'Webhook 设置失败'}${telegramDetail}`);
}
showSuccess('Webhook 设置成功', showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : 'Webhook 设置失败', showAlert);
throw error;
}
});
};
const handleTest = async () => {
if (!testChatId.trim()) {
showError('请输入测试 Chat ID', showAlert);
return;
}
await withLoading('testTelegram', async () => {
try {
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'test', config: buildConfig(), testChatId: testChatId.trim() }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || '发送失败');
showSuccess('测试消息发送成功', showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : '发送失败', showAlert);
throw error;
}
});
};
const webhookUrl = webhookSecret
? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/telegram/webhook/${webhookSecret === '******' ? '<secret>' : webhookSecret}`
: '';
return (
<div className='space-y-6'>
<div className='bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800 rounded-lg p-4'>
<h3 className='text-sm font-medium text-sky-900 dark:text-sky-100 mb-2'>
Telegram Bot
</h3>
<div className='text-sm text-sky-800 dark:text-sky-200 space-y-1'>
<p> Telegram</p>
<p> Webhook Telegram Bot API </p>
<p> Bot Token Webhook Secret </p>
</div>
</div>
<div className='space-y-4'>
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
<div>
<h3 className='text-sm font-medium text-gray-900 dark:text-white'> Telegram Bot</h3>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'> Telegram </p>
</div>
<button
onClick={() => setEnabled(!enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Bot Token *</label>
<input type='password' value={botToken} onChange={(e) => setBotToken(e.target.value)} placeholder='123456:ABC...' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Bot *</label>
<input type='text' value={botUsername} onChange={(e) => setBotUsername(e.target.value)} placeholder='your_bot' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
</div>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Webhook Secret</label>
<input type='password' value={webhookSecret} onChange={(e) => setWebhookSecret(e.target.value)} placeholder='建议填写随机长字符串' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
{webhookUrl && <p className='mt-2 break-all text-xs text-gray-500 dark:text-gray-400'>Webhook URL{webhookUrl}</p>}
<div className='mt-3 flex flex-col gap-2 sm:flex-row'>
<button onClick={handleSetWebhook} disabled={isLoading('setTelegramWebhook')} className={`w-full sm:w-auto ${buttonStyles.primary}`}>{isLoading('setTelegramWebhook') ? '设置中...' : '一键设置 Webhook'}</button>
</div>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'></label>
<input type='text' value={apiProxy} onChange={(e) => setApiProxy(e.target.value)} placeholder='http://127.0.0.1:7890' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>Node Cloudflare/Edge </p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'> Base URL</label>
<input type='text' value={apiBaseUrl} onChange={(e) => setApiBaseUrl(e.target.value)} placeholder='https://telegram-api.example.com' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> https://api.telegram.org。</p>
</div>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3 rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
{[
['允许绑定', bindingEnabled, setBindingEnabled],
['允许 Telegram 登录', loginEnabled, setLoginEnabled],
['启用 Telegram 通知', notificationsEnabled, setNotificationsEnabled],
['新绑定默认开启通知', defaultNotifications, setDefaultNotifications],
].map(([label, value, setter]) => (
<label key={label as string} className='flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300'>
<input type='checkbox' checked={value as boolean} onChange={(e) => (setter as (value: boolean) => void)(e.target.checked)} />
{label as string}
</label>
))}
</div>
<div className='rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'> Chat ID</label>
<div className='flex flex-col gap-2 sm:flex-row'>
<input type='text' value={testChatId} onChange={(e) => setTestChatId(e.target.value)} placeholder='用户或群组 chat_id' className='min-w-0 flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<button onClick={handleTest} disabled={isLoading('testTelegram')} className={`w-full shrink-0 sm:w-auto ${buttonStyles.primary}`}>{isLoading('testTelegram') ? '发送中...' : '测试'}</button>
</div>
</div>
<div className='flex justify-end'>
<button onClick={handleSave} disabled={isLoading('saveTelegram')} className={buttonStyles.success}>
{isLoading('saveTelegram') ? '保存中...' : '保存配置'}
</button>
</div>
</div>
<AlertModal
isOpen={alertModal.isOpen}
onClose={hideAlert}
type={alertModal.type}
title={alertModal.title}
message={alertModal.message}
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
</div>
);
};
// 邮件配置组件
const EmailConfigComponent = ({
config,
@@ -16757,6 +16993,7 @@ function AdminPageClient() {
customAdFilter: false,
themeConfig: false,
emailConfig: false,
telegramConfig: false,
});
// 获取管理员配置
@@ -17392,6 +17629,21 @@ function AdminPageClient() {
/>
</CollapsibleTab>
{/* Telegram Bot 配置标签 */}
<CollapsibleTab
title='Telegram Bot'
icon={
<Send size={20} className='text-gray-600 dark:text-gray-400' />
}
isExpanded={expandedTabs.telegramConfig}
onToggle={() => toggleTab('telegramConfig')}
>
<TelegramConfigComponent
config={config}
refreshConfig={fetchConfig}
/>
</CollapsibleTab>
{/* 分类配置标签 */}
<CollapsibleTab
title='分类配置'
+156
View File
@@ -0,0 +1,156 @@
import { NextRequest, NextResponse } from 'next/server';
import type { AdminConfig } from '@/lib/admin.types';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { getStorage } from '@/lib/db';
import { getTelegramConfig, sendTelegramMessage, setTelegramWebhook, TelegramApiError } from '@/lib/telegram';
export const runtime = 'nodejs';
async function assertAdmin(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) return { error: 'Unauthorized', status: 401 } as const;
const storage = getStorage();
const userInfo = await storage.getUserInfoV2?.(authInfo.username);
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner')) {
return { error: 'Forbidden', status: 403 } as const;
}
return { storage } as const;
}
function maskTelegramConfig(config: AdminConfig['TelegramConfig']) {
return {
enabled: config?.enabled || false,
botToken: config?.botToken ? '******' : '',
botUsername: config?.botUsername || '',
webhookSecret: config?.webhookSecret ? '******' : '',
apiProxy: config?.apiProxy || '',
apiBaseUrl: config?.apiBaseUrl || '',
loginEnabled: config?.loginEnabled !== false,
bindingEnabled: config?.bindingEnabled !== false,
notificationsEnabled: config?.notificationsEnabled !== false,
defaultNotifications: config?.defaultNotifications !== false,
};
}
export async function GET(request: NextRequest) {
const auth = await assertAdmin(request);
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const adminConfig = await getConfig();
return NextResponse.json(maskTelegramConfig(adminConfig.TelegramConfig));
}
export async function POST(request: NextRequest) {
const auth = await assertAdmin(request);
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const body = await request.json();
const { action, config, testChatId, webhookUrl, origin } = body;
if (action === 'set_webhook') {
const overrideConfig = config as AdminConfig['TelegramConfig'] | undefined;
const savedConfig = await getTelegramConfig();
const botToken =
overrideConfig?.botToken && overrideConfig.botToken !== '******'
? overrideConfig.botToken
: savedConfig.botToken;
const secret =
overrideConfig?.webhookSecret && overrideConfig.webhookSecret !== '******'
? overrideConfig.webhookSecret
: savedConfig.webhookSecret;
const baseOrigin = String(origin || '').trim().replace(/\/$/, '');
const resolvedWebhookUrl = String(webhookUrl || '').trim() ||
(baseOrigin && secret ? `${baseOrigin}/api/telegram/webhook/${secret}` : '');
if (!botToken || !secret || !resolvedWebhookUrl) {
return NextResponse.json({ error: '缺少 Bot Token、Webhook Secret 或站点地址' }, { status: 400 });
}
const apiProxy = overrideConfig?.apiProxy || savedConfig.apiProxy;
const apiBaseUrl = overrideConfig?.apiBaseUrl || savedConfig.apiBaseUrl;
try {
const result = await setTelegramWebhook(botToken, resolvedWebhookUrl, secret, {
apiProxy,
apiBaseUrl,
});
return NextResponse.json({ success: true, message: 'Webhook 设置成功', result });
} catch (error) {
if (error instanceof TelegramApiError) {
return NextResponse.json(
{
error: error.message,
telegram: {
status: error.status,
statusText: error.statusText,
body: error.body,
data: error.data,
apiBaseUrl: apiBaseUrl || 'https://api.telegram.org',
apiProxyEnabled: Boolean(apiProxy),
},
},
{ status: 502 }
);
}
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Webhook 设置失败' },
{ status: 502 }
);
}
}
if (action === 'test') {
if (!testChatId) {
return NextResponse.json({ error: '请填写测试 Chat ID' }, { status: 400 });
}
const overrideConfig = config as AdminConfig['TelegramConfig'] | undefined;
const savedConfig = await getTelegramConfig();
const botToken =
overrideConfig?.botToken && overrideConfig.botToken !== '******'
? overrideConfig.botToken
: savedConfig.botToken;
await sendTelegramMessage(
String(testChatId),
'MoonTVPlus Telegram Bot 测试消息发送成功。',
undefined,
{
enabled: true,
botToken,
botUsername: overrideConfig?.botUsername || savedConfig.botUsername,
apiProxy: overrideConfig?.apiProxy || savedConfig.apiProxy,
apiBaseUrl: overrideConfig?.apiBaseUrl || savedConfig.apiBaseUrl,
}
);
return NextResponse.json({ success: true, message: '测试消息发送成功' });
}
if (action !== 'save') {
return NextResponse.json({ error: '无效的操作' }, { status: 400 });
}
const telegramConfig = config as AdminConfig['TelegramConfig'];
if (!telegramConfig) {
return NextResponse.json({ error: 'Telegram 配置不能为空' }, { status: 400 });
}
if (telegramConfig.enabled && (!telegramConfig.botToken || !telegramConfig.botUsername)) {
return NextResponse.json({ error: '启用 Telegram 时必须填写 Bot Token 和 Bot 用户名' }, { status: 400 });
}
const adminConfig = await getConfig();
const oldConfig = adminConfig.TelegramConfig;
if (telegramConfig.botToken === '******') telegramConfig.botToken = oldConfig?.botToken || '';
if (telegramConfig.webhookSecret === '******') telegramConfig.webhookSecret = oldConfig?.webhookSecret || '';
adminConfig.TelegramConfig = telegramConfig;
await auth.storage.setAdminConfig(adminConfig);
return NextResponse.json({ success: true, message: 'Telegram 配置保存成功' });
}
+22
View File
@@ -4,6 +4,11 @@ import { NextRequest, NextResponse } from 'next/server';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
import { lockManager } from '@/lib/lock';
import {
createTelegramBindSession,
getTelegramConfig,
getTelegramDeepLink,
} from '@/lib/telegram';
export const runtime = 'nodejs';
@@ -171,6 +176,23 @@ export async function POST(req: NextRequest) {
await db.createUserV2(username, password, 'user', defaultTags);
const telegramConfig = await getTelegramConfig();
if (telegramConfig.enabled && telegramConfig.bindingEnabled && telegramConfig.botToken) {
const bindSession = await createTelegramBindSession(username);
return NextResponse.json({
ok: true,
message: '注册成功',
telegramBind: {
code: bindSession.code,
expiresAt: bindSession.expiresAt,
botUsername: telegramConfig.botUsername,
deepLink: telegramConfig.botUsername
? getTelegramDeepLink(telegramConfig.botUsername, `bind_${bindSession.code}`)
: '',
},
});
}
// 注册成功
return NextResponse.json({ ok: true, message: '注册成功' });
} catch (err: any) {
+9
View File
@@ -40,6 +40,8 @@ export async function GET(request: NextRequest) {
WatchRoom: watchRoomConfig,
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
DanmakuAutoLoadDefault: true,
EnableTelegramLogin: Boolean(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_BOT_USERNAME && process.env.TELEGRAM_LOGIN_ENABLED !== 'false'),
TelegramBotUsername: process.env.TELEGRAM_BOT_USERNAME || '',
});
}
@@ -60,6 +62,13 @@ export async function GET(request: NextRequest) {
EnableOIDCLogin: config.SiteConfig.EnableOIDCLogin || false,
EnableOIDCRegistration: config.SiteConfig.EnableOIDCRegistration || false,
OIDCButtonText: config.SiteConfig.OIDCButtonText || '',
EnableTelegramLogin: Boolean(
config.TelegramConfig?.enabled &&
config.TelegramConfig?.loginEnabled &&
(config.TelegramConfig?.botToken || process.env.TELEGRAM_BOT_TOKEN) &&
(config.TelegramConfig?.botUsername || process.env.TELEGRAM_BOT_USERNAME)
),
TelegramBotUsername: config.TelegramConfig?.botUsername || process.env.TELEGRAM_BOT_USERNAME || '',
DanmakuAutoLoadDefault: config.SiteConfig.DanmakuAutoLoadDefault !== false,
loginBackgroundImage: config.ThemeConfig?.loginBackgroundImage || '',
registerBackgroundImage: config.ThemeConfig?.registerBackgroundImage || '',
+46
View File
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import {
createTelegramBindSession,
getTelegramBinding,
getTelegramConfig,
getTelegramDeepLink,
} from '@/lib/telegram';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const config = await getTelegramConfig();
const binding = await getTelegramBinding(authInfo.username);
return NextResponse.json({
enabled: config.enabled && config.bindingEnabled && Boolean(config.botToken),
botUsername: config.botUsername,
binding,
});
}
export async function POST(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const config = await getTelegramConfig();
if (!config.enabled || !config.bindingEnabled || !config.botToken) {
return NextResponse.json({ error: 'Telegram Bot 未启用' }, { status: 400 });
}
const session = await createTelegramBindSession(authInfo.username);
return NextResponse.json({
code: session.code,
expiresAt: session.expiresAt,
botUsername: config.botUsername,
deepLink: config.botUsername ? getTelegramDeepLink(config.botUsername, `bind_${session.code}`) : '',
});
}
+16
View File
@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
import { getTelegramConfig } from '@/lib/telegram';
export const runtime = 'nodejs';
export async function GET() {
const config = await getTelegramConfig();
return NextResponse.json({
enabled: config.enabled && Boolean(config.botToken),
botUsername: config.botUsername,
loginEnabled: config.loginEnabled,
bindingEnabled: config.bindingEnabled,
notificationsEnabled: config.notificationsEnabled,
});
}
@@ -0,0 +1,24 @@
import { NextResponse } from 'next/server';
import {
createTelegramLoginSession,
getTelegramConfig,
getTelegramDeepLink,
} from '@/lib/telegram';
export const runtime = 'nodejs';
export async function POST() {
const config = await getTelegramConfig();
if (!config.enabled || !config.loginEnabled || !config.botToken || !config.botUsername) {
return NextResponse.json({ error: 'Telegram 登录未启用' }, { status: 400 });
}
const session = await createTelegramLoginSession();
return NextResponse.json({
token: session.token,
expiresAt: session.expiresAt,
botUsername: config.botUsername,
deepLink: getTelegramDeepLink(config.botUsername, `login_${session.token}`),
});
}
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server';
import {
consumeConfirmedTelegramLogin,
getTelegramLoginSession,
} from '@/lib/telegram';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const token = new URL(request.url).searchParams.get('token');
const session = await getTelegramLoginSession(token);
if (!session) return NextResponse.json({ status: 'expired' });
if (session.status === 'confirmed' && session.authToken) {
const consumed = await consumeConfirmedTelegramLogin(session.token);
const response = NextResponse.json({ status: 'confirmed', username: consumed?.username });
const expires = new Date();
expires.setDate(expires.getDate() + 60);
response.cookies.set('auth', session.authToken, {
path: '/',
expires,
sameSite: 'lax',
httpOnly: false,
secure: false,
});
return response;
}
return NextResponse.json({
status: session.status,
expiresAt: session.expiresAt,
});
}
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import {
handleTelegramWebhookUpdate,
validateTelegramWebhookRequest,
} from '@/lib/telegram';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function POST(
request: NextRequest,
{ params }: { params: { secret: string } }
) {
if (!(await validateTelegramWebhookRequest(request, params.secret))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const update = await request.json();
await handleTelegramWebhookUpdate(update);
return NextResponse.json({ ok: true });
}
+11
View File
@@ -99,6 +99,8 @@ export default async function RootLayout({
let enableOIDCLogin = false;
let enableOIDCRegistration = false;
let oidcButtonText = '';
let telegramLoginEnabled = false;
let telegramBotUsername = '';
let aiEnabled = false;
let aiEnableHomepageEntry = false;
let aiEnableVideoCardEntry = false;
@@ -173,6 +175,13 @@ export default async function RootLayout({
enableOIDCLogin = config.SiteConfig.EnableOIDCLogin || false;
enableOIDCRegistration = config.SiteConfig.EnableOIDCRegistration || false;
oidcButtonText = config.SiteConfig.OIDCButtonText || '';
telegramLoginEnabled = Boolean(
config.TelegramConfig?.enabled &&
config.TelegramConfig?.loginEnabled &&
(config.TelegramConfig?.botToken || process.env.TELEGRAM_BOT_TOKEN) &&
(config.TelegramConfig?.botUsername || process.env.TELEGRAM_BOT_USERNAME)
);
telegramBotUsername = config.TelegramConfig?.botUsername || process.env.TELEGRAM_BOT_USERNAME || '';
// AI配置
aiEnabled = config.AIConfig?.Enabled || false;
aiEnableHomepageEntry = config.AIConfig?.EnableHomepageEntry || false;
@@ -275,6 +284,8 @@ export default async function RootLayout({
ENABLE_OIDC_LOGIN: enableOIDCLogin,
ENABLE_OIDC_REGISTRATION: enableOIDCRegistration,
OIDC_BUTTON_TEXT: oidcButtonText,
ENABLE_TELEGRAM_LOGIN: telegramLoginEnabled,
TELEGRAM_BOT_USERNAME: telegramBotUsername,
AI_ENABLED: aiEnabled && userFeatureAccess.ai_ask,
AI_ENABLE_HOMEPAGE_ENTRY: aiEnableHomepageEntry,
AI_ENABLE_VIDEOCARD_ENTRY: aiEnableVideoCardEntry,
+87 -1
View File
@@ -2,7 +2,7 @@
'use client';
import { AlertCircle, CheckCircle, Eye, EyeOff, User, Lock } from 'lucide-react';
import { AlertCircle, CheckCircle, Eye, EyeOff, Send, User, Lock } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react';
@@ -108,6 +108,9 @@ function LoginPageClient() {
const [siteConfig, setSiteConfig] = useState<any>(null);
const [turnstileWidgetId, setTurnstileWidgetId] = useState<string | null>(null);
const [backgroundImage, setBackgroundImage] = useState<string>('');
const [telegramLoginEnabled, setTelegramLoginEnabled] = useState(false);
const [telegramLoginLoading, setTelegramLoginLoading] = useState(false);
const [telegramLoginHint, setTelegramLoginHint] = useState<string | null>(null);
const { siteName } = useSite();
@@ -150,6 +153,7 @@ function LoginPageClient() {
EnableOIDCLogin: runtimeConfig?.ENABLE_OIDC_LOGIN || false,
OIDCButtonText: runtimeConfig?.OIDC_BUTTON_TEXT || '',
});
setTelegramLoginEnabled(Boolean(runtimeConfig?.ENABLE_TELEGRAM_LOGIN));
// 从localStorage读取记住的密码信息
const rememberedCredentials = localStorage.getItem('rememberedCredentials');
@@ -275,6 +279,58 @@ function LoginPageClient() {
}
};
const handleTelegramLogin = async () => {
setError(null);
setTelegramLoginHint(null);
try {
setTelegramLoginLoading(true);
const createRes = await fetch('/api/telegram/login/create', { method: 'POST' });
const createData = await createRes.json().catch(() => ({}));
if (!createRes.ok) {
setError(createData.error || 'Telegram 登录未启用');
return;
}
setTelegramLoginHint('请在 Telegram 中确认登录');
window.open(createData.deepLink, '_blank', 'noopener,noreferrer');
const startedAt = Date.now();
const timer = window.setInterval(async () => {
if (Date.now() - startedAt > 5 * 60 * 1000) {
window.clearInterval(timer);
setTelegramLoginLoading(false);
setTelegramLoginHint(null);
setError('Telegram 登录已超时,请重试');
return;
}
const statusRes = await fetch(`/api/telegram/login/status?token=${encodeURIComponent(createData.token)}`);
const statusData = await statusRes.json().catch(() => ({}));
if (statusData.status === 'confirmed') {
window.clearInterval(timer);
const redirect = searchParams.get('redirect') || '/';
window.location.replace(redirect);
} else if (statusData.status === 'denied') {
window.clearInterval(timer);
setTelegramLoginLoading(false);
setTelegramLoginHint(null);
setError('已拒绝 Telegram 登录');
} else if (statusData.status === 'expired') {
window.clearInterval(timer);
setTelegramLoginLoading(false);
setTelegramLoginHint(null);
setError('Telegram 登录已过期');
}
}, 2000);
} catch (error) {
setError('Telegram 登录请求失败,请稍后重试');
setTelegramLoginLoading(false);
setTelegramLoginHint(null);
}
};
return (
@@ -400,6 +456,36 @@ function LoginPageClient() {
)}
</form>
{/* Telegram登录按钮 */}
{telegramLoginEnabled && shouldAskUsername && (
<div className='mt-6'>
<div className='relative'>
<div className='absolute inset-0 flex items-center'>
<div className='w-full border-t border-gray-300 dark:border-gray-600'></div>
</div>
<div className='relative flex justify-center text-sm'>
<span className='px-2 bg-white/60 dark:bg-zinc-900/60 text-gray-500 dark:text-gray-400'>
</span>
</div>
</div>
<button
type='button'
disabled={telegramLoginLoading}
onClick={handleTelegramLogin}
className='mt-4 w-full inline-flex justify-center items-center rounded-lg border-2 border-sky-300 dark:border-sky-700 bg-white/60 dark:bg-zinc-800/60 py-3 text-base font-semibold text-sky-700 dark:text-sky-300 shadow-sm transition-all duration-200 hover:bg-sky-50 dark:hover:bg-sky-900/30 disabled:cursor-not-allowed disabled:opacity-60'
>
<Send className='w-5 h-5 mr-2' />
{telegramLoginLoading ? '等待 Telegram 确认...' : '使用 Telegram 登录'}
</button>
{telegramLoginHint && (
<p className='mt-2 text-center text-xs text-gray-500 dark:text-gray-400'>
{telegramLoginHint}
</p>
)}
</div>
)}
{/* OIDC登录按钮 */}
{siteConfig?.EnableOIDCLogin && shouldAskUsername && (
<div className='mt-6'>
+56 -1
View File
@@ -2,7 +2,7 @@
'use client';
import { AlertCircle, CheckCircle, Eye, EyeOff, User, Lock } from 'lucide-react';
import { AlertCircle, CheckCircle, Eye, EyeOff, Send, User, Lock } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react';
@@ -83,6 +83,8 @@ function RegisterPageClient() {
const [siteConfig, setSiteConfig] = useState<any>(null);
const [turnstileWidgetId, setTurnstileWidgetId] = useState<string | null>(null);
const [backgroundImage, setBackgroundImage] = useState<string>('');
const [registerSuccess, setRegisterSuccess] = useState(false);
const [telegramBind, setTelegramBind] = useState<{ code: string; deepLink?: string } | null>(null);
const { siteName } = useSite();
@@ -204,6 +206,16 @@ function RegisterPageClient() {
});
if (res.ok) {
const data = await res.json().catch(() => ({}));
if (data.telegramBind?.code) {
setTelegramBind({
code: data.telegramBind.code,
deepLink: data.telegramBind.deepLink || '',
});
setRegisterSuccess(true);
return;
}
// 注册成功,跳转到登录页
const redirect = searchParams.get('redirect') || '/login';
router.replace(redirect);
@@ -265,6 +277,48 @@ function RegisterPageClient() {
<p className='text-center text-sm text-gray-600 dark:text-gray-400 mb-8'>
</p>
{registerSuccess ? (
<div className='space-y-5'>
<div className='rounded-2xl border border-green-200 bg-green-50 p-4 text-green-800 dark:border-green-800 dark:bg-green-900/30 dark:text-green-200'>
<div className='mb-2 flex items-center gap-2 font-semibold'>
<CheckCircle className='h-5 w-5' />
</div>
<p className='text-sm'> Telegram</p>
</div>
{telegramBind && (
<div className='rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sky-900 dark:border-sky-800 dark:bg-sky-900/30 dark:text-sky-100'>
<div className='mb-3 flex items-center gap-2 font-semibold'>
<Send className='h-5 w-5' />
Telegram
</div>
<p className='text-sm'> Bot </p>
<div className='my-3 rounded-lg bg-white/80 px-3 py-2 font-mono text-lg font-bold tracking-widest dark:bg-zinc-900/70'>
/bind {telegramBind.code}
</div>
{telegramBind.deepLink && (
<button
type='button'
onClick={() => window.open(telegramBind.deepLink, '_blank', 'noopener,noreferrer')}
className='mb-3 inline-flex w-full items-center justify-center rounded-lg bg-sky-600 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-sky-700'
>
Telegram
</button>
)}
<p className='text-xs opacity-80'> 10 </p>
</div>
)}
<button
type='button'
onClick={() => router.replace(searchParams.get('redirect') || '/login')}
className='inline-flex w-full justify-center rounded-lg bg-green-600 py-3 text-base font-semibold text-white shadow-lg transition-all duration-200 hover:bg-green-700'
>
</button>
</div>
) : (
<form onSubmit={handleSubmit} className='space-y-6'>
<div>
<label htmlFor='username' className='sr-only'>
@@ -402,6 +456,7 @@ function RegisterPageClient() {
</button>
</div>
</form>
)}
</div>
{/* 版本信息显示 */}