Compare commits

..

3 Commits

Author SHA1 Message Date
owenrees ba05fbedd3 add context 2026-06-07 23:07:59 +02:00
owenrees 82c24ba9d4 add placeholder function for token 2026-06-07 22:47:26 +02:00
owenrees 0888df153f check callback from lichess server 2026-06-07 22:44:31 +02:00
11 changed files with 227 additions and 24 deletions
+5 -5
View File
@@ -1,12 +1,12 @@
interface IProps { import { useLichess } from "#/hooks/useLichess.ts";
onClickHandler: () => void;
} const LichessButton = () => {
const { lichessLogin, lichessLogout } = useLichess();
const LichessButton = ({ onClickHandler }: IProps) => {
return ( return (
<button <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" 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" type="button"
> >
<div className="w-6 h-6"> <div className="w-6 h-6">
+47
View File
@@ -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;
};
+33
View File
@@ -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,
};
};
+17
View File
@@ -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]);
};
+12
View File
@@ -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();
+13
View File
@@ -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;
+1 -1
View File
@@ -36,7 +36,7 @@ const Chessboard = () => {
return ( return (
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]"> <main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
<Section title="Convert PGN to PDF"> <Section title="Convert PGN to PDF">
<LichessButton onClickHandler={() => console.log("Lichess Login")} /> or{" "} <LichessButton /> or{" "}
<input <input
type="file" type="file"
id="file-input" id="file-input"
+21 -3
View File
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as ContactRouteImport } from './routes/contact' import { Route as ContactRouteImport } from './routes/contact'
import { Route as ChessboardRouteImport } from './routes/chessboard' import { Route as ChessboardRouteImport } from './routes/chessboard'
import { Route as CallbackRouteImport } from './routes/callback'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
const ContactRoute = ContactRouteImport.update({ const ContactRoute = ContactRouteImport.update({
@@ -23,6 +24,11 @@ const ChessboardRoute = ChessboardRouteImport.update({
path: '/chessboard', path: '/chessboard',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const CallbackRoute = CallbackRouteImport.update({
id: '/callback',
path: '/callback',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -31,30 +37,34 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/callback': typeof CallbackRoute
'/chessboard': typeof ChessboardRoute '/chessboard': typeof ChessboardRoute
'/contact': typeof ContactRoute '/contact': typeof ContactRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/callback': typeof CallbackRoute
'/chessboard': typeof ChessboardRoute '/chessboard': typeof ChessboardRoute
'/contact': typeof ContactRoute '/contact': typeof ContactRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/callback': typeof CallbackRoute
'/chessboard': typeof ChessboardRoute '/chessboard': typeof ChessboardRoute
'/contact': typeof ContactRoute '/contact': typeof ContactRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/chessboard' | '/contact' fullPaths: '/' | '/callback' | '/chessboard' | '/contact'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/chessboard' | '/contact' to: '/' | '/callback' | '/chessboard' | '/contact'
id: '__root__' | '/' | '/chessboard' | '/contact' id: '__root__' | '/' | '/callback' | '/chessboard' | '/contact'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
CallbackRoute: typeof CallbackRoute
ChessboardRoute: typeof ChessboardRoute ChessboardRoute: typeof ChessboardRoute
ContactRoute: typeof ContactRoute ContactRoute: typeof ContactRoute
} }
@@ -75,6 +85,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ChessboardRouteImport preLoaderRoute: typeof ChessboardRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/callback': {
id: '/callback'
path: '/callback'
fullPath: '/callback'
preLoaderRoute: typeof CallbackRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -87,6 +104,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
CallbackRoute: CallbackRoute,
ChessboardRoute: ChessboardRoute, ChessboardRoute: ChessboardRoute,
ContactRoute: ContactRoute, ContactRoute: ContactRoute,
} }
+18 -15
View File
@@ -4,6 +4,7 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { type ReactNode, Suspense } from "react"; import { type ReactNode, Suspense } from "react";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx"; import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import Header from "../components/Header"; import Header from "../components/Header";
import appCss from "../styles.css?url"; import appCss from "../styles.css?url";
@@ -51,21 +52,23 @@ function RootDocument({ children }: { children: ReactNode }) {
<HeadContent /> <HeadContent />
</head> </head>
<body className="font-sans antialiased"> <body className="font-sans antialiased">
<Header /> <LichessUserProvider>
{children} <Header />
<ToastContainer position="bottom-right" /> {children}
<Footer /> <ToastContainer position="bottom-right" />
<TanStackDevtools <Footer />
config={{ <TanStackDevtools
position: "bottom-right", config={{
}} position: "bottom-right",
plugins={[ }}
{ plugins={[
name: "Tanstack Router", {
render: <TanStackRouterDevtoolsPanel />, name: "Tanstack Router",
}, render: <TanStackRouterDevtoolsPanel />,
]} },
/> ]}
/>
</LichessUserProvider>
<Scripts /> <Scripts />
<Suspense fallback={null}> <Suspense fallback={null}>
<MatomoAnalytics /> <MatomoAnalytics />
+19
View File
@@ -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 />;
}
+41
View File
@@ -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
},
});
});