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('请在浏览器中打开上述地址以开始登录流程。');
});