diff --git a/config/config.exs b/config/config.exs index beda8bd..ef3c828 100644 --- a/config/config.exs +++ b/config/config.exs @@ -15,7 +15,8 @@ config :chessh, RateLimits, jail_attempt_threshold: 15, max_concurrent_user_sessions: 5, player_session_message_burst_ms: 500, - player_session_message_burst_rate: 8 + player_session_message_burst_rate: 8, + player_public_keys: 15 config :chessh, Web, port: 8080, diff --git a/front/package-lock.json b/front/package-lock.json index e5e7790..fd66ab3 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -15,6 +15,7 @@ "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", "react-dom": "^18.2.0", + "react-modal": "^3.16.1", "react-router-dom": "^6.6.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" @@ -7836,6 +7837,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -14533,6 +14539,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -16620,6 +16649,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -23111,6 +23148,11 @@ "strip-final-newline": "^2.0.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -27768,6 +27810,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -29304,6 +29362,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/front/package.json b/front/package.json index 0406673..f7d9d5d 100644 --- a/front/package.json +++ b/front/package.json @@ -10,6 +10,7 @@ "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", "react-dom": "^18.2.0", + "react-modal": "^3.16.1", "react-router-dom": "^6.6.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" diff --git a/front/src/context/auth_context.js b/front/src/context/auth_context.js index 7318671..bdf789c 100644 --- a/front/src/context/auth_context.js +++ b/front/src/context/auth_context.js @@ -1,4 +1,4 @@ -import React, { useContext, useState, createContext, useEffect } from "react"; +import React, { useContext, useState, createContext } from "react"; export const DEFAULT_EXPIRY_TIME_MS = 12 * 60 * 60 * 1000; @@ -21,63 +21,6 @@ export const AuthProvider = ({ children }) => { const [userId, setUserId] = useState(null); const [username, setUsername] = useState(null); - useEffect(() => { - if (userId) { - localStorage.setItem("userId", userId.toString()); - } - }, [userId]); - - useEffect(() => { - if (username) { - localStorage.setItem("username", username); - } - }, [username]); - - useEffect(() => { - let expiry = localStorage.getItem("expiry"); - if (expiry) { - expiry = new Date(expiry); - if (Date.now() < expiry.getTime()) { - setSignedIn(true); - setSessionOver(expiry); - // We don't have access to the JWT token as it is an HTTP only cookie - - // so we store user info in local storage - ((username) => { - if (username) { - setUsername(username); - } - })(localStorage.getItem("username")); - - ((id) => { - if (id) { - setUserId(parseInt(id, 10)); - } - })(localStorage.getItem("userId")); - } - } - }, []); - - useEffect(() => { - localStorage.setItem("expiry", sessionOver.toISOString()); - setTimeout(() => { - setSessionOver((sessionOver) => { - if (Date.now() >= sessionOver.getTime()) { - setSignedIn((signedIn) => { - if (signedIn) { - alert( - "Session expired. Any further privileged requests will fail until signed in again." - ); - ["userId", "userName"].map((x) => localStorage.removeItem(x)); - return false; - } - return signedIn; - }); - } - return sessionOver; - }); - }, sessionOver.getTime() - Date.now()); - }, [sessionOver]); - return ( { const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext(); @@ -45,15 +45,7 @@ export const Root = () => { ) : ( <> - - setSessionOver( - new Date(Date.now() + DEFAULT_EXPIRY_TIME_MS) - ) - } - href={process.env.REACT_APP_GITHUB_OAUTH} - className="button" - > + 🐙 Login w/ GitHub 🐙 diff --git a/front/src/routes/auth_successful.jsx b/front/src/routes/auth_successful.jsx index 6dbc9f5..1d5a9c2 100644 --- a/front/src/routes/auth_successful.jsx +++ b/front/src/routes/auth_successful.jsx @@ -1,29 +1,34 @@ import { useEffect, useCallback } from "react"; import { Link } from "react-router-dom"; -import { useAuthContext } from "../context/auth_context"; +import { + useAuthContext, + DEFAULT_EXPIRY_TIME_MS, +} from "../context/auth_context"; export const AuthSuccessful = () => { - const { username, signedIn, setSignedIn, setUserId, setUsername } = - useAuthContext(); - - const fetchMyself = useCallback( - () => - fetch("/api/player/me", { - credentials: "same-origin", - }) - .then((r) => r.json()) - .then((player) => { - setSignedIn(!!player); - setUserId(player.id); - setUsername(player.username); - }), - [setSignedIn, setUserId, setUsername] - ); + const { + username, + userId, + sessionOver, + signedIn, + setSignedIn, + setUserId, + setUsername, + setSessionOver, + } = useAuthContext(); useEffect(() => { - fetchMyself(); - }, [fetchMyself]); + fetch("/api/player/me", { + credentials: "same-origin", + }) + .then((r) => r.json()) + .then((player) => { + setSignedIn(!!player); + setUserId(player.id); + setUsername(player.username); + }); + }, []); if (signedIn) { return ( diff --git a/front/src/routes/demo.jsx b/front/src/routes/demo.jsx index 951ed91..b1a2f88 100644 --- a/front/src/routes/demo.jsx +++ b/front/src/routes/demo.jsx @@ -35,8 +35,8 @@ export const Demo = () => {

