Spaces:
Sleeping
Sleeping
| 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('<h1>配置错误</h1><p>未设置 OAUTH_CLIENT_ID 或 CLIENT_ID 环境变量</p>'); | |
| } | |
| 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(`<h1>授权失败</h1><p>${errMsg}</p><a href="/test_interface.html">返回测试页面</a>`); | |
| } | |
| if (!code) { | |
| return res.status(400).send(`<h1>授权失败</h1><p>未收到授权码</p><a href="/test_interface.html">返回测试页面</a>`); | |
| } | |
| // 验证 state(不删除,保留供后续查询) | |
| if (!isValidState(state)) { | |
| return res.status(403).send(`<h1>授权失败</h1><p>State 验证失败</p><a href="/test_interface.html">返回测试页面</a>`); | |
| } | |
| // 将 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('请在浏览器中打开上述地址以开始登录流程。'); | |
| }); | |