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,
});