|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import React, { useState } from "react"; |
| 3 | +import React, { useEffect, useState } from "react"; |
4 | 4 | import type { JSX } from "react"; |
| 5 | +import { useRouter } from "next/navigation"; |
5 | 6 | import { Navigation } from "@/components/navigation"; |
6 | 7 | import { Footer } from "@/components/footer"; |
7 | 8 | import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; |
8 | 9 | import { Badge } from "@/components/ui/badge"; |
9 | 10 | import { Button } from "@/components/ui/button"; |
10 | | -import { Github, Lock, UserCheck, Shield, ArrowRight, Sparkles } from "lucide-react"; |
| 11 | +import { Github, UserCheck, ArrowRight } from "lucide-react"; |
| 12 | + |
| 13 | +type UserInfo = { |
| 14 | + provider: string; |
| 15 | + login: string; |
| 16 | + id: number; |
| 17 | + name: string | null; |
| 18 | + avatar_url?: string | null; |
| 19 | + email?: string | null; |
| 20 | +}; |
| 21 | + |
| 22 | +export default function LoginSuccessPage(): JSX.Element { |
| 23 | + const [loading, setLoading] = useState(true); |
| 24 | + const [error, setError] = useState<string | null>(null); |
| 25 | + const [user, setUser] = useState<UserInfo | null>(null); |
| 26 | + const router = useRouter(); |
| 27 | + |
| 28 | + useEffect(() => { |
| 29 | + // Parse query params |
| 30 | + const params = new URLSearchParams(window.location.search); |
| 31 | + const code = params.get("code"); |
| 32 | + const state = params.get("state"); |
| 33 | + |
| 34 | + if (!code) { |
| 35 | + setError("缺少 code 参数,无法完成登录。"); |
| 36 | + setLoading(false); |
| 37 | + return; |
| 38 | + } |
| 39 | + |
| 40 | + // POST to our server-side exchange endpoint |
| 41 | + (async () => { |
| 42 | + try { |
| 43 | + setLoading(true); |
| 44 | + setError(null); |
| 45 | + |
| 46 | + const resp = await fetch("/api/auth/github", { |
| 47 | + method: "POST", |
| 48 | + headers: { "Content-Type": "application/json" }, |
| 49 | + body: JSON.stringify({ code, state }), |
| 50 | + }); |
| 51 | + |
| 52 | + const data = await resp.json(); |
| 53 | + |
| 54 | + if (!resp.ok || data.error) { |
| 55 | + setError(data.error || `后端返回错误: ${resp.status}`); |
| 56 | + setLoading(false); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + setUser(data.user || null); |
| 61 | + // 持久化到 localStorage,供导航栏读取 |
| 62 | + try { |
| 63 | + if (data?.user) { |
| 64 | + localStorage.setItem("ep_user", JSON.stringify(data.user)); |
| 65 | + } |
| 66 | + } catch (e) { |
| 67 | + // ignore |
| 68 | + } |
| 69 | + setLoading(false); |
| 70 | + |
| 71 | + // 在登录成功后导航到用户页或主页 |
| 72 | + router.push('/users/home'); |
| 73 | + } catch (err: any) { |
| 74 | + setError(err?.message || String(err)); |
| 75 | + setLoading(false); |
| 76 | + } |
| 77 | + })(); |
| 78 | + }, [router]); |
11 | 79 |
|
12 | | -export default function LoginPage(): JSX.Element { |
13 | 80 | return ( |
14 | 81 | <> |
15 | 82 | <Navigation /> |
16 | 83 | <main className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-cyan-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12"> |
17 | 84 | <div className="container mx-auto px-4"> |
18 | | - <h1 className="text-4xl font-bold text-center text-gray-900 dark:text-white"> |
19 | | - 正在开发中... |
20 | | - </h1> |
21 | | - <p className="text-center text-gray-600 dark:text-gray-400"> |
22 | | - 我们正在努力开发新功能,敬请期待! |
23 | | - </p> |
| 85 | + <div className="max-w-xl mx-auto"> |
| 86 | + <Card> |
| 87 | + <CardHeader> |
| 88 | + <CardTitle className="flex items-center gap-2"> |
| 89 | + <Github className="w-6 h-6" /> |
| 90 | + GitHub 登录回调 |
| 91 | + </CardTitle> |
| 92 | + <CardDescription> |
| 93 | + 处理 GitHub 返回的授权 code,并使用服务器端密钥完成 token 交换。 |
| 94 | + </CardDescription> |
| 95 | + </CardHeader> |
| 96 | + <CardContent> |
| 97 | + {loading && ( |
| 98 | + <div className="space-y-2"> |
| 99 | + <p>正在完成登录……</p> |
| 100 | + <p className="text-sm text-gray-500">请稍候,页面会在处理完成后显示结果。</p> |
| 101 | + </div> |
| 102 | + )} |
| 103 | + |
| 104 | + {!loading && error && ( |
| 105 | + <div className="space-y-2"> |
| 106 | + <p className="text-red-600">发生错误:{error}</p> |
| 107 | + <div className="flex gap-2"> |
| 108 | + <Button onClick={() => window.location.assign('/users/login')}>返回登录</Button> |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + )} |
| 112 | + |
| 113 | + {!loading && user && ( |
| 114 | + <div className="space-y-4"> |
| 115 | + <div className="flex items-center gap-4"> |
| 116 | + {user.avatar_url ? ( |
| 117 | + // eslint-disable-next-line @next/next/no-img-element |
| 118 | + <img src={user.avatar_url} alt="avatar" className="w-16 h-16 rounded-full" /> |
| 119 | + ) : ( |
| 120 | + <div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center"> |
| 121 | + <UserCheck /> |
| 122 | + </div> |
| 123 | + )} |
| 124 | + |
| 125 | + <div> |
| 126 | + <div className="text-lg font-medium">{user.name || user.login}</div> |
| 127 | + <div className="text-sm text-gray-500">{user.email || '未公开邮箱'}</div> |
| 128 | + </div> |
| 129 | + </div> |
| 130 | + |
| 131 | + <div className="flex gap-2"> |
| 132 | + <Badge>provider: {user.provider}</Badge> |
| 133 | + <Badge>login: {user.login}</Badge> |
| 134 | + </div> |
| 135 | + |
| 136 | + <div className="flex gap-2"> |
| 137 | + <Button onClick={() => router.push('/')}> |
| 138 | + <ArrowRight className="w-4 h-4 mr-2" /> 返回首页 |
| 139 | + </Button> |
| 140 | + <Button variant="ghost" onClick={() => window.location.assign('/users/login')}> |
| 141 | + 返回登录页 |
| 142 | + </Button> |
| 143 | + </div> |
| 144 | + </div> |
| 145 | + )} |
| 146 | + </CardContent> |
| 147 | + </Card> |
| 148 | + </div> |
24 | 149 | </div> |
25 | 150 | </main> |
26 | 151 | <Footer /> |
|
0 commit comments