Web Client #11

Merged
Simponic merged 19 commits from web into main 2023-01-19 16:04:10 -05:00
8 changed files with 152 additions and 90 deletions
Showing only changes of commit b98655caa3 - Show all commits

View File

@ -1,25 +1,57 @@
import React, { useContext, useState, createContext } from "react";
export const DEFAULT_EXPIRY_TIME_MS = 12 * 60 * 60 * 1000;
import { useEffect, useContext, createContext } from "react";
import { useLocalStorage } from "../hooks/useLocalStorage";
const AuthContext = createContext({
signedIn: false,
setSignedIn: () => null,
sessionOver: new Date(),
setSessionOver: () => null,
userId: null,
setUserId: () => null,
username: "",
setUsername: () => null,
setPlayer: () => null,
player: null,
signOut: () => null,
});
export const useAuthContext = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [signedIn, setSignedIn] = useState(false);
const [sessionOver, setSessionOver] = useState(new Date());
const [userId, setUserId] = useState(null);
const [username, setUsername] = useState(null);
const [signedIn, setSignedIn] = useLocalStorage("signedIn", false);
const [sessionOver, setSessionOver] = useLocalStorage(
"sessionOver",
Date.now()
);
const [player, setPlayer] = useLocalStorage("player", null);
const setDefaults = () => {
setPlayer(null);
setSessionOver(Date.now());
setSignedIn(false);
};
const signOut = () =>
fetch("/api/player/logout", {
method: "GET",
credentials: "same-origin",
}).then(() => setDefaults());
useEffect(() => {
setTimeout(() => {
setSessionOver((sessionOver) => {
if (Date.now() >= sessionOver) {
setSignedIn((signedIn) => {
if (signedIn)
alert(
"Session expired. Any further privileged requests will fail until signed in again."
);
return false;
});
setPlayer(null);
}
return sessionOver;
});
}, sessionOver - Date.now());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionOver]);
return (
<AuthContext.Provider
@ -28,10 +60,9 @@ export const AuthProvider = ({ children }) => {
setSignedIn,
sessionOver,
setSessionOver,
userId,
setUserId,
username,
setUsername,
signOut,
setPlayer,
player,
}}
>
{children}

View File

@ -0,0 +1,28 @@
import { useState, useEffect } from "react";
const STORAGE_KEYS_PREFIX = "chessh-";
const useStorage = (storage, keyPrefix) => (storageKey, fallbackState) => {
if (!storageKey)
throw new Error(
`"storageKey" must be a nonempty string, but "${storageKey}" was passed.`
);
const storedString = storage.getItem(keyPrefix + storageKey);
let parsedObject = null;
if (storedString !== null) parsedObject = JSON.parse(storedString);
const [value, setValue] = useState(parsedObject ?? fallbackState);
useEffect(() => {
storage.setItem(keyPrefix + storageKey, JSON.stringify(value));
}, [value, storageKey]);
return [value, setValue];
};
export const useLocalStorage = useStorage(
window.localStorage,
STORAGE_KEYS_PREFIX
);

View File

