Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit bc91308

Browse files
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)

AltStyle によって変換されたページ (->オリジナル) /