|
import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node"; |
|
import { json } from "@remix-run/node"; |
|
import { useLoaderData, Link, useSearchParams } from "@remix-run/react"; |
|
import { getUserSession } from "~/lib/session.server"; |
|
import { accountLinkingService } from "~/lib/account-linking.server"; |
|
|
|
export const meta: MetaFunction = () => { |
|
return [ |
|
{ title: "GitHub + HuggingFace Account Linking" }, |
|
{ name: "description", content: "Link your GitHub and HuggingFace accounts" }, |
|
]; |
|
}; |
|
|
|
export async function loader({ request }: LoaderFunctionArgs) { |
|
const userSession = await getUserSession(request); |
|
const url = new URL(request.url); |
|
const error = url.searchParams.get("error"); |
|
const details = url.searchParams.get("details"); |
|
const message = url.searchParams.get("message"); |
|
|
|
const cookies = request.headers.get("Cookie") || ""; |
|
console.log("Cookie keys available:", cookies.split(";").map(c => c.trim().split("=")[0])); |
|
|
|
|
|
let linkingStats = null; |
|
if (userSession) { |
|
linkingStats = accountLinkingService.getStats(); |
|
} |
|
|
|
return json({ |
|
userSession, |
|
error, |
|
details, |
|
message, |
|
linkingStats |
|
}); |
|
} |
|
|
|
export default function Index() { |
|
const { userSession, error, details, message, linkingStats } = useLoaderData<typeof loader>(); |
|
const [searchParams] = useSearchParams(); |
|
|
|
|
|
const hasGitHub = !!userSession?.github; |
|
const hasHuggingFace = !!userSession?.huggingface; |
|
const isLinked = userSession?.isLinked || false; |
|
|
|
return ( |
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> |
|
<div className="flex h-screen items-center justify-center"> |
|
<div className="flex flex-col items-center gap-8 max-w-2xl mx-auto px-4"> |
|
<header className="flex flex-col items-center gap-6"> |
|
<div className="flex space-x-4"> |
|
<div className="bg-white rounded-full p-4 shadow-lg"> |
|
<svg |
|
className="w-16 h-16 text-blue-600" |
|
fill="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> |
|
</svg> |
|
</div> |
|
<div className="bg-white rounded-full p-4 shadow-lg"> |
|
<svg |
|
className="w-16 h-16 text-yellow-500" |
|
viewBox="0 0 24 24" |
|
fill="currentColor" |
|
> |
|
<path d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm0 2.25c2.387 0 4.5 1.773 4.5 4.5S14.387 13.5 12 13.5 7.5 11.727 7.5 9s2.113-4.5 4.5-4.5zm0 16.5a9.26 9.26 0 01-6.75-2.798c.042-2.233 4.5-3.452 6.75-3.452s6.708 1.219 6.75 3.452A9.26 9.26 0 0112 21z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
<h1 className="text-4xl font-bold text-gray-900"> |
|
Account Linking |
|
</h1> |
|
<p className="text-lg text-gray-600 text-center"> |
|
Link your GitHub and HuggingFace accounts in one place |
|
</p> |
|
</header> |
|
|
|
{/* Error Messages */} |
|
{error && ( |
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 w-full"> |
|
<div className="flex"> |
|
<div className="flex-shrink-0"> |
|
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> |
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> |
|
</svg> |
|
</div> |
|
<div className="ml-3"> |
|
<h3 className="text-sm font-medium text-red-800"> |
|
Authentication Error |
|
</h3> |
|
<div className="mt-2 text-sm text-red-700"> |
|
{error === "oauth_failed" && ( |
|
<div> |
|
<p>OAuth authentication failed.</p> |
|
{details && <p className="mt-1 font-mono text-xs">Details: {details}</p>} |
|
</div> |
|
)} |
|
{error === "no_code" && "No authorization code received."} |
|
{error === "callback_failed" && ( |
|
<div> |
|
<p>Failed to complete authentication.</p> |
|
{message && <p className="mt-1 font-mono text-xs">Error: {message}</p>} |
|
</div> |
|
)} |
|
{error === "hf_oauth_not_configured" && ( |
|
<div> |
|
<p>HuggingFace OAuth is not properly configured.</p> |
|
<p className="mt-1">Please set up the required environment variables.</p> |
|
</div> |
|
)} |
|
{error === "hf_oauth_failed" && ( |
|
<div> |
|
<p>HuggingFace authentication failed.</p> |
|
{details && <p className="mt-1 font-mono text-xs">Details: {details}</p>} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* Main Account Linking Interface */} |
|
<div className="bg-white rounded-lg shadow-lg p-8 w-full"> |
|
<h2 className="text-2xl font-bold text-gray-900 mb-6 text-center"> |
|
Account Status |
|
</h2> |
|
|
|
<div className="grid grid-cols-2 gap-8 mb-8"> |
|
{/* GitHub Authentication Status */} |
|
<div className="border rounded-lg p-6 flex flex-col items-center"> |
|
<div className={`p-3 rounded-full ${hasGitHub ? 'bg-green-100' : 'bg-gray-100'} mb-4`}> |
|
<svg |
|
className={`w-8 h-8 ${hasGitHub ? 'text-green-600' : 'text-gray-400'}`} |
|
fill="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> |
|
</svg> |
|
</div> |
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GitHub</h3> |
|
|
|
{hasGitHub ? ( |
|
<div className="text-center"> |
|
<div className="flex items-center justify-center mb-2"> |
|
{userSession?.github?.avatar_url && ( |
|
<img |
|
src={userSession.github.avatar_url} |
|
alt={userSession.github.login} |
|
className="w-12 h-12 rounded-full mr-3" |
|
/> |
|
)} |
|
<div> |
|
<p className="font-medium text-gray-900">{userSession?.github?.name || userSession?.github?.login}</p> |
|
<p className="text-sm text-gray-600">@{userSession?.github?.login}</p> |
|
</div> |
|
</div> |
|
<div className="mt-4"> |
|
<Link |
|
to="/auth/logout?service=github" |
|
className="text-sm text-red-600 hover:text-red-800" |
|
> |
|
Disconnect |
|
</Link> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="text-center"> |
|
<p className="text-gray-500 mb-4">Not connected</p> |
|
<Link |
|
to="/auth/github" |
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-900 hover:bg-gray-800 focus:outline-none" |
|
> |
|
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24"> |
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> |
|
</svg> |
|
Connect GitHub |
|
</Link> |
|
</div> |
|
)} |
|
</div> |
|
|
|
{/* HuggingFace Authentication Status */} |
|
<div className="border rounded-lg p-6 flex flex-col items-center"> |
|
<div className={`p-3 rounded-full ${hasHuggingFace ? 'bg-green-100' : 'bg-gray-100'} mb-4`}> |
|
<svg |
|
className={`w-8 h-8 ${hasHuggingFace ? 'text-green-600' : 'text-gray-400'}`} |
|
viewBox="0 0 24 24" |
|
fill="currentColor" |
|
> |
|
<path d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm0 2.25c2.387 0 4.5 1.773 4.5 4.5S14.387 13.5 12 13.5 7.5 11.727 7.5 9s2.113-4.5 4.5-4.5zm0 16.5a9.26 9.26 0 01-6.75-2.798c.042-2.233 4.5-3.452 6.75-3.452s6.708 1.219 6.75 3.452A9.26 9.26 0 0112 21z" /> |
|
</svg> |
|
</div> |
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">HuggingFace</h3> |
|
|
|
{hasHuggingFace ? ( |
|
<div className="text-center"> |
|
<div className="flex items-center justify-center mb-2"> |
|
{userSession?.huggingface?.avatarUrl && ( |
|
<img |
|
src={userSession.huggingface.avatarUrl} |
|
alt={userSession.huggingface.username} |
|
className="w-12 h-12 rounded-full mr-3" |
|
/> |
|
)} |
|
<div> |
|
<p className="font-medium text-gray-900">{userSession?.huggingface?.fullName || userSession?.huggingface?.username}</p> |
|
<p className="text-sm text-gray-600">@{userSession?.huggingface?.username}</p> |
|
</div> |
|
</div> |
|
<div className="mt-4"> |
|
<Link |
|
to="/auth/logout?service=huggingface" |
|
className="text-sm text-red-600 hover:text-red-800" |
|
> |
|
Disconnect |
|
</Link> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="text-center"> |
|
<p className="text-gray-500 mb-4">Not connected</p> |
|
<Link |
|
to="/auth/huggingface" |
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none" |
|
> |
|
<svg |
|
className="w-4 h-4 mr-2" |
|
fill="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm0 2.25c2.387 0 4.5 1.773 4.5 4.5S14.387 13.5 12 13.5 7.5 11.727 7.5 9s2.113-4.5 4.5-4.5zm0 16.5a9.26 9.26 0 01-6.75-2.798c.042-2.233 4.5-3.452 6.75-3.452s6.708 1.219 6.75 3.452A9.26 9.26 0 0112 21z" /> |
|
</svg> |
|
Connect HuggingFace |
|
</Link> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
|
|
{/* Account Linking Status */} |
|
<div className="border rounded-lg p-6 mb-6"> |
|
<div className="flex items-center justify-between mb-4"> |
|
<h3 className="text-lg font-semibold text-gray-900">Account Linking Status</h3> |
|
<div className={`px-3 py-1 rounded-full text-sm font-medium ${isLinked ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}> |
|
{isLinked ? 'Linked' : 'Not Linked'} |
|
</div> |
|
</div> |
|
|
|
{isLinked ? ( |
|
<div className="text-center bg-green-50 p-4 rounded-md"> |
|
<svg className="w-10 h-10 text-green-500 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> |
|
</svg> |
|
<p className="text-green-800 font-medium">Your GitHub and HuggingFace accounts are linked!</p> |
|
<p className="text-sm text-green-700 mt-1">Linked on: {new Date(userSession?.linkedAt || '').toLocaleString()}</p> |
|
|
|
<div className="mt-4"> |
|
<button |
|
onClick={() => { |
|
if (confirm("Are you sure you want to unlink your accounts?")) { |
|
window.location.href = "/auth/logout?unlink=true"; |
|
} |
|
}} |
|
className="text-sm text-red-600 hover:text-red-800 underline" |
|
> |
|
Unlink Accounts |
|
</button> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="text-center"> |
|
{hasGitHub && hasHuggingFace ? ( |
|
<div className="bg-yellow-50 p-4 rounded-md"> |
|
<svg className="w-10 h-10 text-yellow-500 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
|
</svg> |
|
<p className="text-yellow-800 font-medium">Your accounts are not linked</p> |
|
<p className="text-sm text-yellow-700 mt-1">You have both accounts connected but they couldn't be linked automatically.</p> |
|
<p className="text-sm text-yellow-700 mt-1">This may be because one of these accounts is already linked to another account.</p> |
|
|
|
<div className="mt-4"> |
|
<button |
|
onClick={() => { |
|
if (confirm("Try to link your accounts? This will refresh your authentication.")) { |
|
window.location.href = "/auth/github?link=true"; |
|
} |
|
}} |
|
className="text-sm text-blue-600 hover:text-blue-800 underline" |
|
> |
|
Try Manual Linking |
|
</button> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="bg-gray-50 p-4 rounded-md"> |
|
<p className="text-gray-700"> |
|
Connect both your GitHub and HuggingFace accounts to link them. |
|
</p> |
|
</div> |
|
)} |
|
</div> |
|
)} |
|
</div> |
|
|
|
{/* Stats */} |
|
{linkingStats && ( |
|
<div className="text-center text-sm text-gray-500 mt-4"> |
|
<p>Total linked accounts: {linkingStats.totalLinks}</p> |
|
<p>Last updated: {new Date(linkingStats.lastModified).toLocaleString()}</p> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
} |