mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
Compare commits
3 Commits
585bb5549f
...
ba05fbedd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba05fbedd3
|
|||
|
82c24ba9d4
|
|||
|
0888df153f
|
@@ -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">
|
||||||
|
|||||||
@@ -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 (
|
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
@@ -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
@@ -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 />
|
||||||
|
|||||||
@@ -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