feat: 添加代码块组件和警告组件,优化用户界面

This commit is contained in:
wood chen 2025-02-22 15:15:30 +08:00
parent ce3baad450
commit 54b3d8e661
7 changed files with 390 additions and 144 deletions

View File

@ -63,55 +63,7 @@ Q58论坛网址: https://q58.club
## 用户应用接入本系统oauth2.0认证的方式: ## 用户应用接入本系统oauth2.0认证的方式:
1. 发起授权请求 写在"src\app\()\page.tsx"里了.
将用户重定向到授权页面
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"]
}
## 添加新功能的准则 ## 添加新功能的准则

View File

@ -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"
}

View File

@ -1,6 +1,15 @@
import Link from "next/link"; 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 { Button } from "@/components/ui/button";
import { import {
Card, Card,
@ -9,15 +18,10 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { CodeBlock } from "@/components/ui/code-block";
import { Container } from "@/components/ui/container"; import { Container } from "@/components/ui/container";
import { Section } from "@/components/ui/section"; import { Section } from "@/components/ui/section";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
TypographyH1,
TypographyH2,
TypographyLead,
TypographyMuted,
} from "@/components/ui/typography";
import { Header } from "@/components/layout/header"; import { Header } from "@/components/layout/header";
export default function HomePage() { export default function HomePage() {
@ -75,11 +79,11 @@ export default function HomePage() {
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span></span> <span></span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span>SDK</span> <span></span>
</li> </li>
</ul> </ul>
</CardContent> </CardContent>
@ -105,7 +109,7 @@ export default function HomePage() {
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span>CSRF攻击</span> <span>Serverless部署 SLA</span>
</li> </li>
</ul> </ul>
</CardContent> </CardContent>
@ -116,22 +120,22 @@ export default function HomePage() {
<Users className="h-10 w-10 text-primary" /> <Users className="h-10 w-10 text-primary" />
<CardTitle className="text-xl"></CardTitle> <CardTitle className="text-xl"></CardTitle>
<CardDescription className="text-base"> <CardDescription className="text-base">
, , API用户信息和权限管理功
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="space-y-4 text-sm"> <ul className="space-y-4 text-sm">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span></span> <span>API </span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span></span> <span></span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-5 w-5 text-green-500" />
<span></span> <span> </span>
</li> </li>
</ul> </ul>
</CardContent> </CardContent>
@ -146,104 +150,223 @@ export default function HomePage() {
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<Card className="overflow-hidden border-2 p-6"> <Card className="overflow-hidden border-2 p-6">
<CardHeader className="bg-white dark:bg-gray-900"> <CardHeader className="bg-white dark:bg-gray-900">
<CardTitle className="text-2xl"></CardTitle> <CardTitle className="text-2xl">
()
</CardTitle>
<CardDescription className="text-base"> <CardDescription className="text-base">
Q58 Connect 使Authorization
CodeOAuth 2.0
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="bg-white p-0 dark:bg-gray-900"> <CardContent className="bg-white p-0 dark:bg-gray-900">
<Tabs defaultValue="auth" className="w-full"> <Tabs defaultValue="auth" className="w-full">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="auth"></TabsTrigger> <TabsTrigger value="auth">
<TabsTrigger value="callback"></TabsTrigger> <span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
<TabsTrigger value="userinfo"></TabsTrigger> 1
</span>
</TabsTrigger>
<TabsTrigger value="callback">
<span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
2
</span>
</TabsTrigger>
<TabsTrigger value="userinfo">
<span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
3
</span>
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="auth" className="mt-4"> <div className="relative mt-6">
<Card> {/* <div className="absolute left-[3rem] top-0 h-full w-px bg-border" /> */}
<CardHeader> <TabsContent value="auth" className="mt-4 space-y-4">
<CardTitle>1. </CardTitle> <Card className="border-2 shadow-sm transition-all hover:border-primary/50">
<CardDescription> <CardHeader>
<div className="flex items-center gap-4">
</CardDescription> <span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
</CardHeader> 1
<CardContent> </span>
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800"> <div>
<code className="text-sm"> <CardTitle>OAuth授权请求</CardTitle>
{`const authUrl = 'https://connect.q58.club/oauth/authorize?' + <CardDescription>
URL并重定向用户到授权页面
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-l-4 border-l-primary">
<Code2 className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription className="font-mono">
GET https://connect.q58.club/oauth/authorize
</AlertDescription>
</Alert>
<div>
<h4 className="mb-2 font-medium"></h4>
<CodeBlock>
{`const authUrl = 'https://connect.q58.club/oauth/authorize?' +
new URLSearchParams({ new URLSearchParams({
response_type: 'code', // 必填,固定值 response_type: 'code', // 必填,固定值为"code"
client_id: 'your_client_id', // 必填您的应用ID client_id: 'your_client_id', // 必填您的应用ID
redirect_uri: 'https://your-app.com/callback', redirect_uri: 'https://your-app.com/callback', // 必填,回调地址
state: 'random_state', // 建议提供防CSRF攻击 scope: 'read_profile' // 可选权限范围默认read_profile
scope: 'read_profile' // 可选默认read_profile
}); });
window.location.href = authUrl;`} window.location.href = authUrl;`}
</code> </CodeBlock>
</pre> </div>
</CardContent>
</Card> <Alert className="border-green-500">
</TabsContent> <ArrowDownToLine className="h-4 w-4" />
<TabsContent value="callback" className="mt-4"> <AlertTitle></AlertTitle>
<Card> <AlertDescription>
<CardHeader> <p className="mb-2">
<CardTitle>2. </CardTitle>
<CardDescription> </p>
<pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
</CardDescription> <code className="text-sm">
</CardHeader> https://your-app.com/callback?code=ac_xxxxxx...
<CardContent> </code>
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800"> </pre>
<code className="text-sm"> <div className="mt-4 space-y-2">
{`// 获取访问令牌 <p className="font-medium"></p>
const response = await fetch('https://connect.q58.club/api/oauth/access_token', { <ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
<li>ac_前缀 + 40</li>
<li>10</li>
<li>使使</li>
</ul>
</div>
</AlertDescription>
</Alert>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="callback" className="mt-4 space-y-4">
<Card className="border-2 shadow-sm transition-all hover:border-primary/50">
<CardHeader>
<div className="flex items-center gap-4">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
2
</span>
<div>
<CardTitle>使访</CardTitle>
<CardDescription>
使访
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-l-4 border-l-primary">
<Code2 className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription className="font-mono">
POST
https://connect.q58.club/api/oauth/access_token
</AlertDescription>
</Alert>
<div>
<h4 className="mb-2 font-medium"></h4>
<CodeBlock>
{`const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
method: 'POST', method: 'POST',
headers: { 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({ body: new URLSearchParams({
code: '授权码', // 回调参数中的code code: 'ac_xxxxx', // 必填,授权码
redirect_uri: 'https://your-app.com/callback' redirect_uri: 'https://your-app.com/callback' // 必填,与请求授权码时相同
}) })
}); });`}
</CodeBlock>
</div>
const { access_token, expires_in } = await response.json();`} <Alert className="border-green-500">
</code> <ArrowDownToLine className="h-4 w-4" />
</pre> <AlertTitle></AlertTitle>
</CardContent> <AlertDescription>
</Card> <pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
</TabsContent> <code className="text-sm">
<TabsContent value="userinfo" className="mt-4"> {`{
<Card> "access_token": "at_xxxxxxxx", // 访问令牌
<CardHeader> "token_type": "bearer", // 令牌类型固定为bearer
<CardTitle>3. </CardTitle> "expires_in": 604800 // 令牌有效期单位秒7天
<CardDescription>
使访
</CardDescription>
</CardHeader>
<CardContent>
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
<code className="text-sm">
{`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"]
}`} }`}
</code> </code>
</pre> </pre>
</CardContent> </AlertDescription>
</Card> </Alert>
</TabsContent> </CardContent>
</Card>
</TabsContent>
<TabsContent value="userinfo" className="mt-4 space-y-4">
<Card className="border-2 shadow-sm transition-all hover:border-primary/50">
<CardHeader>
<div className="flex items-center gap-4">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
3
</span>
<div>
<CardTitle></CardTitle>
<CardDescription>
使访
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-l-4 border-l-primary">
<Code2 className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription className="font-mono">
GET https://connect.q58.club/api/oauth/user
</AlertDescription>
</Alert>
<div>
<h4 className="mb-2 font-medium"></h4>
<CodeBlock>
{`const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
headers: {
'Authorization': \`Bearer \${access_token}\` // 使用获取到的访问令牌
}
}).then(res => res.json());`}
</CodeBlock>
</div>
<Alert className="border-green-500">
<ArrowDownToLine className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
<pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
<code className="text-sm">
{`{
"id": "user_xxx", // 用户唯一标识
"email": "user@example.com",// 邮箱地址
"username": "username", // 用户名
"name": "用户昵称", // 显示名称
"avatar_url": "https://...",// 头像URL
"admin": false, // 是否管理员
"groups": ["group1"], // 用户组
}`}
</code>
</pre>
</AlertDescription>
</Alert>
</CardContent>
</Card>
</TabsContent>
</div>
</Tabs> </Tabs>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import type { ExtendedAccessToken, ExtendedClient } from "@/types"; import type { ExtendedClient } from "@/types";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { getAuthorizationsByClientId } from "@/lib/dto/authorization"; import { getAuthorizationsByClientId } from "@/lib/dto/authorization";

View File

@ -1,6 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { Prisma } from "@prisma/client";
import { getClientsByUserId } from "@/lib/dto/client"; import { getClientsByUserId } from "@/lib/dto/client";
import { getCurrentUser } from "@/lib/session"; import { getCurrentUser } from "@/lib/session";

View File

@ -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<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@ -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 (
<button
onClick={copy}
className="absolute right-2 top-2 rounded-md p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-gray-500" />
)}
</button>
);
}
export function CodeBlock({ children }: { children: string }) {
return (
<div className="relative">
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
<code className="text-sm">{children}</code>
</pre>
<CopyButton text={children} />
</div>
);
}