diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d50e803..95af345 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -84,6 +84,9 @@ jobs: OAUTH_GITEE_CLIENT_ID: ${{ secrets.OAUTH_GITEE_CLIENT_ID }} OAUTH_GITEE_CLIENT_SECRET: ${{ secrets.OAUTH_GITEE_CLIENT_SECRET }} OAUTH_GITEE_REDIRECT_URI: ${{ secrets.OAUTH_GITEE_REDIRECT_URI }} + OAUTH_TELEGRAM_BOT_NAME: ${{ secrets.OAUTH_TELEGRAM_BOT_NAME }} + OAUTH_TELEGRAM_BOT_TOKEN: ${{ secrets.OAUTH_TELEGRAM_BOT_TOKEN }} + run: | SECRETS="OAUTH_ALLOWED_USERS ENCRYPTION_KEY @@ -94,6 +97,7 @@ jobs: "OAUTH_CLOUDFLARE_CLIENT_ID" "OAUTH_CLOUDFLARE_CLIENT_SECRET" "OAUTH_CLOUDFLARE_ORG_DOMAIN" "OAUTH_CLOUDFLARE_REDIRECT_URI" "OAUTH_NODELOC_CLIENT_ID" "OAUTH_NODELOC_CLIENT_SECRET" "OAUTH_NODELOC_REDIRECT_URI" "OAUTH_GITEE_CLIENT_ID" "OAUTH_GITEE_CLIENT_SECRET" "OAUTH_GITEE_REDIRECT_URI" + "OAUTH_TELEGRAM_BOT_NAME" "OAUTH_TELEGRAM_BOT_TOKEN" ) for VAR in "${OPTIONAL_VARS[@]}"; do @@ -131,6 +135,8 @@ jobs: OAUTH_GITEE_CLIENT_ID: ${{ secrets.OAUTH_GITEE_CLIENT_ID }} OAUTH_GITEE_CLIENT_SECRET: ${{ secrets.OAUTH_GITEE_CLIENT_SECRET }} OAUTH_GITEE_REDIRECT_URI: ${{ secrets.OAUTH_GITEE_REDIRECT_URI }} + OAUTH_TELEGRAM_BOT_NAME: ${{ secrets.OAUTH_TELEGRAM_BOT_NAME }} + OAUTH_TELEGRAM_BOT_TOKEN: ${{ secrets.OAUTH_TELEGRAM_BOT_TOKEN }} ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} JWT_SECRET: ${{ secrets.JWT_SECRET }} diff --git a/backend/src/config.ts b/backend/src/config.ts index 71be798..0e2d05c 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -28,6 +28,8 @@ export const CSP_POLICY = { "data:", "blob:", "https://avatars.githubusercontent.com", + "https://t.me", // Telegram User Avatars + "https://*.telesco.pe", // Telegram Avatar CDN "https://www.nodeloc.com", ], // 连接源: 允许 API 请求、Cloudflare 统计上报 @@ -38,6 +40,10 @@ export const CSP_POLICY = { "https://cloudflareinsights.com", "https://static.cloudflareinsights.com", ], + // 框架源 + FRAMES: [ + "'self'", + ], }; // Cloudflare Workers 环境变量类型定义 @@ -56,6 +62,8 @@ export type EnvBindings = { OAUTH_GITEE_CLIENT_ID?: string; OAUTH_GITEE_CLIENT_SECRET?: string; OAUTH_GITEE_REDIRECT_URI?: string; + OAUTH_TELEGRAM_BOT_NAME?: string; // e.g. MyAuthBot + OAUTH_TELEGRAM_BOT_TOKEN?: string; OAUTH_ALLOWED_USERS: string; // 允许登录的 Email 或 Username 白名单 (必填) JWT_SECRET: string; ENCRYPTION_KEY: string; diff --git a/backend/src/index.ts b/backend/src/index.ts index 3dcc5cd..fa6ba6b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -8,6 +8,7 @@ import { EnvBindings, CSP_POLICY } from './config'; import authRoutes from './routes/auth'; import accountsRoutes from './routes/accounts'; import backupRoutes, { handleScheduledBackup } from './routes/backups'; +import telegramRoutes from './routes/telegram'; // 扩展 EnvBindings 以包含 ASSETS (Cloudflare Pages/Workers Assets) type Bindings = EnvBindings & { ASSETS: { fetch: (req: Request) => Promise } }; @@ -34,6 +35,7 @@ app.use('*', secureHeaders({ imgSrc: CSP_POLICY.IMAGES, // 使用 config.ts 中的配置 connectSrc: CSP_POLICY.CONNECT,// 使用 config.ts 中的配置 fontSrc: ["'self'", "data:"], + frameSrc: CSP_POLICY.FRAMES, // 使用 config.ts 中的配置 workerSrc: ["'self'", "blob:"], // 允许 Service Worker objectSrc: ["'none'"], // 禁止 Flash 等插件 }, @@ -46,6 +48,7 @@ app.get('/api', (c) => c.text('🔐 2FA Secure Manager API is running!')); app.route('/api/oauth', authRoutes); app.route('/api/accounts', accountsRoutes); app.route('/api/backups', backupRoutes); +app.route('/api/telegram', telegramRoutes); // 挂载 Telegram Webhook 路由 // 4. API 404 处理 (必须在静态资源 fallback 之前,确保 API 路径返回 JSON) app.all('/api/*', (c) => { diff --git a/backend/src/providers/oauth/BaseOAuthProvider.ts b/backend/src/providers/oauth/BaseOAuthProvider.ts index 324e803..72b8ae0 100644 --- a/backend/src/providers/oauth/BaseOAuthProvider.ts +++ b/backend/src/providers/oauth/BaseOAuthProvider.ts @@ -26,5 +26,6 @@ export abstract class BaseOAuthProvider { abstract getAuthorizeUrl(state: string): Promise<{ url: string, codeVerifier?: string }> | { url: string, codeVerifier?: string }; // 2. 处理回调:Code -> Token -> UserInfo - abstract handleCallback(code: string, codeVerifier?: string): Promise; + // 修改:支持传入 URLSearchParams (Telegram 需要验证所有参数) 或 string (标准 OAuth code) + abstract handleCallback(params: string | URLSearchParams, codeVerifier?: string): Promise; } \ No newline at end of file diff --git a/backend/src/providers/oauth/CloudflareAccessProvider.ts b/backend/src/providers/oauth/CloudflareAccessProvider.ts index 0cc7bb3..72ff68c 100644 --- a/backend/src/providers/oauth/CloudflareAccessProvider.ts +++ b/backend/src/providers/oauth/CloudflareAccessProvider.ts @@ -80,7 +80,14 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { return { url, codeVerifier: verifier }; } - async handleCallback(code: string, codeVerifier?: string): Promise { + async handleCallback(params: string | URLSearchParams, codeVerifier?: string): Promise { + // 兼容处理:如果是 URLSearchParams,提取 code;如果是 string,直接使用 + const code = typeof params === 'string' ? params : params.get('code'); + + if (!code) { + throw new AppError('Cloudflare Access OAuth callback missing code', 400); + } + const clientId = this.env.OAUTH_CLOUDFLARE_CLIENT_ID; const clientSecret = this.env.OAUTH_CLOUDFLARE_CLIENT_SECRET; const redirectUri = this.env.OAUTH_CLOUDFLARE_REDIRECT_URI; diff --git a/backend/src/providers/oauth/GitHubProvider.ts b/backend/src/providers/oauth/GitHubProvider.ts index 0cc74a5..16454d3 100644 --- a/backend/src/providers/oauth/GitHubProvider.ts +++ b/backend/src/providers/oauth/GitHubProvider.ts @@ -26,7 +26,14 @@ export class GitHubProvider extends BaseOAuthProvider { return { url: `https://github.com/login/oauth/authorize?${params.toString()}` }; } - async handleCallback(code: string, _codeVerifier?: string): Promise { + async handleCallback(params: string | URLSearchParams, _codeVerifier?: string): Promise { + // 兼容处理:如果是 URLSearchParams,提取 code;如果是 string,直接使用 + const code = typeof params === 'string' ? params : params.get('code'); + + if (!code) { + throw new AppError('GitHub OAuth callback missing code', 400); + } + // 1. 使用 Code 换取 Access Token const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', diff --git a/backend/src/providers/oauth/GiteeProvider.ts b/backend/src/providers/oauth/GiteeProvider.ts index 5ed7286..df13adb 100644 --- a/backend/src/providers/oauth/GiteeProvider.ts +++ b/backend/src/providers/oauth/GiteeProvider.ts @@ -31,7 +31,14 @@ export class GiteeProvider extends BaseOAuthProvider { return { url: `https://gitee.com/oauth/authorize?${params.toString()}` }; } - async handleCallback(code: string, _codeVerifier?: string): Promise { + async handleCallback(params: string | URLSearchParams, _codeVerifier?: string): Promise { + // 兼容处理:如果是 URLSearchParams,提取 code;如果是 string,直接使用 + const code = typeof params === 'string' ? params : params.get('code'); + + if (!code) { + throw new AppError('Gitee OAuth callback missing code', 400); + } + const clientId = this.env.OAUTH_GITEE_CLIENT_ID; const clientSecret = this.env.OAUTH_GITEE_CLIENT_SECRET; const redirectUri = this.env.OAUTH_GITEE_REDIRECT_URI; diff --git a/backend/src/providers/oauth/NodeLocProvider.ts b/backend/src/providers/oauth/NodeLocProvider.ts index c2e4d9e..cd22fb2 100644 --- a/backend/src/providers/oauth/NodeLocProvider.ts +++ b/backend/src/providers/oauth/NodeLocProvider.ts @@ -31,7 +31,14 @@ export class NodeLocProvider extends BaseOAuthProvider { return { url: `https://www.nodeloc.com/oauth-provider/authorize?${params.toString()}` }; } - async handleCallback(code: string, _codeVerifier?: string): Promise { + async handleCallback(params: string | URLSearchParams, _codeVerifier?: string): Promise { + // 兼容处理:如果是 URLSearchParams,提取 code;如果是 string,直接使用 + const code = typeof params === 'string' ? params : params.get('code'); + + if (!code) { + throw new AppError('NodeLoc OAuth callback missing code', 400); + } + const clientId = this.env.OAUTH_NODELOC_CLIENT_ID; const clientSecret = this.env.OAUTH_NODELOC_CLIENT_SECRET; const redirectUri = this.env.OAUTH_NODELOC_REDIRECT_URI; diff --git a/backend/src/providers/oauth/TelegramProvider.ts b/backend/src/providers/oauth/TelegramProvider.ts new file mode 100644 index 0000000..a330fbf --- /dev/null +++ b/backend/src/providers/oauth/TelegramProvider.ts @@ -0,0 +1,101 @@ +import { BaseOAuthProvider, OAuthUserInfo } from './BaseOAuthProvider'; +import { EnvBindings, AppError } from '../../config'; + +export class TelegramProvider extends BaseOAuthProvider { + readonly id = 'telegram'; + readonly name = 'Telegram'; + readonly color = '#54a9eb'; + readonly icon = ''; + readonly whitelistFields = ['username', 'id']; // Telegram 不保证提供 email + + constructor(env: EnvBindings) { + super(env); + } + + // 修改为 Deep Link 模式:生成跳转到 Bot 的链接,并携带 state 参数 + // 例如:https://t.me/MyAuthBot?start=state_value + getAuthorizeUrl(state: string) { + const botName = this.env.OAUTH_TELEGRAM_BOT_NAME; + if (!botName) throw new AppError('Telegram Bot Name not configured', 500); + + // 注意:Telegram start 参数只允许 [A-Za-z0-9_-],UUID 通常符合,但最好移除连字符以防万一 + // 这里我们直接使用 state,但在前端生成 state 时最好确保它是 URL 安全的 + return { url: `https://t.me/${botName}?start=${state}` }; + } + + async handleCallback(params: string | URLSearchParams, _codeVerifier?: string): Promise { + if (typeof params === 'string') { + throw new AppError('Telegram provider requires full query parameters, not just code', 400); + } + + const botToken = this.env.OAUTH_TELEGRAM_BOT_TOKEN; + if (!botToken) throw new AppError('Telegram Bot Token not configured', 500); + + // 1. 提取并验证必要字段 + const hash = params.get('hash'); + if (!hash) throw new AppError('Missing hash signature', 400); + + const dataCheckArr: string[] = []; + const allowedKeys = ['auth_date', 'first_name', 'id', 'last_name', 'photo_url', 'username']; + + // 2. 构造 data-check-string (按字母顺序排序 key=value) + params.forEach((value, key) => { + if (key !== 'hash' && allowedKeys.includes(key)) { + dataCheckArr.push(`${key}=${value}`); + } + }); + dataCheckArr.sort(); + const dataCheckString = dataCheckArr.join('\n'); + + // 3. 验证签名 (HMAC-SHA256) + // Secret key 是 Bot Token 的 SHA256 哈希 + const encoder = new TextEncoder(); + const secretKeyData = await crypto.subtle.digest('SHA-256', encoder.encode(botToken)); + + const key = await crypto.subtle.importKey( + 'raw', + secretKeyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['verify'] + ); + + const signature = this.hexToBuf(hash); + const isValid = await crypto.subtle.verify( + 'HMAC', + key, + signature, + encoder.encode(dataCheckString) + ); + + if (!isValid) { + throw new AppError('Telegram signature verification failed', 403); + } + + // 4. 检查时效性 (Telegram 建议检查 auth_date) + const authDate = parseInt(params.get('auth_date') || '0'); + const now = Math.floor(Date.now() / 1000); + if (now - authDate > 86400) { // 24小时过期 + throw new AppError('Telegram login data expired', 401); + } + + // 5. 返回用户信息 + // 注意:Telegram 默认不返回 Email,除非使用 Passport (复杂) + // 这里我们只能依赖 ID 或 Username + return { + id: params.get('id')!, + username: params.get('username') || `tg_user_${params.get('id')}`, + email: '', // Telegram 登录通常没有 Email + avatar: params.get('photo_url') || undefined, + provider: this.id + }; + } + + private hexToBuf(hex: string): ArrayBuffer { + const view = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + view[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return view.buffer; + } +} \ No newline at end of file diff --git a/backend/src/providers/oauth/index.ts b/backend/src/providers/oauth/index.ts index 68bbf58..4656936 100644 --- a/backend/src/providers/oauth/index.ts +++ b/backend/src/providers/oauth/index.ts @@ -4,6 +4,7 @@ import { GitHubProvider } from './GitHubProvider'; import { CloudflareAccessProvider } from './CloudflareAccessProvider'; import { NodeLocProvider } from './NodeLocProvider'; import { GiteeProvider } from './GiteeProvider'; +import { TelegramProvider } from './TelegramProvider'; export function getOAuthProvider(providerId: string, env: EnvBindings): BaseOAuthProvider { switch (providerId.toLowerCase()) { @@ -15,6 +16,8 @@ export function getOAuthProvider(providerId: string, env: EnvBindings): BaseOAut return new CloudflareAccessProvider(env); case 'nodeloc': return new NodeLocProvider(env); + case 'telegram': + return new TelegramProvider(env); default: throw new AppError(`Provider '${providerId}' is not supported`, 400); } @@ -27,6 +30,7 @@ export function getAvailableProviders(env: EnvBindings) { const cloudflareProvider = new CloudflareAccessProvider(env); const nodelocProvider = new NodeLocProvider(env); const giteeProvider = new GiteeProvider(env); + const telegramProvider = new TelegramProvider(env); if (env.OAUTH_GITHUB_CLIENT_ID && env.OAUTH_GITHUB_CLIENT_SECRET) { providers.push({ @@ -37,12 +41,12 @@ export function getAvailableProviders(env: EnvBindings) { }); } - if (env.OAUTH_GITEE_CLIENT_ID && env.OAUTH_GITEE_CLIENT_SECRET) { + if (env.OAUTH_TELEGRAM_BOT_TOKEN && env.OAUTH_TELEGRAM_BOT_NAME) { providers.push({ - id: giteeProvider.id, - name: giteeProvider.name, - icon: giteeProvider.icon, - color: giteeProvider.color, + id: telegramProvider.id, + name: telegramProvider.name, + icon: telegramProvider.icon, + color: telegramProvider.color, }); } @@ -55,6 +59,15 @@ export function getAvailableProviders(env: EnvBindings) { }); } + if (env.OAUTH_GITEE_CLIENT_ID && env.OAUTH_GITEE_CLIENT_SECRET) { + providers.push({ + id: giteeProvider.id, + name: giteeProvider.name, + icon: giteeProvider.icon, + color: giteeProvider.color, + }); + } + if (env.OAUTH_NODELOC_CLIENT_ID && env.OAUTH_NODELOC_CLIENT_SECRET) { providers.push({ id: nodelocProvider.id, diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 4ad2ec3..9c87d7a 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -12,7 +12,16 @@ const auth = new Hono<{ Bindings: EnvBindings, Variables: { user: any } }>(); // ========================================== auth.get('/providers', (c) => { const providers = getAvailableProviders(c.env); - return c.json({ success: true, providers }); + // 注入 Telegram Bot Name 给前端 Widget 使用 + const enhancedProviders = providers.map(p => { + if (p.id === 'telegram') { + // 自动移除可能存在的 @ 前缀,防止 Widget 加载失败 + const rawName = c.env.OAUTH_TELEGRAM_BOT_NAME || ''; + return { ...p, botName: rawName.replace(/^@/, '') }; + } + return p; + }); + return c.json({ success: true, providers: enhancedProviders }); }); // ========================================== @@ -37,15 +46,26 @@ auth.get('/authorize/:provider', async (c) => { // ========================================== auth.post('/callback/:provider', async (c) => { const providerName = c.req.param('provider'); - const { code, codeVerifier } = await c.req.json(); // 接收前端传回的 codeVerifier + const body = await c.req.json(); // 获取完整 Body const env = c.env; - if (!code) { - throw new AppError('Missing OAuth code', 400); - } - const provider = getOAuthProvider(providerName, env); - const userInfo = await provider.handleCallback(code, codeVerifier); + + // 构造参数:Telegram 需要验证所有字段,其他 Provider 只需要 code + let params: string | URLSearchParams; + if (providerName === 'telegram') { + params = new URLSearchParams(); + Object.entries(body).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + } else { + if (!body.code) throw new AppError('Missing OAuth code', 400); + params = body.code; + } + + const userInfo = await provider.handleCallback(params, body.codeVerifier); // 严密的安全校验:基于 Email 或 Username 的白名单 (OAUTH_WHITELIST) const allowedUsersStr = env.OAUTH_ALLOWED_USERS || ''; @@ -69,7 +89,7 @@ auth.post('/callback/:provider', async (c) => { } if (! isAllowed) { - throw new AppError(`Unauthorized user. Email: ${userEmail}, Username: ${username}`, 403); + throw new AppError(`Unauthorized user. Email: ${userEmail}, Username: ${userName}`, 403); } } @@ -84,6 +104,10 @@ auth.post('/callback/:provider', async (c) => { } }; + if (!env.JWT_SECRET) { + throw new AppError('Server configuration error: JWT_SECRET is missing', 500); + } + const token = await generateSecureJWT(payload, env.JWT_SECRET); // 1. 设置 httpOnly 的鉴权 Cookie (前端无法读取,防 XSS) @@ -128,6 +152,10 @@ auth.post('/logout', (c) => { // ========================================== auth.get('/me', authMiddleware, (c) => { const user = c.get('user'); + // 禁止浏览器缓存此接口,防止登录状态更新不及时 + c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + c.header('Pragma', 'no-cache'); + c.header('Expires', '0'); return c.json({ success: true, userInfo: user diff --git a/backend/src/routes/telegram.ts b/backend/src/routes/telegram.ts new file mode 100644 index 0000000..9959697 --- /dev/null +++ b/backend/src/routes/telegram.ts @@ -0,0 +1,85 @@ +import { Hono } from 'hono'; +import { EnvBindings } from '../config'; + +const telegram = new Hono<{ Bindings: EnvBindings }>(); + +// 处理 Telegram Webhook +telegram.post('/webhook', async (c) => { + const token = c.env.OAUTH_TELEGRAM_BOT_TOKEN; + if (!token) return c.text('Bot Token not configured', 500); + + // 1. 安全校验 (可选,建议在 setWebhook 时设置 secret_token) + // const secretToken = c.req.header('X-Telegram-Bot-Api-Secret-Token'); + // if (secretToken !== c.env.TELEGRAM_WEBHOOK_SECRET) return c.text('Unauthorized', 403); + + const update = await c.req.json(); + + // 只处理 Message 类型的 Update + if (!update.message) return c.json({ ok: true }); + + const chatId = update.message.chat.id; + const text = update.message.text || ''; + + // 2. 处理 /start 命令 + if (text.startsWith('/start')) { + // 提取参数: /start + const args = text.split(' '); + const state = args[1]; + + if (!state) { + await sendTelegramMessage(token, chatId, '欢迎使用 2FAuth Worker Bot!\n请从网页端发起登录请求。'); + return c.json({ ok: true }); + } + + // 3. 构造 Login URL 按钮 + // 这个 URL 必须是你的前端回调地址,Telegram 会在用户点击时验证域名 + // 注意:这里我们需要知道前端的域名。通常可以从环境变量或请求头获取, + // 但在 Webhook 中获取请求头可能不准确。建议在 env 中配置 FRONTEND_URL, + // 或者这里暂时硬编码/从 KV 获取。 + // 假设我们通过 Referer 或 Origin 无法获取,这里需要你确保配置了 OAUTH_TELEGRAM_BOT_DOMAIN (即你的网站域名) + // 如果没有配置,尝试从 request url 推断 (不一定准) + + // ⚠️ 关键:Login URL 必须是 HTTPS 且域名与 setdomain 一致 + // 我们构造前端的回调地址,让 Telegram 带着 hash 跳转回来 + // 格式: https://your-domain.com/callback/telegram + + // 这里的 state 参数其实不需要传给 callback,因为 Telegram 验证通过后, + // 前端拿到的是 hash 数据。state 主要用于防止 CSRF,但在 Login Widget 模式下, + // state 的传递比较特殊。 + // 实际上,Login URL 按钮点击后,Telegram 会请求该 URL,并附带 hash 参数。 + + // 为了简化,我们假设你的 Worker 域名就是你的前端域名 + const url = new URL(c.req.url); + const origin = `${url.protocol}//${url.hostname}`; + // 或者使用配置的域名 + // const origin = `https://${c.env.OAUTH_TELEGRAM_BOT_DOMAIN}`; + + const callbackUrl = `${origin}/callback/telegram`; + + await sendTelegramMessage(token, chatId, '请点击下方按钮完成登录验证:', { + inline_keyboard: [[ + { + text: '🔐 确认登录', + login_url: { url: callbackUrl, request_write_access: true } + } + ]] + }); + } + + return c.json({ ok: true }); +}); + +async function sendTelegramMessage(token: string, chatId: number, text: string, replyMarkup?: any) { + const url = `https://api.telegram.org/bot${token}/sendMessage`; + await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: chatId, + text: text, + reply_markup: replyMarkup + }) + }); +} + +export default telegram; \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 5d4fe0a..6d5eb4a 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -11,12 +11,18 @@ const routes = [ { path: '/login', name: 'Login', - component: () => import('../views/Login.vue') + component: () => import('../views/Login.vue'), + meta: { guestOnly: true } // 仅限游客访问 (已登录会自动跳转首页) }, { - path: '/oauth/callback', // 匹配 GitHub 回调的路径 + path: '/oauth/callback', // 统一的回调路径 (GitHub, Telegram, etc.) name: 'OAuthCallback', component: () => import('../views/OAuthCallback.vue') + }, + { + path: '/callback/:provider', // 兼容特定 Provider 的回调路径 (如 /callback/telegram) + name: 'ProviderCallback', + component: () => import('../views/OAuthCallback.vue') } ] @@ -30,16 +36,28 @@ router.beforeEach(async (to, from, next) => { // 1. 检查内存中是否有用户信息 (乐观检查) let isAuthenticated = !!(userState.userInfo && userState.userInfo.id) - // 2. 如果内存无状态且目标页需要登录,尝试通过 Cookie 恢复会话 (例如刷新页面后) - if (!isAuthenticated && to.meta.requiresAuth) { - isAuthenticated = await userState.fetchUserInfo() + // 2. 如果内存无状态,但目标页需要登录 OR 是仅游客页面(如登录页),尝试通过 Cookie 恢复会话 + // (Option 1B: 即使访问登录页,也先确认是否已登录,防止误判) + if (!isAuthenticated && (to.meta.requiresAuth || to.meta.guestOnly)) { + try { + await userState.fetchUserInfo() + isAuthenticated = !!(userState.userInfo && userState.userInfo.id) + } catch (e) {} } + // 3. 处理需要登录的页面 if (to.meta.requiresAuth && !isAuthenticated) { next('/login') - } else { - next() + return } + + // 4. 处理仅限游客的页面 (Option 2B: 已登录用户访问登录页则跳转首页) + if (to.meta.guestOnly && isAuthenticated) { + next('/') + return + } + + next() }) export default router \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index cbea31d..7d0b9aa 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -8,23 +8,23 @@
- - - 通过 {{ provider.name }} 授权登录 - +