Web Client #11
@ -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}
|
||||
|
28
front/src/hooks/useLocalStorage.js
Normal file
28
front/src/hooks/useLocalStorage.js
Normal 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
|
||||
);
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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 =
|
||||
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])}
|
||||
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: :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{
|
||||
state
|
||||
| binbo_pid: binbo_pid,
|
||||
color: if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark),
|
||||
game: new_game
|
||||
}
|
||||
player_color =
|
||||
if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark)
|
||||
|
||||
{:ok, new_state}
|
||||
send(client_pid, {:send_to_ssh, Utils.clear_codes()})
|
||||
|
||||
{:ok,
|
||||
%State{
|
||||
state
|
||||
| binbo_pid: binbo_pid,
|
||||
color: player_color,
|
||||
game: new_game,
|
||||
flipped: player_color == :dark
|
||||
}}
|
||||
end
|
||||
|
||||
def init([
|
||||
|
@ -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
|
||||
conn
|
||||
|> 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} ->
|
||||
|
Loading…
Reference in New Issue
Block a user