mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46: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 { lichessLogin, lichessLogout } = useLichess();
|
||||
const { lichessLogin, lichessLogout } = useLichessOAuth();
|
||||
const { user } = useLichessUser();
|
||||
|
||||
const onClickHandler = () => {
|
||||
return user?.isLoggedIn ? lichessLogout() : lichessLogin();
|
||||
};
|
||||
|
||||
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={lichessLogin}
|
||||
onClick={onClickHandler}
|
||||
type="button"
|
||||
>
|
||||
<div className="w-6 h-6">
|
||||
@@ -20,7 +26,9 @@ const LichessButton = () => {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p>Log into Lichess.org</p>
|
||||
<p>
|
||||
{user?.isLoggedIn ? `Logout ${user.username}` : "Log into Lichess.org"}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { useServerFn } from "@tanstack/react-start";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { getSession } from "#/server/lichess.ts";
|
||||
|
||||
interface ILichessUser {
|
||||
username: string;
|
||||
id: string;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
interface ILichessUserContextType {
|
||||
user: ILichessUser | null;
|
||||
setUser: Dispatch<SetStateAction<ILichessUser | null>>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
||||
@@ -24,15 +29,23 @@ const LichessUserContext = createContext<ILichessUserContextType | undefined>(
|
||||
|
||||
export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<ILichessUser | null>(null);
|
||||
const getSessionFn = useServerFn(getSession);
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
// TODO useEffect to check and login user on mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const session = await getSessionFn();
|
||||
if (session) {
|
||||
setUser({
|
||||
username: session.username,
|
||||
id: session.id,
|
||||
isLoggedIn: true,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [getSessionFn]);
|
||||
|
||||
return (
|
||||
<LichessUserContext.Provider value={{ user, setUser, logout }}>
|
||||
<LichessUserContext.Provider value={{ user, setUser }}>
|
||||
{children}
|
||||
</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 { useLichessCallback } from "#/hooks/useLichessCallback.ts";
|
||||
import { useLichessOAuth } from "#/hooks/useLichessOAuth.ts";
|
||||
|
||||
const routeApi = getRouteApi("/callback");
|
||||
|
||||
const Callback = () => {
|
||||
const { useLichessCallback } = useLichessOAuth();
|
||||
const { code } = routeApi.useSearch();
|
||||
useLichessCallback({ code });
|
||||
useLichessCallback({ code }).then();
|
||||
|
||||
return <div>{code}</div>;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import Callback from "#/pages/Callback.tsx";
|
||||
|
||||
const callbackSchema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
});
|
||||
|
||||
// type CallbackProps = z.infer<typeof callbackSchema>;
|
||||
|
||||
+100
-14
@@ -1,31 +1,121 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import {
|
||||
deleteCookie,
|
||||
getCookie,
|
||||
setCookie,
|
||||
} from "@tanstack/react-start/server";
|
||||
import { env } from "#/env.ts";
|
||||
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
||||
|
||||
export const getToken = createServerFn({ method: "POST" }).handler(async () => {
|
||||
console.log("getTokens");
|
||||
});
|
||||
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
||||
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 () => {
|
||||
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 () => {
|
||||
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
||||
const createChallenge = (verifier: string) =>
|
||||
base64UrlEncode(sha256(verifier));
|
||||
export const setSession = createServerFn({ method: "POST" })
|
||||
.inputValidator((data: { username: string; id: string }) => data)
|
||||
.handler(async ({ data }) => {
|
||||
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 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({
|
||||
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"}`,
|
||||
redirect_uri: getRedirectUri(),
|
||||
scope: "study:read",
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: challenge,
|
||||
@@ -33,9 +123,5 @@ export const login = createServerFn({ method: "POST" }).handler(async () => {
|
||||
|
||||
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