guanghulab/server/app/modules/oauth-providers.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

286 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
};