require('dotenv').config(); const express = require('express'); const axios = require('axios'); const path = require('path'); const crypto = require('crypto'); const apiEndpoints = require('./api_endpoints'); const oauthEndpoints = require('./oauth_endpoints'); const app = express(); const port = process.env.PORT || 7860; // --- 静态文件和通用配置 --- app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); const stateStore = new Map(); // 用于存储 state 的临时存储 // --- OAuth 配置 --- const HUGGINGFACE_AUTH_URL = 'https://huggingface.co/oauth/authorize'; const HUGGINGFACE_TOKEN_URL = 'https://huggingface.co/oauth/token'; const CLIENT_ID = process.env.OAUTH_CLIENT_ID || process.env.CLIENT_ID; const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET || process.env.CLIENT_SECRET; const OAUTH_SCOPES = process.env.OAUTH_SCOPES || 'openid profile inference-api'; // 在 Hugging Face Spaces 中使用 SPACE_HOST 构建回调地址 // 本地开发时使用 .env 中的 REDIRECT_URI const REDIRECT_URI = process.env.SPACE_HOST ? `https://${process.env.SPACE_HOST}/callback` : (process.env.REDIRECT_URI || `http://127.0.0.1:${port}/callback`); /** * 生成一个随机的 state 字符串 * @returns {string} 16 位的随机十六进制字符串 */ const generateRandomState = () => crypto.randomBytes(8).toString('hex'); /** * 验证回调中返回的 state 是否有效 * @param {string} state - 从回调请求中获取的 state * @returns {boolean} 如果有效则返回 true,否则返回 false */ const isValidState = (state) => { if (!state || !stateStore.has(state)) return false; const stateData = stateStore.get(state); if (Date.now() > stateData.expiresAt) { stateStore.delete(state); console.log('State 验证失败: 已过期'); return false; } console.log('State 验证成功'); return true; }; // --- 路由 --- // 1. 主页 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // 2. 引导用户到 Hugging Face 的授权页面(传统模式 - 直接重定向) app.get('/login', (req, res) => { if (!CLIENT_ID) { return res.status(500).send('
未设置 OAUTH_CLIENT_ID 或 CLIENT_ID 环境变量
'); } const state = generateRandomState(); const expiresAt = Date.now() + 10 * 60 * 1000; stateStore.set(state, { expiresAt, createdAt: Date.now() }); const authUrl = new URL(HUGGINGFACE_AUTH_URL); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', OAUTH_SCOPES); authUrl.searchParams.set('state', state); console.log('设置 state:', state); res.redirect(authUrl.href); }); /** * API: 启动 OAuth 流程(测试模式 - 返回 JSON,不自动重定向) * 每次调用生成独立的 state,通过 state 关联后续 callback */ app.get('/api/oauth/start', (req, res) => { if (!CLIENT_ID) { return res.status(500).json({ error: '未设置 OAUTH_CLIENT_ID 或 CLIENT_ID 环境变量' }); } // scope 由前端传入,允许用户自定义请求的权限范围 const scope = req.query.scope || OAUTH_SCOPES; const state = generateRandomState(); const expiresAt = Date.now() + 10 * 60 * 1000; stateStore.set(state, { expiresAt, createdAt: Date.now(), code: null }); const authUrl = new URL(HUGGINGFACE_AUTH_URL); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', scope); authUrl.searchParams.set('state', state); res.json({ step: 1, description: '已生成 state 并构建授权 URL', state: state, authorize_url: authUrl.href, params: { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: 'code', scope: scope, state: state }, next_step: '请在浏览器中打开 authorize_url,完成授权后会回调到本服务的 /callback' }); }); /** * API: 查询指定 state 关联的授权码 * 只能查询自己发起的 state 对应的 code(通过 state 隔离用户) */ app.get('/api/oauth/code/:state', (req, res) => { const { state } = req.params; if (!state || !stateStore.has(state)) { return res.status(404).json({ error: 'State 不存在或已过期' }); } const stateData = stateStore.get(state); if (!stateData.code) { return res.json({ state: state, code: null, message: '授权尚未完成,code 还未生成。请先完成授权流程。' }); } res.json({ state: state, code: stateData.code, received_at: stateData.codeReceivedAt }); }); // 3. 接收授权码 - 保存 code 到对应的 state 记录,不自动换取 token app.get('/callback', async (req, res) => { const { code, state, error, error_description } = req.query; console.log('\n--- 新的回调请求 ---'); console.log('接收授权码 code:', code); console.log('接收 state:', state); // 错误处理 if (error) { const errMsg = `授权失败: ${error} - ${error_description}`; console.error(errMsg); return res.status(400).send(`${errMsg}
返回测试页面`); } if (!code) { return res.status(400).send(`未收到授权码
返回测试页面`); } // 验证 state(不删除,保留供后续查询) if (!isValidState(state)) { return res.status(403).send(`State 验证失败
返回测试页面`); } // 将 code 保存到对应的 state 记录中(按 state 隔离) const stateData = stateStore.get(state); stateData.code = code; stateData.codeReceivedAt = new Date().toISOString(); console.log(`授权码已保存到 state=${state},未消耗`); // 重定向到测试页面,仅携带 state(不在 URL 暴露 code) res.redirect(`/test_interface.html?tab=oauth&state=${encodeURIComponent(state)}`); }); // 挂载 API 路由 app.use('/api', apiEndpoints); app.use('/api/oauth', oauthEndpoints); // 启动服务器 app.listen(port, () => { console.log(`Hugging Face OAuth 测试服务器运行在 http://127.0.0.1:${port}`); console.log('请在浏览器中打开上述地址以开始登录流程。'); });