From 21a2a51ba684bde999278cf69df08a4386e0b338 Mon Sep 17 00:00:00 2001 From: wood chen Date: Fri, 21 Feb 2025 21:12:57 +0800 Subject: [PATCH] feat: Enhance OAuth authentication with robust URL parsing and error logging --- .cursor/rules/newrule.mdc | 131 +++++++++++++++++++++++++ src/components/auth/user-auth-form.tsx | 33 ++++++- 2 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 .cursor/rules/newrule.mdc diff --git a/.cursor/rules/newrule.mdc b/.cursor/rules/newrule.mdc new file mode 100644 index 0000000..356ad18 --- /dev/null +++ b/.cursor/rules/newrule.mdc @@ -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) 文件。 diff --git a/src/components/auth/user-auth-form.tsx b/src/components/auth/user-auth-form.tsx index 103894c..8af1864 100644 --- a/src/components/auth/user-auth-form.tsx +++ b/src/components/auth/user-auth-form.tsx @@ -26,12 +26,28 @@ export function UserAuthForm({ const body: Record = {}; const callbackUrl = searchParams?.get("callbackUrl"); + console.log("Original callbackUrl:", callbackUrl); + // 如果是 OAuth 回调,则提取原始的 /oauth/authorize 部分 if (callbackUrl?.includes("/oauth/authorize")) { - const url = new URL(callbackUrl); - body.return_url = `${window.location.origin}${url.pathname}${url.search}`; + try { + // 先解码一次 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", { method: "POST", headers: { @@ -41,16 +57,25 @@ export function UserAuthForm({ }); if (!response.ok) { - throw new Error("登录请求失败"); + const errorText = await response.text(); + console.error("服务器响应:", errorText); + throw new Error(`登录请求失败: ${errorText}`); } const data = await response.json(); + console.log("收到 SSO URL:", data.sso_url); + + // 确保在跳转前重置加载状态 + setIsLoading(false); window.location.href = data.sso_url; } catch (error) { console.error("登录错误:", error); toast({ title: "错误", - description: "登录过程中发生错误,请稍后重试", + description: + error instanceof Error + ? error.message + : "登录过程中发生错误,请稍后重试", variant: "destructive", }); setIsLoading(false);