Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

View File

@@ -0,0 +1,356 @@
import { Request, Response } from 'express';
import { OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken } from '../models/oauth2';
import { User } from '../models/User';
import { generateRandomString } from '../utils/crypto';
import { Session } from 'express-session';
import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';
import { config } from '../config';
// JWT Payload 类型定义
interface UserPayload {
_id: string;
username: string;
fullname: string;
email: string;
isAdmin: boolean;
}
// 添加 session 接口声明
interface CustomSession extends Session {
userId?: string;
isAuthenticated?: boolean;
}
// 扩展 Request 类型
interface CustomRequest extends Request {
session: CustomSession;
}
export class OAuth2Controller {
// 授权端点
async authorize(req: CustomRequest, res: Response) {
try {
// 调试日志:输出请求信息
console.log('--- OAuth2 authorize 调试信息 ---');
console.log('req.headers:', req.headers);
console.log('req.originalUrl:', req.originalUrl);
const { client_id, redirect_uri, scope, response_type, state } = req.query;
// 统一使用 JWT 认证,从多个来源获取 token
let token: string | undefined;
let currentUser: UserPayload | null = null;
// 1. 检查 Authorization 头
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
// 2. 检查 cookie如果存在
if (!token && req.headers.cookie) {
const cookies: Record<string, string> = {};
req.headers.cookie.split(';').forEach(cookie => {
const parts = cookie.trim().split('=');
if (parts.length === 2) {
cookies[parts[0]] = parts[1];
}
});
// 尝试从 cookie 中获取 token
if (cookies.token) {
token = cookies.token;
}
}
// 3. 验证 token 并获取用户信息
if (token) {
try {
const decoded = jwt.verify(token, config.JWT_SECRET);
if (typeof decoded === 'object' && decoded !== null && '_id' in decoded) {
// 伪造session让后续逻辑认为已登录
req.session.userId = decoded._id as string;
req.session.isAuthenticated = true;
currentUser = decoded as UserPayload;
console.log('JWT验证成功用户信息:', {
userId: currentUser._id,
username: currentUser.username
});
}
} catch (error) {
console.log('JWT验证失败:', error);
}
}
// 如果用户未登录,重定向到登录页面
if (!currentUser) {
console.log('用户未登录,重定向到登录页');
const redirectPath = '/api' + req.originalUrl;
const loginUrl = `/login?redirect=${encodeURIComponent(redirectPath)}`;
return res.redirect(loginUrl);
}
// 用户已登录继续OAuth2授权流程
console.log('用户已登录继续OAuth2授权流程');
// 从数据库中获取完整用户信息
const user = await User.findById(currentUser._id);
if (!user) {
console.log('数据库中未找到用户:', currentUser._id);
return res.status(401).json({ error: 'user_not_found' });
}
// 验证客户端
const client = await OAuth2Client.findOne({ clientId: client_id });
if (!client) {
return res.status(400).json({ error: 'invalid_client' });
}
console.log('OAuth2 authorize start:', {
user: user.username,
clientId: client_id
});
// 查找现有的用户关联
const existingLink = await OAuth2Client.findOne({
clientId: client_id,
'linkedUsers.username': user.username
});
console.log('OAuth2 link check:', {
username: user.username,
exists: !!existingLink,
linkDetails: existingLink
});
// 只在没有现有关联时创建新的关联
if (!existingLink) {
console.log('Creating new OAuth link');
await OAuth2Client.updateOne(
{ clientId: client_id },
{
$addToSet: {
linkedUsers: {
userId: user._id,
username: user.username,
email: user.email
}
}
}
);
} else {
console.log('Using existing OAuth link - proceeding with authorization');
}
// 生成授权码
const code = generateRandomString(32);
const authCode = new OAuth2AuthorizationCode({
code,
clientId: client_id,
userId: user._id,
redirectUri: redirect_uri,
scope: (typeof scope === 'string' ? scope.split(' ') : []) || [],
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
});
await authCode.save();
console.log('Generated auth code:', { code, userId: user._id });
// 重定向回客户端
if (!redirect_uri || typeof redirect_uri !== 'string') {
return res.status(400).json({ error: 'invalid_redirect_uri' });
}
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', code);
redirectUrl.searchParams.set('state', state?.toString() || '');
console.log('Redirecting to:', redirectUrl.toString());
return res.redirect(redirectUrl.toString());
} catch (error: any) {
console.error('OAuth2 authorize error:', error);
throw error;
}
}
// 令牌端点
async token(req: Request, res: Response) {
try {
console.log('=== OAuth2 Token Debug Info ===');
console.log('Token request body:', req.body);
const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;
// 验证客户端
const client = await OAuth2Client.findOne({
clientId: client_id
});
if (!client) {
console.log('Client not found:', client_id);
return res.status(401).json({ error: 'invalid_client' });
}
if (grant_type === 'authorization_code') {
// 查找授权码
const authCode = await OAuth2AuthorizationCode.findOne({ code });
console.log('Found auth code:', authCode);
if (!authCode || authCode.expiresAt < new Date()) {
console.log('Invalid or expired auth code');
return res.status(400).json({ error: 'invalid_grant' });
}
// 查找用户
const user = await User.findById(authCode.userId);
// 生成访问令牌
const accessToken = generateRandomString(32);
const token = new OAuth2AccessToken({
accessToken,
clientId: client_id,
userId: authCode.userId,
scope: authCode.scope,
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
});
await token.save();
console.log('Generated access token:', accessToken);
// 生成 id_token只要 scope 里有 openid
let id_token;
if (authCode.scope.includes('openid')) {
const jwt = require('jsonwebtoken');
id_token = jwt.sign(
{
sub: user._id.toString(),
name: user.username,
fullname: user.fullname,
email: user.email,
// 可根据需要添加其它 claim
},
process.env.OPENID_SESSION_SECRET || 'your-secret-keyq', // 用于签名的密钥
{
algorithm: 'HS256',
expiresIn: '1h',
issuer: process.env.OPENID_ISSUER || 'https://d1kt.cn',
audience: client_id,
}
);
}
// 返回令牌响应
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: authCode.scope.join(' '),
...(id_token ? { id_token } : {}),
});
}
return res.status(400).json({ error: 'unsupported_grant_type' });
} catch (error: any) {
console.error('OAuth2 Token Error:', {
message: error.message,
stack: error.stack,
body: req.body
});
return res.status(500).json({
error: 'server_error',
error_description: error.message
});
}
}
// 用户信息端点
async userinfo(req: Request, res: Response) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'invalid_token' });
}
const accessToken = authHeader.substring(7);
try {
const token = await OAuth2AccessToken.findOne({
accessToken,
expiresAt: { $gt: new Date() }
});
if (!token) {
return res.status(401).json({ error: 'invalid_token' });
}
// 查找已关联的用户
const linkedUser = await OAuth2Client.findOne({
clientId: token.clientId,
'linkedUsers.userId': token.userId
});
const user = await User.findById(token.userId);
if (!user) {
return res.status(401).json({ error: 'user_not_found' });
}
// 根据scope返回用户信息
const userInfo: any = {};
if (token.scope.includes('openid')) {
userInfo.sub = user._id;
}
if (token.scope.includes('profile')) {
userInfo.name = user.username;
userInfo.fullname = user.fullname;
}
if (token.scope.includes('email')) {
userInfo.email = user.email;
}
if (token.scope.includes('firstname')) {
userInfo.firstname = user.username;
}
if (token.scope.includes('lastname')) {
userInfo.lastname = user.fullname;
}
if (token.scope.includes('username')) {
userInfo.username = user.username;
}
console.log('user', user);
console.log('userInfo', userInfo);
console.log('Token scopes:', token.scope);
console.log('最终返回的 userInfo:', userInfo);
res.json(userInfo);
} catch (error) {
res.status(500).json({ error: 'server_error6' });
}
}
async getMoodleSesskey(req: Request, res: Response) {
console.log('Received request for moodle-sesskey'); // 请求开始日志
try {
console.log('Fetching Moodle login page...');
const response = await fetch('https://m.d1kt.cn/login/index.php');
const html = await response.text();
console.log('Moodle response received, length:', html.length); // 检查是否获取到响应
const sesskeyMatch = html.match(/sesskey":"([^"]+)/);
console.log('Sesskey match result:', sesskeyMatch); // 检查正则匹配结果
const sesskey = sesskeyMatch ? sesskeyMatch[1] : '';
if (!sesskey) {
console.log('No sesskey found in response'); // 未找到 sesskey
return res.status(500).json({ error: 'Failed to get sesskey' });
}
console.log('Successfully got sesskey:', sesskey); // 成功获取 sesskey
res.json({ sesskey });
} catch (error) {
console.error('Get sesskey error:', error); // 详细的错误信息
res.status(500).json({ error: 'Failed to get sesskey' });
}
}
}