From 760bbdbafd5aa440afa5342cd4ea8c20d96b07cf Mon Sep 17 00:00:00 2001 From: wood chen Date: Thu, 20 Feb 2025 03:44:05 +0800 Subject: [PATCH] feat: Enhance homepage and OAuth authorization with improved design and functionality - Redesign homepage with modern layout, feature cards, and interactive tabs - Add Radix UI Tabs component for code example section - Improve OAuth authorization page with more robust parameter validation - Refactor Authorizing component with better loading state and error handling - Optimize authorization code generation and expiration logic - Update authorization flow to support standard OAuth 2.0 parameters --- README.md | 135 -------- package.json | 1 + pnpm-lock.yaml | 30 ++ src/actions/authorizing.ts | 70 ++-- src/app/()/page.tsx | 400 +++++++++++++---------- src/app/(oauth)/oauth/authorize/page.tsx | 15 + src/components/auth/authorizing.tsx | 47 ++- src/components/ui/container.tsx | 20 ++ src/components/ui/section.tsx | 15 + src/components/ui/tabs.tsx | 55 ++++ src/components/ui/typography.tsx | 95 ++++++ src/lib/oauth/authorize-url.ts | 17 +- 12 files changed, 548 insertions(+), 352 deletions(-) create mode 100644 src/components/ui/container.tsx create mode 100644 src/components/ui/section.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/typography.tsx diff --git a/README.md b/README.md index 3b14277..2e9d2d5 100644 --- a/README.md +++ b/README.md @@ -26,141 +26,6 @@ pnpm turbo 您可以通过修改 `app/page.tsx` 来开始编辑页面。当您编辑文件时,页面会自动更新。 -## 配置 - -要使用此 OAuth 系统,您需要进行以下配置: - -1. 在您的 Discourse 论坛中启用 SSO 功能。 -2. 设置环境变量: - - `NEXT_PUBLIC_HOST_URL`: 应用程序的主机 URL(不要在末尾添加 "/") - - `DATABASE_URL`: 数据库连接字符串 - - `AUTH_SECRET`: Next Auth 的密钥 - - `DISCOURSE_HOST`: 您的 Discourse 论坛 URL - - `DISCOURSE_SECRET`: 在 Discourse 中设置的 SSO secret - -## 部署 - -### 使用 Docker 部署 - -本项目支持使用 Docker 进行部署。以下是使用 Docker Compose 部署的步骤: - -1. 确保您的系统已安装 Docker 和 Docker Compose。 - -2. 在项目根目录下,运行以下命令启动服务: - - ```bash - docker-compose up -d - ``` - - 这将构建并启动 Web 应用和 PostgreSQL 数据库服务。 - -3. 应用将在 http://localhost:3000 上运行。 - -4. 要停止服务,运行: - - ```bash - docker-compose down - ``` - -### 使用 Vercel 部署 - -另一种部署 Next.js 应用程序的简单方法是使用 [Vercel 平台](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)。 - -查看我们的 [Next.js 部署文档](https://nextjs.org/docs/deployment) 了解更多详情。 - -## OAuth 2.0 接口 - -本项目实现了基于 OAuth 2.0 协议的认证系统。以下是主要的 OAuth 接口及其使用说明: - -### 1. 授权请求 - -**端点:** `https://connect.q58.club/oauth/authorize` - -**方法:** GET - -**参数:** - -- `response_type`: 必须为 "code" -- `client_id`: 您的客户端 ID -- `redirect_uri`: 授权后重定向的 URI -- `scope`: (可选)请求的权限范围 - -**示例:** - -``` -/oauth/authorize?response_type=code&client_id=your_client_id&redirect_uri=https://your-app.com/callback -``` - -### 2. 获取访问令牌 - -**端点:** `https://connect.q58.club/api/oauth/access_token` - -**方法:** POST - -**参数:** - -- `code`: 从授权请求中获得的授权码 -- `redirect_uri`: 必须与授权请求中的 redirect_uri 相同 - -**响应:** - -```json -{ - "access_token": "at_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "expires_in": 604800, - "token_type": "bearer" -} -``` - -### 3. 获取用户信息 - -**端点:** `https://connect.q58.club/api/oauth/user` - -**方法:** GET - -**请求头:** - -- `Authorization: Bearer {access_token}` - -**响应:** - -```json -{ - "id": "user_id", - "email": "user@example.com", - "username": "username", - "admin": false, - "moderator": false, - "avatar_url": "https://example.com/avatar.jpg", - "name": "User Name", - "groups": ["group1", "group2"] -} -``` - -**响应字段说明:** - -- `id`: 用户唯一标识 -- `email`: 用户邮箱 -- `username`: 用户名 -- `admin`: 是否是管理员 -- `moderator`: 是否是版主 -- `avatar_url`: 头像地址 -- `name`: 用户昵称 -- `groups`: 用户所属的论坛用户组列表 - -### 使用流程 - -1. 将用户重定向到授权页面(`/oauth/authorize`)。 -2. 用户授权后,您的应用将收到一个授权码。 -3. 使用授权码请求访问令牌(`/api/oauth/access_token`)。 -4. 使用访问令牌获取用户信息(`/api/oauth/user`)。 - -注意:确保在生产环境中使用 HTTPS 来保护所有的 OAuth 请求和响应。 - -## 贡献 - -欢迎贡献代码、报告问题或提出改进建议。 - ## 许可证 本项目采用 MIT 许可证。详情请见 [LICENSE](LICENSE) 文件。 diff --git a/package.json b/package.json index e1dcab0..f71dea4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dba85e6..3bc90c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: '@radix-ui/react-switch': specifier: ^1.1.0 version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-toast': specifier: ^1.2.1 version: 1.2.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -1088,6 +1091,33 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-tabs@1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-toast@1.2.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==} peerDependencies: diff --git a/src/actions/authorizing.ts b/src/actions/authorizing.ts index 22e1ca1..101b612 100644 --- a/src/actions/authorizing.ts +++ b/src/actions/authorizing.ts @@ -74,9 +74,11 @@ export async function handleAuthorizeAction( return { error: "请先登录" }; } + const userId = currentUser.id; + // 获取用户信息 const user = await prisma.user.findUnique({ - where: { id: currentUser.id }, + where: { id: userId }, select: { username: true }, }); @@ -85,10 +87,7 @@ export async function handleAuthorizeAction( } // 检查平台级权限 - const platformAuth = await checkPlatformAuthorization( - clientId, - currentUser.id, - ); + const platformAuth = await checkPlatformAuthorization(clientId, userId); if (!platformAuth.allowed) { return { error: platformAuth.error }; } @@ -102,39 +101,43 @@ export async function handleAuthorizeAction( return { error: appAuth.error }; } - // 检查或创建授权记录 - const existingAuth = await prisma.authorization.findUnique({ - where: { - userId_clientId: { - userId: currentUser.id, - clientId, - }, - }, - }); - - if (existingAuth) { - // 更新最后使用时间 - await prisma.authorization.update({ + // 使用事务处理授权记录的更新或创建 + await prisma.$transaction(async (tx) => { + const existingAuth = await tx.authorization.findUnique({ where: { userId_clientId: { - userId: currentUser.id, + userId, clientId, }, }, - data: { - lastUsedAt: new Date(), - }, }); - } else { - // 创建新的授权记录 - await createAuthorization({ - userId: currentUser.id, - clientId, - scope, - enabled: true, - lastUsedAt: new Date(), - }); - } + + if (existingAuth) { + // 更新最后使用时间 + await tx.authorization.update({ + where: { + userId_clientId: { + userId, + clientId, + }, + }, + data: { + lastUsedAt: new Date(), + }, + }); + } else { + // 创建新的授权记录 + await tx.authorization.create({ + data: { + userId, + clientId, + scope, + enabled: true, + lastUsedAt: new Date(), + }, + }); + } + }); // 处理重定向 const oauthParams = new URLSearchParams(atob(oauth)); @@ -149,6 +152,9 @@ export async function handleAuthorizeAction( return { redirectUrl }; } catch (error) { console.error("授权处理失败:", error); + if (error instanceof Error) { + return { error: `授权处理失败: ${error.message}` }; + } return { error: "授权处理失败,请稍后重试" }; } } diff --git a/src/app/()/page.tsx b/src/app/()/page.tsx index a78c55f..afc01bf 100644 --- a/src/app/()/page.tsx +++ b/src/app/()/page.tsx @@ -1,202 +1,270 @@ -import dynamic from "next/dynamic"; import Link from "next/link"; -import { ArrowRight } from "lucide-react"; +import { ArrowRight, CheckCircle2 } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Container } from "@/components/ui/container"; +import { Section } from "@/components/ui/section"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + TypographyH1, + TypographyH2, + TypographyLead, + TypographyMuted, +} from "@/components/ui/typography"; import { NavBar } from "@/components/layout/nav-bar"; -import { ThemeToggle } from "@/components/theme-toggle"; -// 动态导入 Logo 组件以避免服务器端渲染错误 -const DynamicLogo = dynamic(() => import("@/components/dynamic-logo"), { - ssr: false, -}); - -export default function IndexPage() { +export default function HomePage() { return (
- {/* Hero Section */} -
-
-

