summaryrefslogtreecommitdiff
path: root/front/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'front/src/routes')
-rw-r--r--front/src/routes/auth_successful.jsx47
-rw-r--r--front/src/routes/demo.jsx61
-rw-r--r--front/src/routes/home.jsx62
-rw-r--r--front/src/routes/keys.jsx204
-rw-r--r--front/src/routes/password.jsx144
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>
+ </>
+ );
+};