Compare commits

...

3 Commits

7 changed files with 171 additions and 61 deletions

62
app/components/guard.tsx Normal file
View File

@@ -0,0 +1,62 @@
import type { PropsWithChildren } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "~/hooks/auth";
import { Button, Modal, Spinner } from "@heroui/react";
export default function Guard({ children }: PropsWithChildren) {
const { user, loading, login, logout } = useAuth();
const navigate = useNavigate();
if (loading) {
return (
<div className="flex flex-col flex-1 h-full w-full justify-center items-center">
<Spinner color="current" size="xl" />
</div>
);
}
if (user !== null) {
return children;
}
return (
<>
<div className="h-dvh w-dvw blur-2xl">
<Modal>
<Modal.Backdrop
isDismissable={false}
isKeyboardDismissDisabled
isOpen
>
<Modal.Container>
<Modal.Dialog>
<Modal.Header>
<Modal.Heading>
Login required.
</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
To view this page, you need to be logged
in with your Steam account.
</p>
</Modal.Body>
<Modal.Footer>
<Button
variant="secondary"
onClick={() => {
navigate(-1);
}}
>
Cancel
</Button>
<Button onClick={login}>Login</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>
</>
);
}

View File

@@ -1,8 +1,25 @@
import { Button, Avatar, Typography, Dropdown, Label, Separator } from "@heroui/react";
import {
Button,
Avatar,
Typography,
Dropdown,
Label,
Separator,
Spinner,
} from "@heroui/react";
import { useAuth } from "~/hooks/auth";
export default function User() {
const {user, login, logout} = useAuth();
const { user, loading, login, logout } = useAuth();
if (loading) {
return (
<Button isPending={true}>
<Spinner color="current" size="md" />
Loading...
</Button>
);
}
if (user === null) {
return <Button onClick={() => login()}>Login</Button>;
@@ -10,23 +27,29 @@ export default function User() {
return (
<Dropdown>
<Button className="pr-0">
<Typography.Paragraph>{user.name}</Typography.Paragraph>
<Avatar>
<Avatar.Image alt="Avatar" src={user.avatar}></Avatar.Image>
</Avatar>
</Button>
<Dropdown.Popover >
<Dropdown.Menu onAction={(key) => handleAction(String(key), logout)}>
<Dropdown.Item id="settings" textValue="Settings">
<Label>Settings</Label>
</Dropdown.Item>
<Separator variant="tertiary"/>
<Dropdown.Item id="logout" textValue="Logout" variant="danger">
<Label>Logout</Label>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
<Button className="pr-0">
<Typography.Paragraph>{user.name}</Typography.Paragraph>
<Avatar>
<Avatar.Image alt="Avatar" src={user.avatar}></Avatar.Image>
</Avatar>
</Button>
<Dropdown.Popover>
<Dropdown.Menu
onAction={(key) => handleAction(String(key), logout)}
>
<Dropdown.Item id="settings" textValue="Settings">
<Label>Settings</Label>
</Dropdown.Item>
<Separator variant="tertiary" />
<Dropdown.Item
id="logout"
textValue="Logout"
variant="danger"
>
<Label>Logout</Label>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
);
}
@@ -37,4 +60,4 @@ function handleAction(key: string, logout: () => Promise<void>) {
} else {
logout();
}
}
}

View File

@@ -1,64 +1,85 @@
import { useEffect, useState, createContext, useContext, type PropsWithChildren } from "react";
import { useSearchParams } from "react-router";
import {
useEffect,
useState,
createContext,
useContext,
type PropsWithChildren,
} from "react";
import { useLocation, useSearchParams } from "react-router";
type User = {
id: string,
steamid: string,
name: string,
avatar: string
}
id: string;
steamid: string;
name: string;
avatar: string;
};
type AuthContextType = {
user: User | null,
login: () => void,
logout: () => Promise<void>
}
user: User | null;
loading: boolean;
login: () => void;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType>({
user: null,
loading: false,
login: () => {},
logout: async () => {}
logout: async () => {},
});
export function AuthProvider({children}: PropsWithChildren) {
export function AuthProvider({ children }: PropsWithChildren) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [searchParams, setSeachParams] = useSearchParams();
const location = useLocation();
useEffect(() => {
const load = async () => {
if (searchParams.get("openid.ns") !== null) {
let response = await fetch("http://localhost:8080/login?" + searchParams.toString(), {credentials: "include"}).then(r => r.text());
setSeachParams({}, {replace: true});
if (response === "nok") {
// TODO: signal error to user
if (searchParams.get("openid.ns") !== null) {
setLoading(true);
let query = searchParams.toString();
setSeachParams({}, { replace: true });
let response = await fetch(
"http://localhost:8080/login?" + query,
{ credentials: "include" },
).then((r) => r.text());
if (response === "nok") {
// TODO: signal error to user
}
}
}
let data = await fetch("http://localhost:8080/me", {credentials: "include"})
.then(r => r.json())
let data = await fetch("http://localhost:8080/me", {
credentials: "include",
}).then((r) => r.json());
console.log(data);
setUser(data);
}
console.log(data);
setUser(data);
setLoading(false);
};
load();
}, []);
const login = () => {
location.replace("https://steamcommunity.com/openid/login?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=http://localhost:5173&openid.return_to=http://localhost:5173");
window.location.replace(
"https://steamcommunity.com/openid/login?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=http://localhost:5173&openid.return_to=http://localhost:5173" +
location.pathname,
);
};
const logout = async () => {
await fetch("http://localhost:8080/logout", {credentials: "include"})
await fetch("http://localhost:8080/logout", { credentials: "include" });
setUser(null);
};
return <AuthContext.Provider value={{user, login, logout}}>
{children}
</AuthContext.Provider>
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
}

View File

@@ -1,13 +1,17 @@
import type { PropsWithChildren } from "react";
import { Footer } from "~/components/footer";
import Guard from "~/components/guard";
import { Header } from "~/components/header";
export default function Base({ children }: PropsWithChildren) {
export default function Base({
children,
needsLogin = false,
}: PropsWithChildren<{ needsLogin?: boolean }>) {
return (
<>
<div className="flex flex-col pt-20 min-h-dvh">
<div className="flex-1">
{children}
<div className="flex flex-col flex-1">
{needsLogin ? <Guard>{children}</Guard> : children}
</div>
<Footer />
</div>

View File

@@ -1,5 +1,5 @@
import Base from "./base"
import Base from "./base";
export function GamemodesPage() {
return <Base>GAMEMODES</Base>
}
return <Base needsLogin>GAMEMODES</Base>;
}

View File

@@ -1,5 +1,5 @@
import Base from "./base"
import Base from "./base";
export function InventoryPage() {
return <Base>INVENTORY</Base>
}
return <Base needsLogin>INVENTORY</Base>;
}

View File

@@ -1,5 +1,5 @@
import Base from "./base"
export function MapsPage() {
return <Base>MAPS</Base>
return <Base needsLogin>MAPS</Base>
}