Compare commits

...

5 Commits

Author SHA1 Message Date
MasterGordon bcdee860c7 fixed ci 2026-04-22 22:01:58 +02:00
MasterGordon a1763bbbc3 readded tailwind config 2026-04-22 21:56:54 +02:00
MasterGordon 17998bc613 removed tailwind config 2026-04-22 21:52:48 +02:00
Gordon Goldbach 19dcf025e6
Merge pull request #6 from MasterGordon/restrict-unauthenticated-access-2097218213544781789
Restrict access to authenticated-only features
2026-04-22 21:50:25 +02:00
google-labs-jules[bot] 92c6fcd1ec Restrict access to authenticated-only features
- Implement ProtectedRoute in main.tsx for history, settings, and collection.
- Update Shell.tsx to use server-side identity (user.getSelf) for navigation visibility.
- Add context-specific login CTAs to Store and Play pages.
- Clear invalid login tokens from localStorage on startup.
- Update RegisterButton to support custom labels.

Co-authored-by: MasterGordon <18127395+MasterGordon@users.noreply.github.com>
2026-04-22 19:47:20 +00:00
10 changed files with 77 additions and 35 deletions

View File

@ -14,7 +14,7 @@ jobs:
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
node-version: 22 node-version: 22
- run: bun install - run: bun install --frozen-lockfile
- run: bun run build - run: bun run build
- run: bun test - run: bun test
- run: bun lint - run: bun lint

BIN
bun.lockb

Binary file not shown.

View File

@ -51,7 +51,7 @@
"zod": "^4.1.8" "zod": "^4.1.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "next", "@tailwindcss/vite": "^4.2.4",
"@types/bun": "latest", "@types/bun": "latest",
"@types/random-seed": "^0.3.5", "@types/random-seed": "^0.3.5",
"@types/react": "^19.1.13", "@types/react": "^19.1.13",

View File

