mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba05fbedd3
|
|||
|
82c24ba9d4
|
|||
|
0888df153f
|
@@ -1,12 +1,12 @@
|
||||
interface IProps {
|
||||
onClickHandler: () => void;
|
||||
}
|
||||
import { useLichess } from "#/hooks/useLichess.ts";
|
||||
|
||||
const LichessButton = () => {
|
||||
const { lichessLogin, lichessLogout } = useLichess();
|
||||
|
||||
const LichessButton = ({ onClickHandler }: IProps) => {
|
||||
return (
|
||||
<button
|
||||
className="text-(--accent) border rounded-xl px-6 py-3 cursor-pointer flex items-center gap-2 p-2 hover:bg-(--accent) hover:text-(--header-bg) transition-colors duration-300 ease-in-out"
|
||||
onClick={onClickHandler}
|
||||
onClick={lichessLogin}
|
||||
type="button"
|
||||
>
|
||||
<div className="w-6 h-6">
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
createContext,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface ILichessUser {
|
||||
username: string;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
interface ILichessUserContextType {
|
||||
user: ILichessUser | null;
|
||||
setUser: Dispatch<SetStateAction<ILichessUser | null>>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<ILichessUser | null>(null);
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
// TODO useEffect to check and login user on mount
|
||||
|
||||
return (
|
||||
<LichessUserContext.Provider value={{ user, setUser, logout }}>
|
||||
{children}
|
||||
</LichessUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLichessUser = () => {
|
||||
const context = useContext(LichessUserContext);
|
||||
if (!context) {
|
||||
throw new Error("useLichessUser must be used within a LichessUserProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useServerFn } from "@tanstack/react-start";
|
||||
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||
import { getToken, login } from "#/server/lichess.ts";
|
||||
|
||||
export const useLichess = () => {
|
||||
const { user, setUser, logout } = useLichessUser();
|
||||
|
||||
const triggerLogin = useServerFn(login);
|
||||
const trigggerGetToken = useServerFn(getToken);
|
||||
|
||||
console.log("Lichess User:", user);
|
||||
|
||||
const lichessTokenVerification = async ({ code }) => {
|
||||
await trigggerGetToken();
|
||||
};
|
||||
|
||||
const lichessLogin = async () => {
|
||||
// TODO add try/catch with toast
|
||||
await triggerLogin();
|
||||
};
|
||||
|
||||
const lichessLogout = () => {
|
||||
console.log("Lichess Logout");
|
||||
// TODO remove cookies
|
||||
logout();
|
||||
};
|
||||
|
||||
return {
|
||||
lichessLogin,
|
||||
lichessLogout,
|
||||
lichessTokenVerification,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLichess } from "#/hooks/useLichess.ts";
|
||||
|
||||
interface IProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const useLichessCallback = ({ code }: IProps) => {
|
||||
const { lichessTokenVerification } = useLichess();
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) return;
|
||||
|
||||
const response = lichessTokenVerification({ code });
|
||||
console.log("Response:", response);
|
||||
}, [code, lichessTokenVerification]);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export const base64UrlEncode = (str: Buffer) => {
|
||||
return str
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
};
|
||||
|
||||
export const sha256 = (buffer: string) =>
|
||||
createHash("sha256").update(buffer).digest();
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getRouteApi } from "@tanstack/react-router";
|
||||
import { useLichessCallback } from "#/hooks/useLichessCallback.ts";
|
||||
|
||||
const routeApi = getRouteApi("/callback");
|
||||
|
||||
const Callback = () => {
|
||||
const { code } = routeApi.useSearch();
|
||||
useLichessCallback({ code });
|
||||
|
||||
return <div>{code}</div>;
|
||||
};
|
||||
|
||||
export default Callback;
|
||||
@@ -36,7 +36,7 @@ const Chessboard = () => {
|
||||
return (
|
||||
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
||||
<Section title="Convert PGN to PDF">
|
||||
<LichessButton onClickHandler={() => console.log("Lichess Login")} /> or{" "}
|
||||
<LichessButton /> or{" "}
|
||||
<input
|
||||
type="file"
|
||||
id="file-input"
|
||||
|
||||
+21
-3
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ContactRouteImport } from './routes/contact'
|
||||
import { Route as ChessboardRouteImport } from './routes/chessboard'
|
||||
import { Route as CallbackRouteImport } from './routes/callback'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const ContactRoute = ContactRouteImport.update({
|
||||
@@ -23,6 +24,11 @@ const ChessboardRoute = ChessboardRouteImport.update({
|
||||
path: '/chessboard',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const CallbackRoute = CallbackRouteImport.update({
|
||||
id: '/callback',
|
||||
path: '/callback',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -31,30 +37,34 @@ const IndexRoute = IndexRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/callback': typeof CallbackRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/callback': typeof CallbackRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/callback': typeof CallbackRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/chessboard' | '/contact'
|
||||
fullPaths: '/' | '/callback' | '/chessboard' | '/contact'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/chessboard' | '/contact'
|
||||
id: '__root__' | '/' | '/chessboard' | '/contact'
|
||||
to: '/' | '/callback' | '/chessboard' | '/contact'
|
||||
id: '__root__' | '/' | '/callback' | '/chessboard' | '/contact'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
CallbackRoute: typeof CallbackRoute
|
||||
ChessboardRoute: typeof ChessboardRoute
|
||||
ContactRoute: typeof ContactRoute
|
||||
}
|
||||
@@ -75,6 +85,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ChessboardRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/callback': {
|
||||
id: '/callback'
|
||||
path: '/callback'
|
||||
fullPath: '/callback'
|
||||
preLoaderRoute: typeof CallbackRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -87,6 +104,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
CallbackRoute: CallbackRoute,
|
||||
ChessboardRoute: ChessboardRoute,
|
||||
ContactRoute: ContactRoute,
|
||||
}
|
||||
|
||||
+18
-15
@@ -4,6 +4,7 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { type ReactNode, Suspense } from "react";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
||||
import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
|
||||
import Footer from "../components/Footer";
|
||||
import Header from "../components/Header";
|
||||
import appCss from "../styles.css?url";
|
||||
@@ -51,21 +52,23 @@ function RootDocument({ children }: { children: ReactNode }) {
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<Header />
|
||||
{children}
|
||||
<ToastContainer position="bottom-right" />
|
||||
<Footer />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<LichessUserProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<ToastContainer position="bottom-right" />
|
||||
<Footer />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</LichessUserProvider>
|
||||
<Scripts />
|
||||
<Suspense fallback={null}>
|
||||
<MatomoAnalytics />
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
import Callback from "#/pages/Callback.tsx";
|
||||
|
||||
const callbackSchema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
});
|
||||
|
||||
// type CallbackProps = z.infer<typeof callbackSchema>;
|
||||
|
||||
export const Route = createFileRoute("/callback")({
|
||||
validateSearch: (search) => callbackSchema.parse(search),
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <Callback />;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { env } from "#/env.ts";
|
||||
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
||||
|
||||
export const getToken = createServerFn({ method: "POST" }).handler(async () => {
|
||||
console.log("getTokens");
|
||||
});
|
||||
|
||||
export const verifyToken = createServerFn({ method: "POST" }).handler(
|
||||
async () => {
|
||||
console.log("verifyTokens");
|
||||
},
|
||||
);
|
||||
|
||||
export const login = createServerFn({ method: "POST" }).handler(async () => {
|
||||
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
||||
const createChallenge = (verifier: string) =>
|
||||
base64UrlEncode(sha256(verifier));
|
||||
|
||||
const verifier = createVerifier();
|
||||
const challenge = createChallenge(verifier);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
client_id: env.LICHESS_CLIENT_ID || "",
|
||||
redirect_uri: `${process.env.NODE_ENV === "production" ? `https://${env.LICHESS_CLIENT_ID}/callback` : "http://localhost:3000/callback"}`,
|
||||
scope: "study:read",
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: challenge,
|
||||
});
|
||||
|
||||
throw redirect({
|
||||
href: `https://lichess.org/oauth?${params.toString()}`,
|
||||
statusCode: 302,
|
||||
headers: {
|
||||
"Set-Cookie": `lichess_oauth_verifier=${verifier}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600`, // Expires in 10 mins
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user