- Q58 Connect -

-

- 基于Q58论坛的OAuth 2.0认证服务, - 让用户使用Q58论坛账号快速登录您的应用 -

-
- - - -
-
-
- - {/* Features Section */} -
-
-
-

- 使用方法 -

-

- 只需几个简单步骤,即可在您的应用中集成 Q58 论坛的用户系统 -

-
- -
-
- {/* Step 1 */} -
-
- 1 -
-

- 创建应用 -

-

- 登录后在控制台创建您的应用,获取 Client ID 和 Client Secret -

-
- - {/* Step 2 */} -
-
- 2 -
-

- 集成代码 -

-

- 按照文档说明,在您的应用中集成 OAuth 2.0 认证流程 -

-
- - {/* Step 3 */} -
-
- 3 -
-

+
+ +
+ + Q58 Connect + + + 使用 Q58 论坛账号,一键登录您的应用 + +
+ +

-

- 用户可以使用论坛账号一键登录您的应用,无需重新注册 -

-
+ + +
+ + - {/* API Example */} -
-
-

- OAuth 2.0 认证流程 -

-
-

- 重要提示:{" "} - 授权请求必须通过浏览器重定向实现,不能使用 AJAX/Fetch - 等方式直接请求。 -

-
-
-                  {`// 1. 重定向到授权页面
-window.location.href = 'https://connect.q58.club/oauth/authorize?' + new URLSearchParams({
-  response_type: 'code',      // 必填,固定值 'code'
-  client_id: 'your_client_id',// 必填,在控制台获取的客户端ID
-  redirect_uri: 'https://your-app.com/callback' // 必填,授权后的回调地址
-});
+        
+ +
+ + + 简单集成 + + 只需几行代码,即可为您的应用添加 Q58 论坛账号登录功能 + + + +
    +
  • + + 标准 OAuth 2.0 协议 +
  • +
  • + + 完整的开发文档 +
  • +
  • + + 示例代码和SDK +
  • +
+
+
-// 2. 获取访问令牌 + + + 安全可靠 + + 采用业界最佳实践,确保您的应用和用户数据安全 + + + +
    +
  • + + HTTPS 加密传输 +
  • +
  • + + 授权码+令牌双重验证 +
  • +
  • + + 防CSRF攻击 +
  • +
+
+
+ + + + 功能丰富 + + 提供完整的用户信息和权限管理功能 + + + +
    +
  • + + 用户基本信息 +
  • +
  • + + 用户组权限 +
  • +
  • + + 管理员特权 +
  • +
+
+
+
+
+
+ +
+ + + + 快速开始 + + 按照以下步骤,快速接入 Q58 Connect + + + + + + 发起授权 + 处理回调 + 获取用户 + + + + + 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'  // 必须与授权请求中的一致
+    code: '授权码',  // 回调参数中的code
+    redirect_uri: 'https://your-app.com/callback'
   })
 });
 
-// 返回数据示例:
-{
-  "access_token": "at_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // 访问令牌,以 at_ 开头
-  "token_type": "bearer",   // 令牌类型
-  "expires_in": 604800      // 令牌有效期(秒),默认7天
-}
-
-// 3. 获取用户信息
-const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
+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}\`  // 使用上一步获取的访问令牌
+    'Authorization': \`Bearer \${access_token}\`
   }
 }).then(res => res.json());
 
 // 返回数据示例:
 {
-  "id": "user_xxxxxx",           // 用户唯一标识
-  "email": "user@example.com",   // 用户邮箱
-  "username": "username",        // 用户名
-  "name": "用户昵称",            // 用户昵称
-  "avatar_url": "https://...",   // 头像URL
-  "admin": false,               // 是否是管理员
-  "moderator": false,           // 是否是版主
-  "groups": ["group1", "group2"] // 用户所属的论坛用户组
-}`}
-                
-
-

