feat: Enhance OAuth authentication with robust URL parsing and error logging

This commit is contained in:
wood chen 2025-02-21 21:12:57 +08:00
parent c176362949
commit 21a2a51ba6
2 changed files with 160 additions and 4 deletions

131
.cursor/rules/newrule.mdc Normal file
View File

@ -0,0 +1,131 @@
---
description:
globs:
---
# Discourse Connect
这是一个基于Next.js, 实现了使用 Discourse SSO (Single Sign-On) 用户系统的 OAuth 认证功能。
前端UI使用shadcn/ui.
> shadcn安装组件的命令,举例: npx shadcn@latest add button
本项目是一个中间项目, 用于允许用户通过oauth2.0认证的方式接入本项目, 但是实际用户信息是本项目通过SSO连接到Discourse论坛获取的.
原始项目地址: https://github.com/Tuluobo/discourse-connect
本项目主要是进行了:
1. 前端页面内容补充
2. 管理员的相关管理页面和统计页面
3. 本系统用户可以查看自己应用的统计信息
4. Navbar的导航菜单添加
5. 本系统用户可以限制自己应用的允许授权使用者
6. 首页说明的优化
整体流程应该是这样的:
用户登录接入应用 - 接入应用通过oauth2.0向本系统发起授权请求 - 本系统向Discourse论坛发起SSO请求 - 用户在Discourse论坛中进行登录 - Discourse论坛重定向到本系统, 并附带sso和sig参数 - 本系统通过sso和sig参数向Discourse论坛发起获取用户信息请求 - 本系统通过oauth2.0向接入应用发起回调请求, 并附带用户信息 - 接入应用通过oauth2.0向本系统发起获取用户信息请求 - 本系统通过oauth2.0向Discourse论坛发起获取用户信息请求 - 本系统返回用户信息给接入应用
## 项目概述
本项目提供了一个 OAuth 认证系统,允许其他应用程序使用 Discourse 论坛的用户账号进行身份验证。这样可以让用户使用他们已有的 Discourse 账号登录到您的应用程序,无需创建新的账号。
目前Discourse论坛是Q58论坛.
Q58论坛网址: https://q58.club
本项目部署网址: https://connect.q58.club
主要特性:
- 基于 Discourse SSO 的用户认证
- OAuth 2.0 协议支持
- 使用 Next.js 框架构建,提供良好的性能和开发体验
本项目部署在vercel, 数据库使用Neon.
## 基础功能的检查
1. 直接登录本系统
2. 未登录本系统, 未登录q58论坛, 检查: 用户在接入应用中登录, 然后登录本系统, 然后登录q58论坛, 正常一直回调到用户应用
3. 未登录本系统, 登录了q58论坛, 检查: 用户在接入应用中登录, 然后登录本系统, 正常回调到用户应用
## 管理员功能检查
1. 管理员登录本系统
2. 管理员在应用管理页面, 检查应用列表, 可以查看应用列表, 可以编辑应用, 可以删除应用
3. 管理员在用户管理页面, 可以查看用户列表, 可以编辑用户, 可以删除用户
4. 管理员在日志管理页面, 可以查看日志列表
## 应用控制功能检查
1. 启用/禁用 应用的功能有效检查
2. 限制授权用户 的功能有效检查
1. 限制为空时,是否所有用户都能登录
2. 新增允许用户, 是否能正常登录
3. 删除允许用户, 是否能正常限制
4. 当用户从可用变不可用, 或者不可用变可用时, 是否正确进行限制
## 用户应用接入本系统oauth2.0认证的方式:
1. 发起授权请求
将用户重定向到授权页面
const authUrl = 'https://connect.q58.club/oauth/authorize?' +
new URLSearchParams({
response_type: 'code', // 必填,固定值
client_id: 'your_client_id', // 必填您的应用ID
redirect_uri: 'https://your-app.com/callback',
state: 'random_state', // 建议提供防CSRF攻击
scope: 'read_profile' // 可选默认read_profile
});
window.location.href = authUrl;
2. 处理授权回调
在回调地址处理授权结果
// 获取访问令牌
const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
code: '授权码', // 回调参数中的code
redirect_uri: 'https://your-app.com/callback'
})
});
const { access_token, expires_in } = await response.json();
3. 获取用户信息
使用访问令牌获取用户数据
const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
headers: {
'Authorization': `Bearer ${access_token}`
}
}).then(res => res.json());
// 返回数据示例:
{
"id": "user_xxx",
"email": "user@example.com",
"username": "username",
"name": "用户昵称",
"avatar_url": "https://...",
"groups": ["group1", "group2"]
}
## 添加新功能的准则
1. 尽量简单, 尽量少修改代码
2. 不能影响已有的功能
3. 尽量不新增文件
4. 尽量使用已有的组件和函数
5. 前端页面要尽量使用shadcn的组件
## 许可证
本项目采用 MIT 许可证。详情请见 [LICENSE](LICENSE) 文件。

View File

@ -26,12 +26,28 @@ export function UserAuthForm({
const body: Record<string, any> = {}; const body: Record<string, any> = {};
const callbackUrl = searchParams?.get("callbackUrl"); const callbackUrl = searchParams?.get("callbackUrl");
console.log("Original callbackUrl:", callbackUrl);
// 如果是 OAuth 回调,则提取原始的 /oauth/authorize 部分 // 如果是 OAuth 回调,则提取原始的 /oauth/authorize 部分
if (callbackUrl?.includes("/oauth/authorize")) { if (callbackUrl?.includes("/oauth/authorize")) {
const url = new URL(callbackUrl); try {
body.return_url = `${window.location.origin}${url.pathname}${url.search}`; // 先解码一次 URL
const decodedUrl = decodeURIComponent(callbackUrl);
console.log("Decoded URL:", decodedUrl);
// 提取 /oauth/authorize 部分
const match = decodedUrl.match(/\/oauth\/authorize[^&]*/);
if (match) {
body.return_url = `${window.location.origin}${match[0]}`;
console.log("Extracted return_url:", body.return_url);
}
} catch (e) {
console.error("URL 处理错误:", e);
body.return_url = `${window.location.origin}/oauth/authorize`;
}
} }
console.log("发送请求体:", body);
const response = await fetch("/api/auth/q58", { const response = await fetch("/api/auth/q58", {
method: "POST", method: "POST",
headers: { headers: {
@ -41,16 +57,25 @@ export function UserAuthForm({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("登录请求失败"); const errorText = await response.text();
console.error("服务器响应:", errorText);
throw new Error(`登录请求失败: ${errorText}`);
} }
const data = await response.json(); const data = await response.json();
console.log("收到 SSO URL:", data.sso_url);
// 确保在跳转前重置加载状态
setIsLoading(false);
window.location.href = data.sso_url; window.location.href = data.sso_url;
} catch (error) { } catch (error) {
console.error("登录错误:", error); console.error("登录错误:", error);
toast({ toast({
title: "错误", title: "错误",
description: "登录过程中发生错误,请稍后重试", description:
error instanceof Error
? error.message
: "登录过程中发生错误,请稍后重试",
variant: "destructive", variant: "destructive",
}); });
setIsLoading(false); setIsLoading(false);