'use strict'; const crypto = require('crypto'); const axios = require('axios'); const { URL } = require('url'); // ─── 常量 ─── const SESSION_EXPIRE_MS = 7 * 24 * 60 * 60 * 1000; // 7天 const TOKEN_BYTES = 32; const OWNER_EMAIL = (process.env.ZY_OWNER_EMAIL || '<<@committer_handle:base64>>').trim().toLowerCase(); // ─── GitHub OAuth 配置 ─── const GITHUB_CLIENT_ID = process.env.ZY_GITHUB_CLIENT_ID; const GITHUB_CLIENT_SECRET = process.env.ZY_GITHUB_CLIENT_SECRET; const GITHUB_REDIRECT_URI = process.env.ZY_GITHUB_REDIRECT_URI || 'http://localhost:3800/auth/github/callback'; // ─── Notion OAuth 配置 ─── const NOTION_CLIENT_ID = process.env.ZY_NOTION_CLIENT_ID; const NOTION_CLIENT_SECRET = process.env.ZY_NOTION_CLIENT_SECRET; const NOTION_REDIRECT_URI = process.env.ZY_NOTION_REDIRECT_URI || 'http://localhost:3800/auth/notion/callback'; // ─── 内存存储 ─── const pendingStates = new Map(); // state → { provider, createdAt } const activeSessions = new Map(); // token → { email, provider, createdAt, expiresAt } /** * 生成安全state参数(防CSRF) */ function generateState() { return crypto.randomBytes(16).toString('hex'); } /** * 生成安全session token */ function generateToken() { return crypto.randomBytes(TOKEN_BYTES).toString('hex'); } /** * GitHub OAuth 授权URL */ function getGitHubAuthUrl() { if (!GITHUB_CLIENT_ID) throw new Error('GitHub OAuth未配置: 需要ZY_GITHUB_CLIENT_ID环境变量'); const state = generateState(); const url = new URL('https://github.com/login/oauth/authorize'); url.searchParams.set('client_id', GITHUB_CLIENT_ID); url.searchParams.set('redirect_uri', GITHUB_REDIRECT_URI); url.searchParams.set('state', state); url.searchParams.set('scope', 'user:email'); pendingStates.set(state, { provider: 'github', createdAt: Date.now() }); return url.toString(); } /** * Notion OAuth 授权URL */ function getNotionAuthUrl() { if (!NOTION_CLIENT_ID) throw new Error('Notion OAuth未配置: 需要ZY_NOTION_CLIENT_ID环境变量'); const state = generateState(); const url = new URL('https://api.notion.com/v1/oauth/authorize'); url.searchParams.set('client_id', NOTION_CLIENT_ID); url.searchParams.set('redirect_uri', NOTION_REDIRECT_URI); url.searchParams.set('state', state); url.searchParams.set('response_type', 'code'); pendingStates.set(state, { provider: 'notion', createdAt: Date.now() }); return url.toString(); } /** * 验证state参数 */ function validateState(state, provider) { if (!state || !provider) return false; const pending = pendingStates.get(state); if (!pending) return false; // 检查provider匹配 if (pending.provider !== provider) return false; // 检查过期(10分钟) if (Date.now() - pending.createdAt > 10 * 60 * 1000) { pendingStates.delete(state); return false; } // 验证通过后清理state pendingStates.delete(state); return true; } /** * GitHub OAuth 回调处理 */ async function handleGitHubCallback(code, state) { if (!validateState(state, 'github')) { throw new Error('无效的state参数'); } if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) { throw new Error('GitHub OAuth未配置'); } // 1. 获取access token const tokenRes = await axios.post( 'https://github.com/login/oauth/access_token', { client_id: GITHUB_CLIENT_ID, client_secret: GITHUB_CLIENT_SECRET, code, redirect_uri: GITHUB_REDIRECT_URI }, { headers: { Accept: 'application/json' } } ); if (!tokenRes.data.access_token) { throw new Error('GitHub token获取失败'); } // 2. 获取用户邮箱 const userRes = await axios.get('https://api.github.com/user/emails', { headers: { Authorization: `token ${tokenRes.data.access_token}`, Accept: 'application/vnd.github.v3+json' } }); // 3. 查找主邮箱 const primaryEmail = userRes.data.find(e => e.primary)?.email; if (!primaryEmail) { throw new Error('无法获取GitHub主邮箱'); } // 4. 主权验证 const normalizedEmail = primaryEmail.trim().toLowerCase(); if (normalizedEmail !== OWNER_EMAIL) { throw new Error('此频道为专属频道,仅限频道主权持有者登录'); } // 5. 创建session const token = generateToken(); const expiresAt = Date.now() + SESSION_EXPIRE_MS; activeSessions.set(token, { email: normalizedEmail, provider: 'github', createdAt: Date.now(), expiresAt }); return { token, email: normalizedEmail, provider: 'github', expiresAt: new Date(expiresAt).toISOString() }; } /** * Notion OAuth 回调处理 */ async function handleNotionCallback(code, state) { if (!validateState(state, 'notion')) { throw new Error('无效的state参数'); } if (!NOTION_CLIENT_ID || !NOTION_CLIENT_SECRET) { throw new Error('Notion OAuth未配置'); } // 1. 获取access token const tokenRes = await axios.post( 'https://api.notion.com/v1/oauth/token', { grant_type: 'authorization_code', code, redirect_uri: NOTION_REDIRECT_URI }, { auth: { username: NOTION_CLIENT_ID, password: NOTION_CLIENT_SECRET }, headers: { 'Content-Type': 'application/json' } } ); if (!tokenRes.data.access_token) { throw new Error('Notion token获取失败'); } // 2. 获取用户信息 const userRes = await axios.get('https://api.notion.com/v1/users/me', { headers: { Authorization: `Bearer ${tokenRes.data.access_token}`, 'Notion-Version': '2022-06-28' } }); // 3. 提取邮箱 const email = userRes.data.bot?.owner?.user?.email || userRes.data.person?.email; if (!email) { throw new Error('无法获取Notion邮箱'); } // 4. 主权验证 const normalizedEmail = email.trim().toLowerCase(); if (normalizedEmail !== OWNER_EMAIL) { throw new Error('此频道为专属频道,仅限频道主权持有者登录'); } // 5. 创建session const token = generateToken(); const expiresAt = Date.now() + SESSION_EXPIRE_MS; activeSessions.set(token, { email: normalizedEmail, provider: 'notion', createdAt: Date.now(), expiresAt }); return { token, email: normalizedEmail, provider: 'notion', expiresAt: new Date(expiresAt).toISOString() }; } /** * 验证session token */ function validateSession(token) { if (!token || typeof token !== 'string') { return { valid: false }; } const session = activeSessions.get(token); if (!session) { return { valid: false }; } if (Date.now() > session.expiresAt) { activeSessions.delete(token); return { valid: false }; } return { valid: true, email: session.email, provider: session.provider, expiresAt: new Date(session.expiresAt).toISOString() }; } /** * 注销session */ function revokeSession(token) { activeSessions.delete(token); } module.exports = { getGitHubAuthUrl, getNotionAuthUrl, handleGitHubCallback, handleNotionCallback, validateSession, revokeSession };