@ -5,18 +5,8 @@ import logo from "./assets/chessh_sm.svg";
import { useAuthContext } from "./context/auth_context";
export const Root = () => {
const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext();
const signOut = () => {
fetch("/api/player/logout", {
method: "GET",
credentials: "same-origin",
}).then(() => {
setSignedIn(false);
setUserId(null);
setSessionOver(new Date());
});
};
const { signedIn, setUserId, setSignedIn, setSessionOver, signOut } =
useAuthContext();
return (
<>

View File

@ -1,33 +1,23 @@
import { useEffect, useCallback } from "react";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import {
useAuthContext,
DEFAULT_EXPIRY_TIME_MS,
} from "../context/auth_context";
import { useAuthContext } from "../context/auth_context";
export const AuthSuccessful = () => {
const {
username,
userId,
sessionOver,
signedIn,
setSignedIn,
setUserId,
setUsername,
setSessionOver,
} = useAuthContext();
const { player, setPlayer, signedIn, setSignedIn, setSessionOver } =
useAuthContext();
useEffect(() => {
fetch("/api/player/me", {
fetch("/api/player/token/me", {
credentials: "same-origin",
})
.then((r) => r.json())
.then((player) => {
.then(({ player, expiration }) => {
setSignedIn(!!player);
setUserId(player.id);
setUsername(player.username);
setPlayer(player);
setSessionOver(expiration);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (signedIn) {
@ -35,7 +25,7 @@ export const AuthSuccessful = () => {
<>
<h1>Authentication Successful</h1>
<div>
<span>Hello there, {username || ""}! </span>
<span>Hello there, {player?.username || ""}! </span>
<Link to="/home" className="button">
Go Home{" "}
</Link>

View File

@ -4,7 +4,10 @@ import { Link } from "react-router-dom";
import { useAuthContext } from "../context/auth_context";
export const Home = () => {
const { username, signedIn } = useAuthContext();
const {
player: { username },
signedIn,
} = useAuthContext();
if (signedIn) {
const sshConfig = `Host chessh

View File

@ -67,7 +67,7 @@ const AddKeyButton = ({ onSave }) => {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: key.value,
key: key.value.trim(),
name: name.value,
}),
})
@ -77,7 +77,6 @@ const AddKeyButton = ({ onSave }) => {
if (onSave) {
onSave();
}
setDefaults();
close();
} else if (d.errors) {
if (typeof d.errors === "object") {
@ -158,7 +157,9 @@ const AddKeyButton = ({ onSave }) => {
};
export const Keys = () => {
const { userId } = useAuthContext();
const {
player: { id: userId },
} = useAuthContext();
const [keys, setKeys] = useState(null);
const refreshKeys = useCallback(
@ -175,9 +176,8 @@ export const Keys = () => {
}
}, [userId, refreshKeys]);
if (!keys) {
return <p>Loading...</p>;
}
if (!keys) return <p>Loading...</p>;
if (Array.isArray(keys)) {
return (
<>

View File

@ -35,16 +35,23 @@ defmodule Chessh.SSH.Client.Game do
def init([
%State{
color: color,
game: %Game{dark_player_id: dark_player_id, light_player_id: light_player_id}
game: %Game{dark_player_id: dark_player_id, light_player_id: light_player_id},
player_session: %{player_id: player_id}
} = state
| tail
])
when is_nil(color) do
{is_dark, is_light} = {player_id == dark_player_id, player_id == light_player_id}
new_state =
if is_dark || is_light do
%State{state | color: if(is_light, do: :light, else: :dark)}
else
case {is_nil(dark_player_id), is_nil(light_player_id)} do
{true, false} -> %State{state | color: :dark}
{false, true} -> %State{state | color: :light}
{_, _} -> %State{state | color: Enum.random([:light, :dark])}
{_, _} -> %State{state | color: :light}
end
end
init([new_state | tail])
@ -89,18 +96,22 @@ defmodule Chessh.SSH.Client.Game do
end
binbo_pid = initialize_game(game_id, fen)
send(client_pid, {:send_to_ssh, Utils.clear_codes()})
new_game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player])
new_state = %State{
player_color =
if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark)
send(client_pid, {:send_to_ssh, Utils.clear_codes()})
{:ok,
%State{
state
| binbo_pid: binbo_pid,
color: if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark),
game: new_game
}
{:ok, new_state}
color: player_color,
game: new_game,
flipped: player_color == :dark
}}
end
def init([

View File

@ -39,21 +39,8 @@ defmodule Chessh.Web.Endpoint do
{status, body} =
create_player_from_github_response(resp, github_user_api_url, github_user_agent)
case body do
%{jwt: token} ->
client_redirect_location =
Application.get_env(:chessh, Web)[:client_redirect_after_successful_sign_in]
conn
|> put_resp_cookie("jwt", token)
|> put_resp_header("location", client_redirect_location)
|> send_resp(301, '')
_ ->
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
|> assign_jwt_and_redirect_or_encode(status, body)
end
put "/player/password" do
@ -115,8 +102,7 @@ defmodule Chessh.Web.Endpoint do
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
|> assign_jwt_and_redirect_or_encode(status, body)
end
get "/player/logout" do
@ -171,12 +157,15 @@ defmodule Chessh.Web.Endpoint do
|> send_resp(status, Jason.encode!(body))
end
get "/player/me" do
player = get_player_from_jwt(conn)
get "/player/token/me" do
{:ok, jwt} = Token.verify_and_validate(get_jwt(conn))
%{"uid" => player_id, "exp" => expiration} = jwt
player = Repo.get(Player, player_id)
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(player))
|> send_resp(200, Jason.encode!(%{player: player, expiration: expiration * 1000}))
end
get "/player/:id/keys" do
@ -242,19 +231,39 @@ defmodule Chessh.Web.Endpoint do
)
end
defp get_player_from_jwt(conn) do
defp get_jwt(conn) do
auth_header =
Enum.find_value(conn.req_headers, fn {header, value} ->
if header === "authorization", do: value
end)
jwt = if auth_header, do: auth_header, else: Map.get(fetch_cookies(conn).cookies, "jwt")
if auth_header, do: auth_header, else: Map.get(fetch_cookies(conn).cookies, "jwt")
end
{:ok, %{"uid" => uid}} = Token.verify_and_validate(jwt)
defp get_player_from_jwt(conn) do
{:ok, %{"uid" => uid}} = Token.verify_and_validate(get_jwt(conn))
Repo.get(Player, uid)
end
defp assign_jwt_and_redirect_or_encode(conn, status, body) do
case body do
%{jwt: token} ->
client_redirect_location =
Application.get_env(:chessh, Web)[:client_redirect_after_successful_sign_in]
conn
|> put_resp_cookie("jwt", token)
|> put_resp_header("location", client_redirect_location)
|> send_resp(301, '')
_ ->
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
end
defp create_player_from_github_response(resp, github_user_api_url, github_user_agent) do
case resp do
%{"access_token" => access_token} ->