Spaces:
Sleeping
Sleeping
File size: 6,528 Bytes
2e1c9ec 03be224 2e1c9ec 3be9c7a 2e1c9ec 03be224 3be9c7a 03b2b03 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03b2b03 3be9c7a 2e1c9ec 03b2b03 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 2e1c9ec 03be224 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | 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('请在浏览器中打开上述地址以开始登录流程。');
});
|