Web Client #11
@ -1,25 +1,57 @@
|
|||||||
import React, { useContext, useState, createContext } from "react";
|
import { useEffect, useContext, createContext } from "react";
|
||||||
|
import { useLocalStorage } from "../hooks/useLocalStorage";
|
||||||
export const DEFAULT_EXPIRY_TIME_MS = 12 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
const AuthContext = createContext({
|
const AuthContext = createContext({
|
||||||
signedIn: false,
|
signedIn: false,
|
||||||
setSignedIn: () => null,
|
setSignedIn: () => null,
|
||||||
sessionOver: new Date(),
|
sessionOver: new Date(),
|
||||||
setSessionOver: () => null,
|
setSessionOver: () => null,
|
||||||
userId: null,
|
setPlayer: () => null,
|
||||||
setUserId: () => null,
|
player: null,
|
||||||
username: "",
|
signOut: () => null,
|
||||||
setUsername: () => null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAuthContext = () => useContext(AuthContext);
|
export const useAuthContext = () => useContext(AuthContext);
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [signedIn, setSignedIn] = useState(false);
|
const [signedIn, setSignedIn] = useLocalStorage("signedIn", false);
|
||||||
const [sessionOver, setSessionOver] = useState(new Date());
|
const [sessionOver, setSessionOver] = useLocalStorage(
|
||||||
const [userId, setUserId] = useState(null);
|
"sessionOver",
|
||||||
const [username, setUsername] = useState(null);
|
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 (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
@ -28,10 +60,9 @@ export const AuthProvider = ({ children }) => {
|
|||||||
setSignedIn,
|
setSignedIn,
|
||||||
sessionOver,
|
sessionOver,
|
||||||
setSessionOver,
|
setSessionOver,
|
||||||
userId,
|
signOut,
|
||||||
setUserId,
|
setPlayer,
|
||||||
username,
|
player,
|
||||||
setUsername,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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";
|
import { useAuthContext } from "./context/auth_context";
|
||||||
|
|
||||||
export const Root = () => {
|
export const Root = () => {
|
||||||
const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext();
|
const { signedIn, setUserId, setSignedIn, setSessionOver, signOut } =
|
||||||
|
useAuthContext();
|
||||||
const signOut = () => {
|
|
||||||
fetch("/api/player/logout", {
|
|
||||||
method: "GET",
|
|
||||||
credentials: "same-origin",
|
|
||||||
}).then(() => {
|
|
||||||
setSignedIn(false);
|
|
||||||
setUserId(null);
|
|
||||||
setSessionOver(new Date());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,33 +1,23 @@
|
|||||||
import { useEffect, useCallback } from "react";
|
import { useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import { useAuthContext } from "../context/auth_context";
|
||||||
useAuthContext,
|
|
||||||
DEFAULT_EXPIRY_TIME_MS,
|
|
||||||
} from "../context/auth_context";
|
|
||||||
|
|
||||||
export const AuthSuccessful = () => {
|
export const AuthSuccessful = () => {
|
||||||
const {
|
const { player, setPlayer, signedIn, setSignedIn, setSessionOver } =
|
||||||
username,
|
useAuthContext();
|
||||||
userId,
|
|
||||||
sessionOver,
|
|
||||||
signedIn,
|
|
||||||
setSignedIn,
|
|
||||||
setUserId,
|
|
||||||
setUsername,
|
|
||||||
setSessionOver,
|
|
||||||
} = useAuthContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/player/me", {
|
fetch("/api/player/token/me", {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((player) => {
|
.then(({ player, expiration }) => {
|
||||||
setSignedIn(!!player);
|
setSignedIn(!!player);
|
||||||
setUserId(player.id);
|
setPlayer(player);
|
||||||
setUsername(player.username);
|
setSessionOver(expiration);
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
@ -35,7 +25,7 @@ export const AuthSuccessful = () => {
|
|||||||
<>
|
<>
|
||||||
<h1>Authentication Successful</h1>
|
<h1>Authentication Successful</h1>
|
||||||
<div>
|
<div>
|
||||||
<span>Hello there, {username || ""}! </span>
|
<span>Hello there, {player?.username || ""}! </span>
|
||||||
<Link to="/home" className="button">
|
<Link to="/home" className="button">
|
||||||
Go Home{" "}
|
Go Home{" "}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -4,7 +4,10 @@ import { Link } from "react-router-dom";
|
|||||||
import { useAuthContext } from "../context/auth_context";
|
import { useAuthContext } from "../context/auth_context";
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const { username, signedIn } = useAuthContext();
|
const {
|
||||||
|
player: { username },
|
||||||
|
signedIn,
|
||||||
|
} = useAuthContext();
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
const sshConfig = `Host chessh
|
const sshConfig = `Host chessh
|
||||||
|
@ -67,7 +67,7 @@ const AddKeyButton = ({ onSave }) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key: key.value,
|
key: key.value.trim(),
|
||||||
name: name.value,
|
name: name.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@ -77,7 +77,6 @@ const AddKeyButton = ({ onSave }) => {
|
|||||||
if (onSave) {
|
if (onSave) {
|
||||||
onSave();
|
onSave();
|
||||||
}
|
}
|
||||||
setDefaults();
|
|
||||||
close();
|
close();
|
||||||
} else if (d.errors) {
|
} else if (d.errors) {
|
||||||
if (typeof d.errors === "object") {
|
if (typeof d.errors === "object") {
|
||||||
@ -158,7 +157,9 @@ const AddKeyButton = ({ onSave }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Keys = () => {
|
export const Keys = () => {
|
||||||
const { userId } = useAuthContext();
|
const {
|
||||||
|
player: { id: userId },
|
||||||
|
} = useAuthContext();
|
||||||
const [keys, setKeys] = useState(null);
|
const [keys, setKeys] = useState(null);
|
||||||
|
|
||||||
const refreshKeys = useCallback(
|
const refreshKeys = useCallback(
|
||||||
@ -175,9 +176,8 @@ export const Keys = () => {
|
|||||||
}
|
}
|
||||||
}, [userId, refreshKeys]);
|
}, [userId, refreshKeys]);
|
||||||
|
|
||||||
if (!keys) {
|
if (!keys) return <p>Loading...</p>;
|
||||||
return <p>Loading...</p>;
|
|
||||||
}
|
|
||||||
if (Array.isArray(keys)) {
|
if (Array.isArray(keys)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -35,16 +35,23 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
def init([
|
def init([
|
||||||
%State{
|
%State{
|
||||||
color: color,
|
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
|
} = state
|
||||||
| tail
|
| tail
|
||||||
])
|
])
|
||||||
when is_nil(color) do
|
when is_nil(color) do
|
||||||
|
{is_dark, is_light} = {player_id == dark_player_id, player_id == light_player_id}
|
||||||
|
|
||||||
new_state =
|
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
|
case {is_nil(dark_player_id), is_nil(light_player_id)} do
|
||||||
{true, false} -> %State{state | color: :dark}
|
{true, false} -> %State{state | color: :dark}
|
||||||
{false, true} -> %State{state | color: :light}
|
{false, true} -> %State{state | color: :light}
|
||||||
{_, _} -> %State{state | color: Enum.random([:light, :dark])}
|
{_, _} -> %State{state | color: :light}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
init([new_state | tail])
|
init([new_state | tail])
|
||||||
@ -89,18 +96,22 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
end
|
end
|
||||||
|
|
||||||
binbo_pid = initialize_game(game_id, fen)
|
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_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
|
state
|
||||||
| binbo_pid: binbo_pid,
|
| binbo_pid: binbo_pid,
|
||||||
color: if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark),
|
color: player_color,
|
||||||
game: new_game
|
game: new_game,
|
||||||
}
|
flipped: player_color == :dark
|
||||||
|
}}
|
||||||
{:ok, new_state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def init([
|
def init([
|
||||||
|
@ -39,21 +39,8 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
{status, body} =
|
{status, body} =
|
||||||
create_player_from_github_response(resp, github_user_api_url, github_user_agent)
|
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
|
conn
|
||||||
|> put_resp_cookie("jwt", token)
|
|> assign_jwt_and_redirect_or_encode(status, body)
|
||||||
|> 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
|
end
|
||||||
|
|
||||||
put "/player/password" do
|
put "/player/password" do
|
||||||
@ -115,8 +102,7 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
end
|
end
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/json")
|
|> assign_jwt_and_redirect_or_encode(status, body)
|
||||||
|> send_resp(status, Jason.encode!(body))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/player/logout" do
|
get "/player/logout" do
|
||||||
@ -171,12 +157,15 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
|> send_resp(status, Jason.encode!(body))
|
|> send_resp(status, Jason.encode!(body))
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/player/me" do
|
get "/player/token/me" do
|
||||||
player = get_player_from_jwt(conn)
|
{:ok, jwt} = Token.verify_and_validate(get_jwt(conn))
|
||||||
|
|
||||||
|
%{"uid" => player_id, "exp" => expiration} = jwt
|
||||||
|
player = Repo.get(Player, player_id)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/json")
|
|> put_resp_content_type("application/json")
|
||||||
|> send_resp(200, Jason.encode!(player))
|
|> send_resp(200, Jason.encode!(%{player: player, expiration: expiration * 1000}))
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/player/:id/keys" do
|
get "/player/:id/keys" do
|
||||||
@ -242,19 +231,39 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_player_from_jwt(conn) do
|
defp get_jwt(conn) do
|
||||||
auth_header =
|
auth_header =
|
||||||
Enum.find_value(conn.req_headers, fn {header, value} ->
|
Enum.find_value(conn.req_headers, fn {header, value} ->
|
||||||
if header === "authorization", do: value
|
if header === "authorization", do: value
|
||||||
end)
|
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)
|
Repo.get(Player, uid)
|
||||||
end
|
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
|
defp create_player_from_github_response(resp, github_user_api_url, github_user_agent) do
|
||||||
case resp do
|
case resp do
|
||||||
%{"access_token" => access_token} ->
|
%{"access_token" => access_token} ->
|
||||||
|
Loading…
Reference in New Issue
Block a user