From 54b3d8e6618c303569ab94ba9215f933b6424169 Mon Sep 17 00:00:00 2001 From: wood chen Date: Sat, 22 Feb 2025 15:15:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9D=97=E7=BB=84=E4=BB=B6=E5=92=8C=E8=AD=A6=E5=91=8A=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 50 +-- oauth2.0的授权模式.txt | 75 +++++ src/app/()/page.tsx | 309 ++++++++++++------ src/app/(admin)/admin/clients/[id]/page.tsx | 2 +- .../(dashboard)/dashboard/clients/page.tsx | 1 - src/components/ui/alert.tsx | 59 ++++ src/components/ui/code-block.tsx | 38 +++ 7 files changed, 390 insertions(+), 144 deletions(-) create mode 100644 oauth2.0的授权模式.txt create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/code-block.tsx diff --git a/README.md b/README.md index b39b445..5431af9 100644 --- a/README.md +++ b/README.md @@ -63,55 +63,7 @@ Q58论坛网址: https://q58.club ## 用户应用接入本系统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"] -} +写在"src\app\()\page.tsx"里了. ## 添加新功能的准则 diff --git a/oauth2.0的授权模式.txt b/oauth2.0的授权模式.txt new file mode 100644 index 0000000..21d8f86 --- /dev/null +++ b/oauth2.0的授权模式.txt @@ -0,0 +1,75 @@ +授权码模式(Authorization Code) +第一步:获取授权码 + +授权码请求链接格式: + +http://localhost:8080/oauth/authorize?client_id=123456&response_type=code&scope=all&redirect_url=http://localhost:8080/oauth/token +返回的JSON格式: + +{ + "code": "123456", + "state": "123456" +} +第二步:申请令牌 + +令牌请求链接格式: + +http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_url=http://localhost:8080/oauth/callback +返回的JSON格式: + +{ + "access_token": "123456", + "token_type": "bearer", + "scope": "read", + "refresh_token": "123456" +} +资源请求链接格式: + +http://localhost:8080/oauth/resource?access_token=123456 +返回的JSON格式: + +{ + "resource": "123456" +} +简化模式(Implicit) +简化模式跳过授权码,直接获取访问令牌,适用于没有后台服务程序的单页面应用。 + +令牌请求链接格式: + +http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&response_type=token&scope=all&redirect_url=http://localhost:8080/oauth/callback +返回的JSON格式: + +{ + "access_token": "123456", + "token_type": "bearer", + "scope": "read", + "refresh_token": "123456" +} +密码模式(Password) +用户通过客户端使用用户名和密码向授权服务器请求授权,授权服务器向客户端发送访问令牌和更新令牌。 + +请求链接格式: + +http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=password&username=admin&password=admin +返回的JSON格式: + +{ + "access_token": "123456", + "token_type": "bearer", + "scope": "read", + "refresh_token": "123456" +} +客户端模式(Client Credentials) +客户端以自己的名义使用客户端ID和密钥向授权服务器请求授权,最简单的授权模式。 + +请求链接格式: + +http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=client_credentials +返回的JSON格式: + +{ + "access_token": "123456", + "token_type": "bearer", + "scope": "read", + "refresh_token": "123456" +} \ No newline at end of file diff --git a/src/app/()/page.tsx b/src/app/()/page.tsx index 9f25910..8257c2f 100644 --- a/src/app/()/page.tsx +++ b/src/app/()/page.tsx @@ -1,6 +1,15 @@ import Link from "next/link"; -import { ArrowRight, CheckCircle2, Code2, Lock, Users } from "lucide-react"; +import { + ArrowDownToLine, + ArrowRight, + CheckCircle2, + Code2, + Lock, + Send, + Users, +} from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, @@ -9,15 +18,10 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { CodeBlock } from "@/components/ui/code-block"; 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 { Header } from "@/components/layout/header"; export default function HomePage() { @@ -75,11 +79,11 @@ export default function HomePage() {
  • - 完整的开发文档 + 完整的接入文档
  • - 示例代码和SDK + 示例代码
  • @@ -105,7 +109,7 @@ export default function HomePage() {
  • - 防CSRF攻击 + Serverless部署 高SLA
  • @@ -116,22 +120,22 @@ export default function HomePage() { 功能丰富 - 提供完整的用户信息和权限管理功能 + 提供完整的应用管理, 仪表盘, API用户信息和权限管理功能
    • - 用户基本信息 + API 返回信息完善
    • - 用户组权限 + 仪表盘和管理美观界面
    • - 管理员特权 + 限制应用可用者 接入个人应用
    @@ -146,104 +150,223 @@ export default function HomePage() {
    - 快速开始 + + 接入应用 (授权码模式) + - 按照以下步骤,快速接入 Q58 Connect + 以下示例使用授权码(Authorization + Code)模式,这是最安全和最完整的OAuth 2.0授权模式, + 适用于有后端服务器的应用。 - 发起授权 - 处理回调 - 获取用户 + + + 1 + + 发起授权 + + + + 2 + + 处理回调 + + + + 3 + + 获取用户 + - - - - 1. 发起授权请求 - - 将用户重定向到授权页面 - - - -
    -                            
    -                              {`const authUrl = 'https://connect.q58.club/oauth/authorize?' + 
    +                    
    + {/*
    */} + + + +
    + + 1 + +
    + 发起OAuth授权请求 + + 构建授权URL并重定向用户到授权页面 + +
    +
    +
    + + + + 请求地址 + + GET https://connect.q58.club/oauth/authorize + + + +
    +

    请求参数

    + + {`const authUrl = 'https://connect.q58.club/oauth/authorize?' + new URLSearchParams({ - response_type: 'code', // 必填,固定值 + response_type: 'code', // 必填,固定值为"code" client_id: 'your_client_id', // 必填,您的应用ID - redirect_uri: 'https://your-app.com/callback', - state: 'random_state', // 建议提供,防CSRF攻击 - scope: 'read_profile' // 可选,默认read_profile + redirect_uri: 'https://your-app.com/callback', // 必填,回调地址 + scope: 'read_profile' // 可选,权限范围,默认read_profile }); window.location.href = authUrl;`} -
    -
    -
    -
    -
    - - - - 2. 处理授权回调 - - 在回调地址处理授权结果 - - - -
    -                            
    -                              {`// 获取访问令牌
    -const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
    +                              
    +                            
    + + + + 授权响应 + +

    + 用户授权后,将重定向到您的回调地址: +

    +
    +                                  
    +                                    https://your-app.com/callback?code=ac_xxxxxx...
    +                                  
    +                                
    +
    +

    授权码说明:

    +
      +
    • 格式:ac_前缀 + 40位随机字符
    • +
    • 有效期:10分钟
    • +
    • 使用限制:仅可使用一次
    • +
    +
    +
    +
    + + + + + + + +
    + + 2 + +
    + 使用授权码交换访问令牌 + + 在服务器端使用授权码换取访问令牌 + +
    +
    +
    + + + + 请求地址 + + POST + https://connect.q58.club/api/oauth/access_token + + + +
    +

    请求示例

    + + {`const response = await fetch('https://connect.q58.club/api/oauth/access_token', { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + btoa(client_id + ':' + client_secret) }, body: new URLSearchParams({ - code: '授权码', // 回调参数中的code - redirect_uri: 'https://your-app.com/callback' + code: 'ac_xxxxx', // 必填,授权码 + 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"]
    +                            
    +                              
    +                              响应数据
    +                              
    +                                
    +                                  
    +                                    {`{
    +  "access_token": "at_xxxxxxxx",  // 访问令牌
    +  "token_type": "bearer",         // 令牌类型,固定为bearer
    +  "expires_in": 604800            // 令牌有效期,单位秒(7天)
     }`}
    -                            
    -                          
    - - - +
    +
    + + +
    +
    +
    + + + + +
    + + 3 + +
    + 获取用户信息 + + 使用访问令牌获取已授权用户的详细信息 + +
    +
    +
    + + + + 请求地址 + + GET https://connect.q58.club/api/oauth/user + + + +
    +

    请求示例

    + + {`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://...",// 头像URL
    +  "admin": false,            // 是否管理员
    +  "groups": ["group1"],      // 用户组
    +}`}
    +                                  
    +                                
    +
    +
    +
    +
    +
    + diff --git a/src/app/(admin)/admin/clients/[id]/page.tsx b/src/app/(admin)/admin/clients/[id]/page.tsx index 841b59a..bb22b72 100644 --- a/src/app/(admin)/admin/clients/[id]/page.tsx +++ b/src/app/(admin)/admin/clients/[id]/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { notFound, redirect } from "next/navigation"; -import type { ExtendedAccessToken, ExtendedClient } from "@/types"; +import type { ExtendedClient } from "@/types"; import { ArrowLeft } from "lucide-react"; import { getAuthorizationsByClientId } from "@/lib/dto/authorization"; diff --git a/src/app/(dashboard)/dashboard/clients/page.tsx b/src/app/(dashboard)/dashboard/clients/page.tsx index a66e670..90b2f73 100644 --- a/src/app/(dashboard)/dashboard/clients/page.tsx +++ b/src/app/(dashboard)/dashboard/clients/page.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; import { redirect } from "next/navigation"; -import type { Prisma } from "@prisma/client"; import { getClientsByUserId } from "@/lib/dto/client"; import { getCurrentUser } from "@/lib/session"; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..13219e7 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
    +)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/code-block.tsx b/src/components/ui/code-block.tsx new file mode 100644 index 0000000..e1f7724 --- /dev/null +++ b/src/components/ui/code-block.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + + const copy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + +export function CodeBlock({ children }: { children: string }) { + return ( +
    +
    +        {children}
    +      
    + +
    + ); +}