- CheSSH is a multiplayer, scalable, free, open source, and potentially - passwordless game of Chess over the SSH protocol. + CheSSH is a multiplayer, scalable, free, open source, and (optionally) + passwordless game of Chess over the SSH protocol, written in Elixir.

{ target="_blank" rel="noreferrer" > - 🌟 Star 🌟 + 🌟 Star Repo 🌟

@@ -53,7 +53,7 @@ export const Demo = () => {

Would you like to play a game?

- Yes, Joshua ⇒ + Yes, Falken ⇒
diff --git a/front/src/routes/home.jsx b/front/src/routes/home.jsx index 92c9b39..c8e804f 100644 --- a/front/src/routes/home.jsx +++ b/front/src/routes/home.jsx @@ -9,8 +9,8 @@ export const Home = () => { if (signedIn) { const sshConfig = `Host chessh Hostname ${process.env.REACT_APP_SSH_SERVER} - User ${username} Port ${process.env.REACT_APP_SSH_PORT} + User ${username} PubkeyAuthentication yes`; return ( <> @@ -37,10 +37,10 @@ export const Home = () => { />
-
  • And connect with:
  • +
  • Then, connect with:
  • { return (

    CheSSH

    +

    Hello!

    +

    Looks like you're not signed in 👀.

    +

    Please link your GitHub account above!

    ); }; diff --git a/front/src/routes/keys.jsx b/front/src/routes/keys.jsx index ee34143..4f6c505 100644 --- a/front/src/routes/keys.jsx +++ b/front/src/routes/keys.jsx @@ -1,6 +1,9 @@ -import { useEffect, useState } from "react"; +import Modal from "react-modal"; +import { useEffect, useState, useCallback } from "react"; import { useAuthContext } from "../context/auth_context"; +Modal.setAppElement("#root"); + const MINIMIZE_KEY_LEN = 40; const minimizeKey = (key) => { const n = key.length; @@ -11,7 +14,7 @@ const minimizeKey = (key) => { return key; }; -const KeyCard = ({ props }) => { +const KeyCard = ({ onDelete, props }) => { const { id, name, key } = props; const deleteThisKey = () => { @@ -20,25 +23,41 @@ const KeyCard = ({ props }) => { method: "DELETE", }) .then((r) => r.json()) - .then((d) => d.success); //&& onDelete()); + .then((d) => d.success && onDelete && onDelete()); }; return (
    -

    {name}

    -

    {minimizeKey(key)}

    +

    {name}

    +

    {minimizeKey(key)}

    -
    ); }; -const AddKey = () => { - const [key, setKey] = useState(""); - const [name, setName] = useState(""); - const [error, setError] = useState(""); +const AddKeyButton = ({ onSave }) => { + const [open, setOpen] = useState(false); + const [name, setName] = useState({ value: "", error: "" }); + const [key, setKey] = useState({ value: "", error: "" }); + const [errors, setErrors] = useState(null); + + const setDefaults = () => { + setName({ value: "", error: "" }); + setKey({ value: "", error: "" }); + setErrors(null); + }; + + const close = () => { + setDefaults(); + setOpen(false); + }; const createKey = () => { fetch(`/api/player/keys`, { @@ -48,28 +67,92 @@ const AddKey = () => { "Content-Type": "application/json", }, body: JSON.stringify({ - key, - name, + key: key.value, + name: name.value, }), }) .then((r) => r.json()) .then((d) => { if (d.success) { - setName(""); - setKey(""); - } else { - setError(d.errors); + if (onSave) { + onSave(); + } + setDefaults(); + close(); + } else if (d.errors) { + if (typeof d.errors === "object") { + setErrors( + Object.keys(d.errors).map( + (field) => `${field}: ${d.errors[field].join(",")}` + ) + ); + } else { + setErrors([d.errors]); + } } }); }; return ( -
    - setName(e.target.value)} /> -