- 权限说明: -

-
    -
  • read_profile - 获取用户基本信息,包括邮箱、用户名等
  • -
  • groups - 获取用户所属的论坛用户组信息
  • -
  • admin - 获取用户的管理权限状态
  • -
-

- 安全说明: -

-
    -
  • 授权码(code)是一次性的,使用后立即失效
  • -
  • 授权码有效期为10分钟
  • -
  • access_token 有效期为7天,请在过期前重新获取
  • -
  • 请确保回调地址(redirect_uri)与应用注册时完全一致
  • -
  • 建议使用 HTTPS 确保数据传输安全
  • -
-

- 错误说明: -

-
    -
  • 400 Invalid code params - 授权码参数缺失
  • -
  • 400 Invalid code credentials - 授权码无效或已过期
  • -
  • 400 Invalid redirect uri - 回调地址不匹配
  • -
  • 401 Invalid access token - 访问令牌无效或已过期
  • -
-
+ "id": "user_xxx", + "email": "user@example.com", + "username": "username", + "name": "用户昵称", + "avatar_url": "https://...", + "groups": ["group1", "group2"] +}`} +
+
+ + + + + + + + + +
+ +
+ 准备好了吗? + + 创建一个应用,开始使用 Q58 Connect + +
+ + +
-
-
+ +
-
); diff --git a/src/app/(oauth)/oauth/authorize/page.tsx b/src/app/(oauth)/oauth/authorize/page.tsx index 8095572..8fec27f 100644 --- a/src/app/(oauth)/oauth/authorize/page.tsx +++ b/src/app/(oauth)/oauth/authorize/page.tsx @@ -42,6 +42,21 @@ export default async function AuthorizePage({ ); } + // 验证 response_type + if (searchParams.response_type !== "code") { + return ( +
+ +
+ ); + } + const client = await getClientByClientId(searchParams.client_id); if (!client) { return ( diff --git a/src/components/auth/authorizing.tsx b/src/components/auth/authorizing.tsx index ccca1ed..21b3bd4 100644 --- a/src/components/auth/authorizing.tsx +++ b/src/components/auth/authorizing.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { handleAuthorizeAction } from "@/actions/authorizing"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ErrorCard } from "@/components/auth/error-card"; interface AuthorizingProps { @@ -20,22 +21,29 @@ export function Authorizing({ redirectUri, }: AuthorizingProps) { const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const authorize = async () => { - const result = await handleAuthorizeAction(oauth, clientId, scope); - if (result.error) { - setError(result.error); - } else if (result.redirectUrl) { - const url = await result.redirectUrl; - window.location.href = url; + try { + const result = await handleAuthorizeAction(oauth, clientId, scope); + if (result.error) { + setError(result.error); + } else if (result.redirectUrl) { + const url = await result.redirectUrl; + window.location.href = url; + } else { + setError("授权响应无效"); + } + } catch (err) { + console.error("授权过程出错:", err); + setError(err instanceof Error ? err.message : "授权过程发生未知错误"); + } finally { + setIsLoading(false); } }; - authorize().catch((err) => { - console.error("授权过程出错:", err); - setError("授权过程发生错误,请稍后重试"); - }); + authorize(); }, [oauth, clientId, scope]); if (error) { @@ -53,11 +61,18 @@ export function Authorizing({ } return ( -
-
-
正在处理授权...
-
请稍候,我们正在处理您的授权请求
-
-
+ + +
+
+
+ 正在处理授权 +
+ +

+ {isLoading ? "请稍候,我们正在处理您的授权请求" : "正在跳转..."} +

+
+
); } diff --git a/src/components/ui/container.tsx b/src/components/ui/container.tsx new file mode 100644 index 0000000..1177ebe --- /dev/null +++ b/src/components/ui/container.tsx @@ -0,0 +1,20 @@ +import { HTMLAttributes } from "react"; + +import { cn } from "@/lib/utils"; + +type ContainerProps = HTMLAttributes & { + as?: "div" | "section" | "article" | "main" | "header" | "footer"; +}; + +export function Container({ + className, + as: Component = "div", + ...props +}: ContainerProps) { + return ( + + ); +} diff --git a/src/components/ui/section.tsx b/src/components/ui/section.tsx new file mode 100644 index 0000000..2d1081b --- /dev/null +++ b/src/components/ui/section.tsx @@ -0,0 +1,15 @@ +import { HTMLAttributes } from "react"; + +import { cn } from "@/lib/utils"; + +type SectionProps = HTMLAttributes & { + as?: "section" | "div" | "article" | "main" | "header" | "footer"; +}; + +export function Section({ + className, + as: Component = "section", + ...props +}: SectionProps) { + return ; +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..ca123a0 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/typography.tsx b/src/components/ui/typography.tsx new file mode 100644 index 0000000..b5e6f67 --- /dev/null +++ b/src/components/ui/typography.tsx @@ -0,0 +1,95 @@ +import { cn } from "@/lib/utils"; + +export function TypographyH1({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function TypographyH2({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function TypographyH3({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function TypographyP({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function TypographyLead({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function TypographyLarge({ + className, + ...props +}: React.HTMLAttributes) { + return

; +} + +export function TypographySmall({ + className, + ...props +}: React.HTMLAttributes) { + return ( + + ); +} + +export function TypographyMuted({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} diff --git a/src/lib/oauth/authorize-url.ts b/src/lib/oauth/authorize-url.ts index 7b8c948..5b872a5 100644 --- a/src/lib/oauth/authorize-url.ts +++ b/src/lib/oauth/authorize-url.ts @@ -5,6 +5,17 @@ import WordArray from "crypto-js/lib-typedarrays"; import { getClientByClientId } from "@/lib/dto/client"; import { createCode } from "@/lib/dto/code"; +function generateAuthCode(): string { + // 生成一个 48 字节的随机字符串 + const randomBytes = WordArray.random(48); + // 转换为 base64 并移除可能的特殊字符 + return randomBytes + .toString() + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + export async function getAuthorizeUrl(params: URLSearchParams) { // client const client = await getClientByClientId(params.get("client_id") as string); @@ -17,14 +28,14 @@ export async function getAuthorizeUrl(params: URLSearchParams) { if (params.has("state")) { redirect_uri.searchParams.append("state", params.get("state") || ""); } - const code = WordArray.random(32).toString(); + const code = generateAuthCode(); redirect_uri.searchParams.append("code", code); - // storage code + // storage code with shorter expiration time try { await createCode({ code, - expiresAt: new Date(Date.now() + 10 * 60 * 1000), + expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5分钟过期 clientId: client.id, userId: client.userId, });