Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bc91308

Browse files
committed
feat: Implement Gitee OAuth Authorization using Next.js and shadcn/ui
- add authentication with Gitee via Next.js, includes middleware for route protection. - implement UI components using Tailwind CSS, includes global styles, and various utility components.
1 parent c42d339 commit bc91308

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+6316
-5607
lines changed

app/api/auth/callback/gitee/route.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { type NextRequest, NextResponse } from "next/server"
2+
import { cookies } from "next/headers"
3+
4+
export async function GET(request: NextRequest) {
5+
try {
6+
// 从 URL 获取授权码
7+
const searchParams = request.nextUrl.searchParams
8+
const code = searchParams.get("code")
9+
10+
if (!code) {
11+
// 返回一个 HTML 页面,通知父窗口授权失败
12+
return new NextResponse(
13+
`
14+
<!DOCTYPE html>
15+
<html>
16+
<head>
17+
<title>授权失败</title>
18+
<meta charSet="utf-8" />
19+
<script>
20+
window.onload = function() {
21+
if (window.opener) {
22+
window.opener.postMessage({ type: "gitee-auth-error", error: "未收到授权码" }, "${request.nextUrl.origin}");
23+
setTimeout(function() { window.close(); }, 1000);
24+
} else {
25+
window.location.href = "${request.nextUrl.origin}/auth/error?error=no_code";
26+
}
27+
};
28+
</script>
29+
</head>
30+
<body>
31+
<h3>授权失败</h3>
32+
<p>未收到授权码,正在关闭窗口...</p>
33+
</body>
34+
</html>
35+
`,
36+
{
37+
headers: {
38+
"Content-Type": "text/html",
39+
},
40+
},
41+
)
42+
}
43+
44+
try {
45+
// 使用授权码获取访问令牌
46+
const tokenResponse = await fetch("https://gitee.com/oauth/token", {
47+
method: "POST",
48+
headers: {
49+
"Content-Type": "application/json",
50+
},
51+
body: JSON.stringify({
52+
grant_type: "authorization_code",
53+
code,
54+
client_id: process.env.NEXT_PUBLIC_GITEE_CLIENT_ID,
55+
client_secret: process.env.GITEE_CLIENT_SECRET,
56+
redirect_uri: process.env.NEXT_PUBLIC_GITEE_REDIRECT_URI,
57+
}),
58+
})
59+
60+
if (!tokenResponse.ok) {
61+
const error = await tokenResponse.text()
62+
console.error("获取访问令牌失败:", error)
63+
throw new Error("获取访问令牌失败")
64+
}
65+
66+
const tokenData = await tokenResponse.json()
67+
const accessToken = tokenData.access_token
68+
69+
// 使用访问令牌获取用户信息
70+
const userResponse = await fetch("https://gitee.com/api/v5/user", {
71+
headers: {
72+
Authorization: `token ${accessToken}`,
73+
},
74+
})
75+
76+
if (!userResponse.ok) {
77+
const error = await userResponse.text()
78+
console.error("获取用户信息失败:", error)
79+
throw new Error("获取用户信息失败")
80+
}
81+
82+
const userData = await userResponse.json()
83+
84+
// 创建会话 Cookie
85+
const sessionData = {
86+
user: {
87+
id: userData.id,
88+
name: userData.name,
89+
login: userData.login,
90+
avatar_url: userData.avatar_url,
91+
email: userData.email,
92+
},
93+
accessToken,
94+
expiresAt: Date.now() + tokenData.expires_in * 1000,
95+
}
96+
97+
// 设置会话 Cookie
98+
cookies().set({
99+
name: "session",
100+
value: JSON.stringify(sessionData),
101+
httpOnly: true,
102+
secure: process.env.NODE_ENV === "production",
103+
maxAge: tokenData.expires_in,
104+
path: "/",
105+
})
106+
107+
// 返回一个 HTML 页面,通知父窗口授权成功
108+
return new NextResponse(
109+
`
110+
<!DOCTYPE html>
111+
<html>
112+
<head>
113+
<meta charSet="utf-8" />
114+
<title>授权成功</title>
115+
<script>
116+
window.onload = function() {
117+
if (window.opener) {
118+
window.opener.postMessage({ type: "gitee-auth-success" }, "${request.nextUrl.origin}");
119+
setTimeout(function() { window.close(); }, 1000);
120+
} else {
121+
window.location.href = "${request.nextUrl.origin}/dashboard";
122+
}
123+
};
124+
</script>
125+
</head>
126+
<body>
127+
<h3>授权成功</h3>
128+
<p>您已成功授权,正在关闭窗口...</p>
129+
</body>
130+
</html>
131+
`,
132+
{
133+
headers: {
134+
"Content-Type": "text/html",
135+
},
136+
},
137+
)
138+
} catch (error) {
139+
console.error("OAuth 回调处理错误:", error)
140+
141+
// 返回一个 HTML 页面,通知父窗口授权失败
142+
return new NextResponse(
143+
`
144+
<!DOCTYPE html>
145+
<html>
146+
<head>
147+
<meta charSet="utf-8" />
148+
<title>授权失败</title>
149+
<script>
150+
window.onload = function() {
151+
if (window.opener) {
152+
window.opener.postMessage({
153+
type: "gitee-auth-error",
154+
error: "${error instanceof Error ? error.message : "服务器处理错误"}"
155+
}, "${request.nextUrl.origin}");
156+
setTimeout(function() { window.close(); }, 1000);
157+
} else {
158+
window.location.href = "${request.nextUrl.origin}/auth/error?error=server_error";
159+
}
160+
};
161+
</script>
162+
</head>
163+
<body>
164+
<h3>授权失败</h3>
165+
<p>${error instanceof Error ? error.message : "服务器处理错误"},正在关闭窗口...</p>
166+
</body>
167+
</html>
168+
`,
169+
{
170+
headers: {
171+
"Content-Type": "text/html",
172+
},
173+
},
174+
)
175+
}
176+
} catch (error) {
177+
console.error("OAuth 回调处理错误:", error)
178+
return NextResponse.redirect(new URL("/auth/error?error=server_error", request.url))
179+
}
180+
}

