diff options
Diffstat (limited to 'front/src/routes')
-rw-r--r-- | front/src/routes/auth_successful.jsx | 47 | ||||
-rw-r--r-- | front/src/routes/demo.jsx | 61 | ||||
-rw-r--r-- | front/src/routes/home.jsx | 62 | ||||
-rw-r--r-- | front/src/routes/keys.jsx | 204 | ||||
-rw-r--r-- | front/src/routes/password.jsx | 144 |
5 files changed, 518 insertions, 0 deletions
diff --git a/front/src/routes/auth_successful.jsx b/front/src/routes/auth_successful.jsx new file mode 100644 index 0000000..7c66587 --- /dev/null +++ b/front/src/routes/auth_successful.jsx @@ -0,0 +1,47 @@ +import { useEffect } from "react"; +import { Link } from "react-router-dom"; + +import { useAuthContext } from "../context/auth_context"; + +export const AuthSuccessful = () => { + const { player, setPlayer, signedIn, setSignedIn, setSessionOver } = + useAuthContext(); + + useEffect(() => { + fetch("/api/player/token/me", { + credentials: "same-origin", + }) + .then((r) => r.json()) + .then(({ player, expiration }) => { + setSignedIn(!!player); + setPlayer(player); + setSessionOver(expiration); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (signedIn) { + return ( + <> + <h3>Hello there, {player?.username || ""}! </h3> + <div> + <span> If you have not already done so: </span> + <Link to="/keys" className="button"> + Add a Public Key + </Link> + </div> + <br /> + <div> + <Link to="/home" className="button"> + Go Home + </Link> + </div> + </> + ); + } + return ( + <> + <p>Loading...</p> + </> + ); +}; diff --git a/front/src/routes/demo.jsx b/front/src/routes/demo.jsx new file mode 100644 index 0000000..b1a2f88 --- /dev/null +++ b/front/src/routes/demo.jsx @@ -0,0 +1,61 @@ +import { useEffect, useRef, useState } from "react"; +import { Link } from "react-router-dom"; + +import * as AsciinemaPlayer from "asciinema-player"; +import "asciinema-player/dist/bundle/asciinema-player.css"; + +const demoProps = { + theme: "tango", + startAt: 12, + autoPlay: true, +}; + +const demoCast = "/chessh.cast"; +const demoCastElementId = "demo"; + +export const Demo = () => { + const player = useRef(null); + const [renderedPlayer, setRenderedPlayer] = useState(false); + + useEffect(() => { + if (!renderedPlayer) { + AsciinemaPlayer.create( + demoCast, + document.getElementById(demoCastElementId), + demoProps + ); + setRenderedPlayer(true); + } + }, [renderedPlayer, player]); + + return ( + <div className="demo-container"> + <h1> + Welcome to <span style={{ color: "green" }}>> CheSSH!</span> + </h1> + <div className="flex-row-around"> + <p> + CheSSH is a multiplayer, scalable, free, open source, and (optionally) + passwordless game of Chess over the SSH protocol, written in Elixir. + </p> + <a + className="button gold" + href="https://github.com/Simponic/chessh" + target="_blank" + rel="noreferrer" + > + 🌟 Star Repo 🌟 + </a> + </div> + <hr /> + <div ref={player} id={demoCastElementId} /> + <hr /> + <div className="flex-row-around"> + <h3>Would you like to play a game?</h3> + <Link className="button" to="/home"> + Yes, Falken ⇒ + </Link> + </div> + </div> + ); +}; diff --git a/front/src/routes/home.jsx b/front/src/routes/home.jsx new file mode 100644 index 0000000..baccb5f --- /dev/null +++ b/front/src/routes/home.jsx @@ -0,0 +1,62 @@ +import { CopyBlock, dracula } from "react-code-blocks"; +import { Link } from "react-router-dom"; + +import { useAuthContext } from "../context/auth_context"; + +export const Home = () => { + const { player, signedIn } = useAuthContext(); + + if (signedIn) { + const sshConfig = `Host chessh + Hostname ${process.env.REACT_APP_SSH_SERVER} + Port ${process.env.REACT_APP_SSH_PORT} + User ${player?.username} + PubkeyAuthentication yes`; + return ( + <> + <h2>Welcome, {player?.username}</h2> + <hr /> + <h3>Getting Started</h3> + <ol> + <div> + <li> + Add a <Link to="/keys">public key</Link>, or{" "} + <Link to="/password">set a password</Link>. + </li> + </div> + <div> + <li> + Insert the following block in your{" "} + <a href="https://linux.die.net/man/5/ssh_config">ssh config</a>: + </li> + + <CopyBlock + theme={dracula} + text={sshConfig} + showLineNumbers={true} + wrapLines + codeBlock + /> + </div> + + <div> + <li>Then, connect with:</li> + <CopyBlock + theme={dracula} + text={"ssh -t chessh"} + language={"shell"} + showLineNumbers={false} + codeBlock + /> + </div> + </ol> + </> + ); + } + + return ( + <> + <p>Looks like you're not signed in 👀. </p> + </> + ); +}; diff --git a/front/src/routes/keys.jsx b/front/src/routes/keys.jsx new file mode 100644 index 0000000..4dee1ce --- /dev/null +++ b/front/src/routes/keys.jsx @@ -0,0 +1,204 @@ +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; + if (n >= MINIMIZE_KEY_LEN) { + const half = Math.floor(MINIMIZE_KEY_LEN / 2); + return key.substring(0, half) + "..." + key.substring(n - half, n); + } + return key; +}; + +const KeyCard = ({ onDelete, props }) => { + const { id, name, key } = props; + + const deleteThisKey = () => { + if ( + window.confirm( + "Are you sure? This will close all your current ssh sessions." + ) + ) { + fetch(`/api/keys/${id}`, { + credentials: "same-origin", + method: "DELETE", + }) + .then((r) => r.json()) + .then((d) => d.success && onDelete && onDelete()); + } + }; + + return ( + <div className="key-card"> + <h4 style={{ flex: 1 }}>{name}</h4> + <p style={{ flex: 4 }}>{minimizeKey(key)}</p> + + <button + style={{ flex: 0 }} + className="button red" + onClick={deleteThisKey} + > + Delete + </button> + </div> + ); +}; + +const AddKeyButton = ({ onSave }) => { + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [key, setKey] = useState(""); + const [errors, setErrors] = useState(null); + + const setDefaults = () => { + setName(""); + setKey(""); + setErrors(null); + }; + + const close = () => { + setDefaults(); + setOpen(false); + }; + + const createKey = () => { + fetch(`/api/player/keys`, { + credentials: "same-origin", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + key: key.trim(), + name: name.trim(), + }), + }) + .then((r) => r.json()) + .then((d) => { + if (d.success) { + if (onSave) { + onSave(); + } + 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 ( + <div> + <button className="button" onClick={() => setOpen(true)}> + + Add Key + </button> + <Modal + isOpen={open} + onRequestClose={close} + className="modal" + contentLabel="Add Key" + > + <div> + <h3>Add SSH Key</h3> + <p> + Not sure about this? Check{" "} + <a + href="https://www.ssh.com/academy/ssh/keygen" + target="_blank" + rel="noreferrer" + > + here + </a>{" "} + for help! + </p> + <hr /> + <p>Key Name *</p> + <input + value={name} + onChange={(e) => setName(e.target.value)} + required + /> + </div> + <div> + <p>SSH Key *</p> + <textarea + cols={40} + rows={5} + value={key} + onChange={(e) => setKey(e.target.value)} + required + /> + </div> + <div> + {errors && ( + <div style={{ color: "red" }}> + {errors.map((error, i) => ( + <p key={i}>{error}</p> + ))} + </div> + )} + </div> + <div className="flex-end-row"> + <button className="button" onClick={createKey}> + Add + </button> + <button className="button red" onClick={close}> + Cancel + </button> + </div> + </Modal> + </div> + ); +}; + +export const Keys = () => { + const { + player: { id: userId }, + } = useAuthContext(); + const [keys, setKeys] = useState(null); + + const refreshKeys = useCallback( + () => + fetch(`/api/player/${userId}/keys`) + .then((r) => r.json()) + .then((keys) => setKeys(keys)), + [userId] + ); + + useEffect(() => { + if (userId) { + refreshKeys(); + } + }, [userId, refreshKeys]); + + if (!keys) return <p>Loading...</p>; + + if (Array.isArray(keys)) { + return ( + <> + <h2>My Keys</h2> + <AddKeyButton onSave={refreshKeys} /> + <div className="key-card-collection"> + {keys.length ? ( + keys.map((key) => ( + <KeyCard key={key.id} onDelete={refreshKeys} props={key} /> + )) + ) : ( + <p>Looks like you've got no keys, try adding some!</p> + )} + </div> + </> + ); + } +}; diff --git a/front/src/routes/password.jsx b/front/src/routes/password.jsx new file mode 100644 index 0000000..11fb775 --- /dev/null +++ b/front/src/routes/password.jsx @@ -0,0 +1,144 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; + +export const Password = () => { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState(null); + const [success, setSuccess] = useState(false); + + const resetFields = () => { + setErrors(null); + setPassword(""); + setConfirmPassword(""); + }; + + const reset = () => { + resetFields(); + setSuccess(false); + }; + + const deletePassword = () => { + if ( + window.confirm( + "Are you sure? This will close all your current ssh sessions." + ) + ) { + fetch(`/api/player/token/password`, { + method: "DELETE", + credentials: "same-origin", + }) + .then((r) => r.json()) + .then((r) => { + if (r.success) { + resetFields(); + setSuccess(true); + } + }); + } + }; + + const submitPassword = () => { + if ( + window.confirm( + "Are you sure? This will close all your current ssh sessions." + ) + ) { + fetch(`/api/player/token/password`, { + method: "PUT", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password, + password_confirmation: confirmPassword, + }), + }) + .then((r) => r.json()) + .then((p) => { + if (p.success) { + resetFields(); + setSuccess(true); + } else if (p.errors) { + if (typeof p.errors === "object") { + setErrors( + Object.keys(p.errors).map( + (field) => `${field}: ${p.errors[field].join(",")}` + ) + ); + } else { + setErrors([p.errors]); + } + } + }); + } + }; + + return ( + <> + <div> + <h3>Update SSH Password</h3> + <p> + An SSH password allows you to connect from any device. However, it is + inherently less secure than a <Link to="/keys">public key</Link>. + </p> + <p>Use a password at your own risk.</p> + </div> + <hr /> + <div> + <h4> Previously set a password and no longer want it? </h4> + <button className="button red" onClick={deletePassword}> + Delete Password + </button> + </div> + <div> + <h4>Or if you're dead set on it...</h4> + <div> + <p>Password *</p> + <input + value={password} + onChange={(e) => setPassword(e.target.value)} + type="password" + required + /> + </div> + <div> + <p>Confirm Password *</p> + <input + value={confirmPassword} + type="password" + onChange={(e) => setConfirmPassword(e.target.value)} + required + /> + </div> + <div> + {errors && ( + <div style={{ color: "red" }}> + {errors.map((error, i) => ( + <p key={i}>{error}</p> + ))} + </div> + )} + </div> + + <div + className="flex-end-row" + style={{ justifyContent: "start", marginTop: "1rem" }} + > + <button className="button" onClick={submitPassword}> + Submit + </button> + <button className="button gold" onClick={reset}> + Reset Form + </button> + </div> + </div> + + <br /> + <div> + {success && <div style={{ color: "green" }}>Password updated</div>} + </div> + </> + ); +}; |