mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 09:56:57 +00:00
Compare commits
4 Commits
ba05fbedd3
...
0b1a0e6457
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b1a0e6457
|
|||
|
3b29c42b3b
|
|||
|
430ec2db75
|
|||
|
d1cdc7764b
|
@@ -1,12 +1,18 @@
|
|||||||
import { useLichess } from "#/hooks/useLichess.ts";
|
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||||
|
import { useLichessOAuth } from "#/hooks/useLichessOAuth.ts";
|
||||||
|
|
||||||
const LichessButton = () => {
|
const LichessButton = () => {
|
||||||
const { lichessLogin, lichessLogout } = useLichess();
|
const { lichessLogin, lichessLogout } = useLichessOAuth();
|
||||||
|
const { user } = useLichessUser();
|
||||||
|
|
||||||
|
const onClickHandler = () => {
|
||||||
|
return user?.isLoggedIn ? lichessLogout() : lichessLogin();
|
||||||
|
};
|
||||||
|
|
||||||
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={lichessLogin}
|
onClick={onClickHandler}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
@@ -20,7 +26,9 @@ const LichessButton = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Log into Lichess.org</p>
|
<p>
|
||||||
|
{user?.isLoggedIn ? `Logout ${user.username}` : "Log into Lichess.org"}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
import { getSession } from "#/server/lichess.ts";
|
||||||
|
|
||||||
interface ILichessUser {
|
interface ILichessUser {
|
||||||
username: string;
|
username: string;
|
||||||
|
id: string;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILichessUserContextType {
|
interface ILichessUserContextType {
|
||||||
user: ILichessUser | null;
|
user: ILichessUser | null;
|
||||||
setUser: Dispatch<SetStateAction<ILichessUser | null>>;
|
setUser: Dispatch<SetStateAction<ILichessUser | null>>;
|
||||||
logout: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
||||||
@@ -24,15 +29,23 @@ const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
|||||||
|
|
||||||
export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
|
export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [user, setUser] = useState<ILichessUser | null>(null);
|
const [user, setUser] = useState<ILichessUser | null>(null);
|
||||||
|
const getSessionFn = useServerFn(getSession);
|
||||||
|
|
||||||
const logout = () => {
|
useEffect(() => {
|
||||||
setUser(null);
|
(async () => {
|
||||||
};
|
const session = await getSessionFn();
|
||||||
|
if (session) {
|
||||||
// TODO useEffect to check and login user on mount
|
setUser({
|
||||||
|
username: session.username,
|
||||||
|
id: session.id,
|
||||||
|
isLoggedIn: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [getSessionFn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LichessUserContext.Provider value={{ user, setUser, logout }}>
|
<LichessUserContext.Provider value={{ user, setUser }}>
|
||||||
{children}
|
{children}
|
||||||
</LichessUserContext.Provider>
|
</LichessUserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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,88 @@
|
|||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||||
|
import {
|
||||||
|
getSession,
|
||||||
|
getToken,
|
||||||
|
getUser,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
setSession,
|
||||||
|
} from "#/server/lichess.ts";
|
||||||
|
|
||||||
|
interface ITokenData {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLichessOAuth = () => {
|
||||||
|
const { setUser } = useLichessUser();
|
||||||
|
|
||||||
|
const loginFn = useServerFn(login);
|
||||||
|
const logoutFn = useServerFn(logout);
|
||||||
|
const accessTokenFn = useServerFn(getToken);
|
||||||
|
const getUserFn = useServerFn(getUser);
|
||||||
|
const setSessionFn = useServerFn(setSession);
|
||||||
|
const getSessionFn = useServerFn(getSession);
|
||||||
|
|
||||||
|
const lichessAccessToken = async ({ code }: { code: string }) => {
|
||||||
|
return accessTokenFn({ data: { code } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLichessUser = async ({
|
||||||
|
username,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
id: string;
|
||||||
|
}) => {
|
||||||
|
await setSessionFn({ data: { username, id } });
|
||||||
|
setUser({ username, id, isLoggedIn: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLichessUser = async ({ tokenData }: { tokenData: ITokenData }) => {
|
||||||
|
return getUserFn({
|
||||||
|
data: { access_token: tokenData.access_token },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const lichessLogin = async () => {
|
||||||
|
await loginFn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const lichessLogout = async () => {
|
||||||
|
await logoutFn();
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLichessSession = async () => {
|
||||||
|
return getSessionFn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLichessCallback = async ({ code }: { code: string }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const tokenData = await lichessAccessToken({ code });
|
||||||
|
const userData = await getLichessUser({ tokenData });
|
||||||
|
await setLichessUser({ username: userData.username, id: userData.id });
|
||||||
|
await navigate({ to: "/chessboard" });
|
||||||
|
})();
|
||||||
|
}, [navigate, code]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
lichessLogin,
|
||||||
|
lichessLogout,
|
||||||
|
lichessAccessToken,
|
||||||
|
getLichessUser,
|
||||||
|
setLichessUser,
|
||||||
|
getLichessSession,
|
||||||
|
useLichessCallback,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { getRouteApi } from "@tanstack/react-router";
|
import { getRouteApi } from "@tanstack/react-router";
|
||||||
import { useLichessCallback } from "#/hooks/useLichessCallback.ts";
|
import { useLichessOAuth } from "#/hooks/useLichessOAuth.ts";
|
||||||
|
|
||||||
const routeApi = getRouteApi("/callback");
|
const routeApi = getRouteApi("/callback");
|
||||||
|
|
||||||
const Callback = () => {
|
const Callback = () => {
|
||||||
|
const { useLichessCallback } = useLichessOAuth();
|
||||||
const { code } = routeApi.useSearch();
|
const { code } = routeApi.useSearch();
|
||||||
useLichessCallback({ code });
|
useLichessCallback({ code }).then();
|
||||||
|
|
||||||
return <div>{code}</div>;
|
return <div>{code}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Callback from "#/pages/Callback.tsx";
|
|||||||
|
|
||||||
const callbackSchema = z.object({
|
const callbackSchema = z.object({
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
state: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// type CallbackProps = z.infer<typeof callbackSchema>;
|
// type CallbackProps = z.infer<typeof callbackSchema>;
|
||||||
|
|||||||
+100
-14
@@ -1,31 +1,121 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { redirect } from "@tanstack/react-router";
|
import { redirect } from "@tanstack/react-router";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import {
|
||||||
|
deleteCookie,
|
||||||
|
getCookie,
|
||||||
|
setCookie,
|
||||||
|
} from "@tanstack/react-start/server";
|
||||||
import { env } from "#/env.ts";
|
import { env } from "#/env.ts";
|
||||||
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
||||||
|
|
||||||
export const getToken = createServerFn({ method: "POST" }).handler(async () => {
|
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
||||||
console.log("getTokens");
|
const createChallenge = (verifier: string) => base64UrlEncode(sha256(verifier));
|
||||||
});
|
|
||||||
|
|
||||||
export const verifyToken = createServerFn({ method: "POST" }).handler(
|
const getRedirectUri = () =>
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? `https://${env.LICHESS_CLIENT_ID}/callback`
|
||||||
|
: "http://localhost:3000/callback";
|
||||||
|
|
||||||
|
export const getSession = createServerFn({ method: "GET" }).handler(
|
||||||
async () => {
|
async () => {
|
||||||
console.log("verifyTokens");
|
const session = getCookie("lichess-session");
|
||||||
|
if (!session) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(session) as { username: string; id: string };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const login = createServerFn({ method: "POST" }).handler(async () => {
|
export const setSession = createServerFn({ method: "POST" })
|
||||||
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
.inputValidator((data: { username: string; id: string }) => data)
|
||||||
const createChallenge = (verifier: string) =>
|
.handler(async ({ data }) => {
|
||||||
base64UrlEncode(sha256(verifier));
|
setCookie(
|
||||||
|
"lichess-session",
|
||||||
|
JSON.stringify({ username: data.username, id: data.id }),
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setCookie("lichess-oauth-verifier", "", {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { username: data.username, id: data.id };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getToken = createServerFn({ method: "POST" })
|
||||||
|
.inputValidator((data: { code: string }) => data)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const verifier = getCookie("lichess-oauth-verifier");
|
||||||
|
|
||||||
|
if (!verifier)
|
||||||
|
throw new Error("OAuth session expired. Please login again.");
|
||||||
|
|
||||||
|
const response = await fetch("https://lichess.org/api/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
client_id: env.LICHESS_CLIENT_ID || "",
|
||||||
|
code: data.code,
|
||||||
|
redirect_uri: getRedirectUri(),
|
||||||
|
code_verifier: verifier,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(`Token exchange failed: ${await response.text()}`);
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUser = createServerFn({ method: "GET" })
|
||||||
|
.inputValidator((data: { access_token: string }) => data)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const response = await fetch("https://lichess.org/api/account", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logout = createServerFn().handler(async () => {
|
||||||
|
deleteCookie("lichess-session", { path: "/" });
|
||||||
|
deleteCookie("lichess-oauth-verifier", { path: "/" });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const login = createServerFn({ method: "GET" }).handler(async () => {
|
||||||
const verifier = createVerifier();
|
const verifier = createVerifier();
|
||||||
const challenge = createChallenge(verifier);
|
const challenge = createChallenge(verifier);
|
||||||
|
|
||||||
|
setCookie("lichess-oauth-verifier", verifier, {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 10,
|
||||||
|
});
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
client_id: env.LICHESS_CLIENT_ID || "",
|
client_id: env.LICHESS_CLIENT_ID || "",
|
||||||
redirect_uri: `${process.env.NODE_ENV === "production" ? `https://${env.LICHESS_CLIENT_ID}/callback` : "http://localhost:3000/callback"}`,
|
redirect_uri: getRedirectUri(),
|
||||||
scope: "study:read",
|
scope: "study:read",
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
code_challenge: challenge,
|
code_challenge: challenge,
|
||||||
@@ -33,9 +123,5 @@ export const login = createServerFn({ method: "POST" }).handler(async () => {
|
|||||||
|
|
||||||
throw redirect({
|
throw redirect({
|
||||||
href: `https://lichess.org/oauth?${params.toString()}`,
|
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