app/api/auth/login/gitee/route.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type NextRequest, NextResponse } from "next/server"
2+
3+
export async function GET(request: NextRequest) {
4+
try {
5+
// 确保环境变量存在,否则使用硬编码的值(仅用于开发)
6+
const clientId = process.env.GITEE_CLIENT_ID || "5e96ca868817fa1b190d14e20ffd7b19f03f2c9aa6c064dda3bfe9e715ee8dd2"
7+
const redirectUri = encodeURIComponent(
8+
process.env.GITEE_REDIRECT_URI || "http://localhost:3000/api/auth/callback/gitee",
9+
)
10+
11+
console.log("使用的 Client ID:", clientId)
12+
console.log("使用的 Redirect URI:", redirectUri)
13+
14+
// 构建 Gitee 授权 URL
15+
const authUrl = `https://gitee.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`
16+
17+
console.log("重定向到:", authUrl)
18+
19+
// 重定向到 Gitee 授权页面
20+
return NextResponse.redirect(authUrl)
21+
} catch (error) {
22+
console.error("Gitee 登录路由错误:", error)
23+
24+
// 返回错误响应而不是重定向
25+
return new NextResponse(
26+
JSON.stringify({ error: "登录处理失败", details: error instanceof Error ? error.message : "未知错误" }),
27+
{
28+
status: 500,
29+
headers: {
30+
"Content-Type": "application/json",
31+
},
32+
},
33+
)
34+
}
35+
}

app/api/auth/logout/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { type NextRequest, NextResponse } from "next/server"
2+
import { cookies } from "next/headers"
3+
4+
export async function GET(request: NextRequest) {
5+
// 清除会话 Cookie
6+
cookies().delete("session")
7+
8+
// 重定向到首页
9+
return NextResponse.redirect(new URL("/", request.url))
10+
}

app/api/test-env/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NextResponse } from "next/server"
2+
3+
export async function GET() {
4+
return NextResponse.json({
5+
clientId: process.env.GITEE_CLIENT_ID || "未设置",
6+
redirectUri: process.env.GITEE_REDIRECT_URI || "未设置",
7+
// 不要在生产环境中返回 client secret
8+
hasClientSecret: !!process.env.GITEE_CLIENT_SECRET,
9+
})
10+
}

