diff --git a/app/api/oauth/authorize/route.ts b/app/api/oauth/authorize/route.ts new file mode 100644 index 0000000..fa692c1 --- /dev/null +++ b/app/api/oauth/authorize/route.ts @@ -0,0 +1,19 @@ +import { redirect } from 'next/navigation'; + +export async function GET() { + const CLIENT_ID = process.env.LINUX_DO_CLIENT_ID; + const REDIRECT_URI = `${process.env.NEXT_PUBLIC_APP_URL}/api/oauth/callback`; + + if (!CLIENT_ID || !process.env.NEXT_PUBLIC_APP_URL) { + return new Response('Missing configuration', { status: 500 }); + } + + const params = new URLSearchParams({ + client_id: CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: 'code', + scope: 'user', // As per user guide + }); + + redirect(`https://connect.linux.do/oauth2/authorize?${params.toString()}`); +} diff --git a/app/api/oauth/callback/route.ts b/app/api/oauth/callback/route.ts new file mode 100644 index 0000000..b030faf --- /dev/null +++ b/app/api/oauth/callback/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const code = searchParams.get('code'); + + if (!code) { + return NextResponse.redirect(new URL('/settings?error=no_code', request.url)); + } + + const CLIENT_ID = process.env.LINUX_DO_CLIENT_ID; + const CLIENT_SECRET = process.env.LINUX_DO_CLIENT_SECRET; + const REDIRECT_URI = `${process.env.NEXT_PUBLIC_APP_URL}/api/oauth/callback`; + + if (!CLIENT_ID || !CLIENT_SECRET || !process.env.NEXT_PUBLIC_APP_URL) { + return NextResponse.redirect(new URL('/settings?error=config_missing', request.url)); + } + + try { + // 1. Exchange code for token + const tokenResponse = await fetch('https://connect.linux.do/oauth2/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + code, + redirect_uri: REDIRECT_URI, + grant_type: 'authorization_code', + }), + }); + + const tokenData = await tokenResponse.json(); + + if (!tokenData.access_token) { + throw new Error('Failed to get access token'); + } + + // 2. Get User Info + const userResponse = await fetch('https://connect.linux.do/api/user', { + headers: { Authorization: `Bearer ${tokenData.access_token}` }, + }); + + const userData = await userResponse.json(); + + // 3. Redirect with data + const redirectUrl = new URL('/settings', request.url); + redirectUrl.searchParams.set('auth_success', 'true'); + redirectUrl.searchParams.set('token', tokenData.access_token); + redirectUrl.searchParams.set('user', JSON.stringify(userData)); + + return NextResponse.redirect(redirectUrl); + } catch (error) { + console.error('OAuth Error:', error); + return NextResponse.redirect(new URL('/settings?error=auth_failed', request.url)); + } +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 18fc909..e94d291 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -8,6 +8,7 @@ import { SourceSettings } from '@/components/settings/SourceSettings'; import { SortSettings } from '@/components/settings/SortSettings'; import { DataSettings } from '@/components/settings/DataSettings'; import { SettingsHeader } from '@/components/settings/SettingsHeader'; +import { AccountSettings } from '@/components/settings/AccountSettings'; import { useSettingsPage } from './hooks/useSettingsPage'; export default function SettingsPage() { @@ -38,10 +39,13 @@ export default function SettingsPage() { return (
-
+
{/* Header */} + {/* Account Settings */} + + {/* Source Management */} { + const authSuccess = searchParams.get('auth_success'); + const token = searchParams.get('token'); + const userStr = searchParams.get('user'); + + if (authSuccess && token && userStr) { + try { + const userData = JSON.parse(userStr) as LinuxDoUser; + // Clear existing data before logging in new user + clearHistory(); + clearSearchHistory(); + + login(userData, token); + + // Clean URL + router.replace('/settings'); + } catch (e) { + console.error('Failed to parse user data', e); + } + } + }, [searchParams, login, clearHistory, clearSearchHistory, router]); + + const handleLogin = () => { + // Clear data before redirecting to ensure clean state + clearHistory(); + clearSearchHistory(); + router.push('/api/oauth/authorize'); + }; + + const handleLogout = () => { + logout(); + clearHistory(); + clearSearchHistory(); + }; + + return ( +
+
+ + + + +

账号

+
+ +
+ {!isAuthenticated ? ( +
+

登录 Linux DO 以同步您的账号信息

+ +
+ ) : ( +
+
+ {user?.name +
+

{user?.name || user?.username}

+

@{user?.username}

+
+ + Trust Level {user?.trust_level} + + {user?.active && ( + + Active + + )} +
+
+
+ +
+ +
+
+ )} +
+
+ ); +} diff --git a/components/settings/SettingsHeader.tsx b/components/settings/SettingsHeader.tsx index 03281e6..de72f2b 100644 --- a/components/settings/SettingsHeader.tsx +++ b/components/settings/SettingsHeader.tsx @@ -4,7 +4,7 @@ export function SettingsHeader() { const router = useRouter(); return ( -
+