@ -17,8 +17,7 @@ import { useMediaQuery } from "@uidotdev/usehooks";
import Header from "./components/Header"; import Header from "./components/Header";
import { Tag } from "./components/Tag"; import { Tag } from "./components/Tag";
import Feed from "./components/Feed/Feed"; import Feed from "./components/Feed/Feed";
import { useAtom } from "jotai"; import { useWSQuery } from "./hooks";
import { loginTokenAtom } from "./atoms";
const drawerWidth = 256; const drawerWidth = 256;
const drawerWidthWithPadding = drawerWidth; const drawerWidthWithPadding = drawerWidth;
@ -26,7 +25,7 @@ const drawerWidthWithPadding = drawerWidth;
const Shell: React.FC<PropsWithChildren> = ({ children }) => { const Shell: React.FC<PropsWithChildren> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const drawerRef = useRef<HTMLDivElement>(null); const drawerRef = useRef<HTMLDivElement>(null);
const [loginToken] = useAtom(loginTokenAtom); const { data: username } = useWSQuery("user.getSelf", null);
const x = isOpen ? 0 : -drawerWidthWithPadding; const x = isOpen ? 0 : -drawerWidthWithPadding;
const width = isOpen ? drawerWidthWithPadding : 0; const width = isOpen ? drawerWidthWithPadding : 0;
@ -82,7 +81,7 @@ const Shell: React.FC<PropsWithChildren> = ({ children }) => {
<Play /> <Play />
Play Play
</NavLink> </NavLink>
{loginToken && ( {username && (
<NavLink href="/history"> <NavLink href="/history">
<History /> <History />
History History
@ -92,13 +91,13 @@ const Shell: React.FC<PropsWithChildren> = ({ children }) => {
<Store /> <Store />
Store Store
</NavLink> </NavLink>
{loginToken && ( {username && (
<NavLink href="/collection"> <NavLink href="/collection">
<Library /> <Library />
Collection <Tag size="sm">NEW</Tag> Collection <Tag size="sm">NEW</Tag>
</NavLink> </NavLink>
)} )}
{loginToken && ( {username && (
<NavLink href="/settings"> <NavLink href="/settings">
<Settings /> <Settings />
Settings Settings

View File

@ -15,7 +15,7 @@ import { useQueryClient } from "@tanstack/react-query";
import PasswordInput from "./PasswordInput"; import PasswordInput from "./PasswordInput";
import { wsClient } from "../../wsClient"; import { wsClient } from "../../wsClient";
const RegisterButton = () => { const RegisterButton = ({ label }: { label?: string }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isLoginMode, setIsLoginMode] = useState(false); const [isLoginMode, setIsLoginMode] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -36,7 +36,7 @@ const RegisterButton = () => {
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="primary" className="self-start"> <Button variant="primary" className="self-start">
Register {label ?? "Register"}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>

View File

@ -188,11 +188,11 @@ const Board: React.FC<BoardProps> = (props) => {
(props.width || props.height (props.width || props.height
? { left: 0, right: boardWidth, top: 0, bottom: boardHeight } ? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
: { : {
left: -theme.size, left: -theme.size,
right: boardWidth + theme.size, right: boardWidth + theme.size,
top: -theme.size, top: -theme.size,
bottom: boardHeight + theme.size, bottom: boardHeight + theme.size,
}), }),
[boardHeight, boardWidth, props.height, props.width, theme], [boardHeight, boardWidth, props.height, props.width, theme],
); );
const clampZoom = useMemo( const clampZoom = useMemo(

View File

@ -15,17 +15,49 @@ import Collection from "./views/collection/Collection.tsx";
import { AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import Store from "./views/store/Store.tsx"; import Store from "./views/store/Store.tsx";
import Profile from "./views/profile/Profile.tsx"; import Profile from "./views/profile/Profile.tsx";
import { useWSQuery } from "./hooks.ts";
import RegisterButton from "./components/Auth/RegisterButton.tsx";
const ProtectedRoute: React.FC<{
component: React.ComponentType<any>;
path: string;
}> = ({ component: Component, path }) => {
const { data: username, isLoading } = useWSQuery("user.getSelf", null);
return (
<Route path={path}>
{(params) => {
if (isLoading) return null;
if (!username) {
return (
<div className="flex flex-col items-center justify-center py-24 gap-6">
<h2 className="text-white/90 text-2xl font-bold">
This page is only available to logged-in users.
</h2>
<RegisterButton label="Login to access" />
</div>
);
}
return <Component {...params} />;
}}
</Route>
);
};
const setup = async () => { const setup = async () => {
const token = localStorage.getItem("loginToken"); const token = localStorage.getItem("loginToken");
if (token) { if (token) {
try { try {
await wsClient.dispatch("user.loginWithToken", { const res = await wsClient.dispatch("user.loginWithToken", {
token: JSON.parse(token), token: JSON.parse(token),
}); });
if (!res.success) {
localStorage.removeItem("loginToken");
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
localStorage.removeItem("loginToken");
} }
} }
}; };
@ -41,9 +73,9 @@ setup().then(() => {
<Route path="/play/:gameId?"> <Route path="/play/:gameId?">
{(params) => <Endless gameId={params.gameId} />} {(params) => <Endless gameId={params.gameId} />}
</Route> </Route>
<Route path="/history" component={MatchHistory} /> <ProtectedRoute path="/history" component={MatchHistory} />
<Route path="/settings" component={Settings} /> <ProtectedRoute path="/settings" component={Settings} />
<Route path="/collection" component={Collection} /> <ProtectedRoute path="/collection" component={Collection} />
<Route path="/store" component={Store} /> <Route path="/store" component={Store} />
<Route path="/profile/:username?"> <Route path="/profile/:username?">
{(params) => <Profile username={params.username} />} {(params) => <Profile username={params.username} />}

View File

@ -1,6 +1,6 @@
import { useWSMutation, useWSQuery } from "../../hooks"; import { useWSMutation, useWSQuery } from "../../hooks";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { gameIdAtom, loginTokenAtom } from "../../atoms"; import { gameIdAtom } from "../../atoms";
import { Button } from "../../components/Button"; import { Button } from "../../components/Button";
import LeaderboardButton from "../../components/LeaderboardButton"; import LeaderboardButton from "../../components/LeaderboardButton";
import { ShareButton } from "../../components/ShareButton"; import { ShareButton } from "../../components/ShareButton";
@ -15,7 +15,6 @@ interface EndlessProps {
const Endless: React.FC<EndlessProps> = (props) => { const Endless: React.FC<EndlessProps> = (props) => {
const [gameId, setGameId] = useAtom(gameIdAtom); const [gameId, setGameId] = useAtom(gameIdAtom);
const [loginToken] = useAtom(loginTokenAtom);
const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId); const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId);
const { data: settings } = useWSQuery("user.getSettings", null); const { data: settings } = useWSQuery("user.getSettings", null);
const { data: currentUsername } = useWSQuery("user.getSelf", null); const { data: currentUsername } = useWSQuery("user.getSelf", null);
@ -93,7 +92,7 @@ const Endless: React.FC<EndlessProps> = (props) => {
<div className="w-full grid md:grid-cols-[350px_1fr]"> <div className="w-full grid md:grid-cols-[350px_1fr]">
<div className="flex flex-col md:border-r-white/10 md:border-r-1 gap-8 pr-12"> <div className="flex flex-col md:border-r-white/10 md:border-r-1 gap-8 pr-12">
<h2 className="text-white/90 text-xl">Minesweeper Endless</h2> <h2 className="text-white/90 text-xl">Minesweeper Endless</h2>
{loginToken ? ( {currentUsername ? (
<Button <Button
className="w-fit" className="w-fit"
variant="primary" variant="primary"
@ -107,7 +106,7 @@ const Endless: React.FC<EndlessProps> = (props) => {
Start Game Start Game
</Button> </Button>
) : ( ) : (
<RegisterButton /> <RegisterButton label="Sign in to Play" />
)} )}
<h2 className="text-white/80 text-lg mt-8">How to play</h2> <h2 className="text-white/80 text-lg mt-8">How to play</h2>
<p className="text-white/90"> <p className="text-white/90">

View File

@ -18,11 +18,13 @@ import { useEffect } from "react";
import { initParticlesEngine, Particles as ParticlesComponent } from "@tsparticles/react"; import { initParticlesEngine, Particles as ParticlesComponent } from "@tsparticles/react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import BounceImg from "../../components/BounceImg"; import BounceImg from "../../components/BounceImg";
import RegisterButton from "../../components/Auth/RegisterButton";
const Store = () => { const Store = () => {
const openLootbox = useWSMutation("user.openLootbox"); const openLootbox = useWSMutation("user.openLootbox");
const [lootboxResult, setLootboxResult] = useAtom(lootboxResultAtom); const [lootboxResult, setLootboxResult] = useAtom(lootboxResultAtom);
const currentLootbox = lootboxes.find((l) => l.id === lootboxResult?.lootbox); const currentLootbox = lootboxes.find((l) => l.id === lootboxResult?.lootbox);
const { data: username } = useWSQuery("user.getSelf", null);
const { refetch } = useWSQuery("user.getOwnGems", null); const { refetch } = useWSQuery("user.getOwnGems", null);
// this should be run only once per application lifetime // this should be run only once per application lifetime
@ -184,18 +186,24 @@ const Store = () => {
</Dialog> </Dialog>
</div> </div>
<BounceImg src={lootbox.image} className="w-[360px]" /> <BounceImg src={lootbox.image} className="w-[360px]" />
<Button {username ? (
variant="outline" <Button
size="default" variant="outline"
className="mx-auto items-center" size="default"
onClick={() => { className="mx-auto items-center"
openLootbox onClick={() => {
.mutateAsync({ id: lootbox.id }) openLootbox
.then(() => refetch()); .mutateAsync({ id: lootbox.id })
}} .then(() => refetch());
> }}
Buy for <b>{lootbox.priceText}</b> <GemsIcon /> >
</Button> Buy for <b>{lootbox.priceText}</b> <GemsIcon />
</Button>
) : (
<div className="flex justify-center">
<RegisterButton label="Login to Buy" />
</div>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -0,0 +1,4 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
};