app/auth/callback/page.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5+
6+
export default function AuthCallback() {
7+
const [status, setStatus] = useState<"loading" | "success" | "error">("loading")
8+
const [error, setError] = useState<string>("")
9+
10+
useEffect(() => {
11+
async function handleCallback() {
12+
try {
13+
// 从 URL 获取授权码
14+
const urlParams = new URLSearchParams(window.location.search)
15+
const code = urlParams.get("code")
16+
17+
if (!code) {
18+
setStatus("error")
19+
setError("未收到授权码")
20+
// 通知父窗口授权失败
21+
if (window.opener) {
22+
window.opener.postMessage(
23+
{
24+
type: "gitee-auth-error",
25+
error: "未收到授权码",
26+
},
27+
window.location.origin,
28+
)
29+
}
30+
return
31+
}
32+
33+
// 发送授权码到服务器
34+
const response = await fetch("/api/auth/callback/gitee", {
35+
method: "POST",
36+
headers: {
37+
"Content-Type": "application/json",
38+
},
39+
body: JSON.stringify({ code }),
40+
})
41+
42+
if (!response.ok) {
43+
const errorData = await response.json()
44+
throw new Error(errorData.error || "服务器处理错误")
45+
}
46+
47+
setStatus("success")
48+
49+
// 通知父窗口授权成功
50+
if (window.opener) {
51+
window.opener.postMessage(
52+
{
53+
type: "gitee-auth-success",
54+
},
55+
window.location.origin,
56+
)
57+
58+
// 关闭弹出窗口
59+
setTimeout(() => window.close(), 1000)
60+
}
61+
} catch (error) {
62+
console.error("处理回调错误:", error)
63+
setStatus("error")
64+
setError(error instanceof Error ? error.message : "未知错误")
65+
66+
// 通知父窗口授权失败
67+
if (window.opener) {
68+
window.opener.postMessage(
69+
{
70+
type: "gitee-auth-error",
71+
error: error instanceof Error ? error.message : "未知错误",
72+
},
73+
window.location.origin,
74+
)
75+
}
76+
}
77+
}
78+
79+
handleCallback()
80+
}, [])
81+
82+
return (
83+
<div className="flex h-screen items-center justify-center">
84+
<Card className="w-[350px]">
85+
<CardHeader>
86+
<CardTitle>
87+
{status === "loading" && "处理授权中..."}
88+
{status === "success" && "授权成功"}
89+
{status === "error" && "授权失败"}
90+
</CardTitle>
91+
<CardDescription>
92+
{status === "loading" && "正在处理 Gitee 授权,请稍候..."}
93+
{status === "success" && "您已成功授权,即将返回应用"}
94+
{status === "error" && error}
95+
</CardDescription>
96+
</CardHeader>
97+
<CardContent>
98+
{status === "success" && (
99+
<p className="text-sm text-muted-foreground">如果页面没有自动关闭,请手动关闭此窗口</p>
100+
)}
101+
{status === "error" && <p className="text-sm text-muted-foreground">请关闭此窗口并重试</p>}
102+
</CardContent>
103+
</Card>
104+
</div>
105+
)
106+
}

app/auth/error/page.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Button } from "@/components/ui/button"
2+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
3+
import Link from "next/link"
4+
5+
export default function AuthError({
6+
searchParams,
7+
}: {
8+
searchParams: { error?: string }
9+
}) {
10+
const errorMessages: Record<string, string> = {
11+
no_code: "未收到授权码",
12+
token_error: "获取访问令牌失败",
13+
user_error: "获取用户信息失败",
14+
server_error: "服务器处理错误",
15+
default: "登录过程中发生错误",
16+
}
17+
18+
const errorMessage = errorMessages[searchParams.error || "default"]
19+
20+
return (
21+
<div className="container flex h-screen items-center justify-center">
22+
<Card className="w-full max-w-md">
23+
<CardHeader>
24+
<CardTitle className="text-2xl">登录失败</CardTitle>
25+
<CardDescription>在尝试使用 Gitee 账号登录时发生错误</CardDescription>
26+
</CardHeader>
27+
<CardContent>
28+
<p className="text-destructive">{errorMessage}</p>
29+
</CardContent>
30+
<CardFooter>
31+
<Button asChild className="w-full">
32+
<Link href="/">返回首页</Link>
33+
</Button>
34+
</CardFooter>
35+
</Card>
36+
</div>
37+
)
38+
}

0 commit comments

Comments
 (0)