guanghulab/server/app/modules/oauth-providers.js
铸渊 ICE-GL-ZY001 f418aec5d8
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
cz: D119 temporal-brain同步 · 衔光频道首次推送 · glada日志归档
2026-06-03 11:45:50 +08:00

286 lines
7.0 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 || '565183519@qq.com').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
};