Web Client #11
@ -8,3 +8,7 @@ GITHUB_CLIENT_SECRET=
|
|||||||
GITHUB_USER_AGENT=
|
GITHUB_USER_AGENT=
|
||||||
|
|
||||||
JWT_SECRET=aVerySecretJwtSigningSecret
|
JWT_SECRET=aVerySecretJwtSigningSecret
|
||||||
|
|
||||||
|
SSH_PORT=42069
|
||||||
|
REACT_APP_SSH_SERVER=localhost
|
||||||
|
REACT_APP_SSH_PORT=42069
|
@ -7,7 +7,6 @@ config :hammer,
|
|||||||
config :chessh,
|
config :chessh,
|
||||||
ecto_repos: [Chessh.Repo],
|
ecto_repos: [Chessh.Repo],
|
||||||
key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
|
key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
|
||||||
port: 42_069,
|
|
||||||
max_sessions: 255,
|
max_sessions: 255,
|
||||||
ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json")
|
ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json")
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
config :chessh,
|
||||||
|
port: String.to_integer(System.get_env("SSH_PORT", "42069"))
|
||||||
|
|
||||||
config :chessh, Web,
|
config :chessh, Web,
|
||||||
github_client_id: System.get_env("GITHUB_CLIENT_ID"),
|
github_client_id: System.get_env("GITHUB_CLIENT_ID"),
|
||||||
github_client_secret: System.get_env("GITHUB_CLIENT_SECRET"),
|
github_client_secret: System.get_env("GITHUB_CLIENT_SECRET"),
|
||||||
|
686
front/package-lock.json
generated
686
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-code-blocks": "^0.0.9-0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
@ -21,13 +21,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [userId, setUserId] = useState(null);
|
const [userId, setUserId] = useState(null);
|
||||||
const [username, setUsername] = useState(null);
|
const [username, setUsername] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!signedIn) {
|
|
||||||
setUsername(null);
|
|
||||||
setUserId(null);
|
|
||||||
}
|
|
||||||
}, [signedIn]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
localStorage.setItem("userId", userId.toString());
|
localStorage.setItem("userId", userId.toString());
|
||||||
|
@ -27,6 +27,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--main-bg-color);
|
color: var(--main-bg-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -34,6 +35,8 @@ body {
|
|||||||
border: var(--primary-text-color) solid 1px;
|
border: var(--primary-text-color) solid 1px;
|
||||||
background-color: var(--success-color);
|
background-color: var(--success-color);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
font-family: "DM Mono";
|
||||||
}
|
}
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background-color: var(--success-color-hover);
|
background-color: var(--success-color-hover);
|
||||||
@ -44,6 +47,13 @@ body {
|
|||||||
.gold:hover {
|
.gold:hover {
|
||||||
background-color: var(--gold-color-hover);
|
background-color: var(--gold-color-hover);
|
||||||
}
|
}
|
||||||
|
.red {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
background-color: var(--red-color);
|
||||||
|
}
|
||||||
|
.red:hover {
|
||||||
|
background-color: var(--red-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
@ -63,17 +73,20 @@ body {
|
|||||||
border: var(--purple-color) solid 1px;
|
border: var(--purple-color) solid 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
a:hover {
|
||||||
color: var(--success-color-hover);
|
background-color: var(--success-color-hover);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -119,3 +132,23 @@ body {
|
|||||||
-webkit-transform: translateX(-50%) translateY(-50%);
|
-webkit-transform: translateX(-50%) translateY(-50%);
|
||||||
transform: translateX(-50%) translateY(-50%);
|
transform: translateX(-50%) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: solid 1px var(--gold-color);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 680px) {
|
||||||
|
.container {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { AuthProvider } from "./context/auth_context";
|
|||||||
import { Root } from "./root";
|
import { Root } from "./root";
|
||||||
import { Demo } from "./routes/demo";
|
import { Demo } from "./routes/demo";
|
||||||
import { Home } from "./routes/home";
|
import { Home } from "./routes/home";
|
||||||
|
import { Keys } from "./routes/keys";
|
||||||
import { AuthSuccessful } from "./routes/auth_successful";
|
import { AuthSuccessful } from "./routes/auth_successful";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@ -26,13 +27,17 @@ const router = createBrowserRouter([
|
|||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "auth-successful",
|
path: "keys",
|
||||||
element: <AuthSuccessful />,
|
element: <Keys />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "keys",
|
path: "faq",
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "auth-successful",
|
||||||
|
element: <AuthSuccessful />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -5,17 +5,32 @@ import logo from "./assets/chessh_sm.svg";
|
|||||||
import { useAuthContext, DEFAULT_EXPIRY_TIME_MS } from "./context/auth_context";
|
import { useAuthContext, DEFAULT_EXPIRY_TIME_MS } from "./context/auth_context";
|
||||||
|
|
||||||
export const Root = () => {
|
export const Root = () => {
|
||||||
const { signedIn, setSignedIn, setSessionOver } = useAuthContext();
|
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());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
<div>
|
<div className="flex-row-around">
|
||||||
<Link to="/home">
|
<Link to="/home">
|
||||||
<img src={logo} className="logo" alt="CheSSH Logo" />
|
<img src={logo} className="logo" alt="CheSSH Logo" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav">
|
<div className="nav">
|
||||||
|
<Link className="link" to="/faq">
|
||||||
|
FAQ
|
||||||
|
</Link>
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<>
|
<>
|
||||||
<Link className="link" to="/user">
|
<Link className="link" to="/user">
|
||||||
@ -24,11 +39,7 @@ export const Root = () => {
|
|||||||
<Link className="link" to="/keys">
|
<Link className="link" to="/keys">
|
||||||
Keys
|
Keys
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link className="button" onClick={signOut} to="/">
|
||||||
className="button"
|
|
||||||
onClick={() => setSignedIn(false)}
|
|
||||||
to="/"
|
|
||||||
>
|
|
||||||
Sign Out
|
Sign Out
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { useEffect, useCallback } from "react";
|
import { useEffect, useCallback } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { useAuthContext } from "../context/auth_context";
|
import { useAuthContext } from "../context/auth_context";
|
||||||
|
|
||||||
export const AuthSuccessful = () => {
|
export const AuthSuccessful = () => {
|
||||||
@ -23,10 +25,22 @@ export const AuthSuccessful = () => {
|
|||||||
fetchMyself();
|
fetchMyself();
|
||||||
}, [fetchMyself]);
|
}, [fetchMyself]);
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Authentication Successful</h1>
|
||||||
|
<div>
|
||||||
|
<span>Hello there, {username || ""}! </span>
|
||||||
|
<Link to="/home" className="button">
|
||||||
|
Go Home{" "}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Successful Auth</h1>
|
<p>Loading...</p>
|
||||||
{signedIn ? <p>Hello there, {username || ""}</p> : <p>Loading...</p>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,59 @@
|
|||||||
|
import { CopyBlock, dracula } from "react-code-blocks";
|
||||||
|
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 { username, signedIn } = useAuthContext();
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
const sshConfig = `Host chessh
|
||||||
|
Hostname ${process.env.REACT_APP_SSH_SERVER}
|
||||||
|
User ${username}
|
||||||
|
Port ${process.env.REACT_APP_SSH_PORT}
|
||||||
|
PubkeyAuthentication yes`;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Hello there, {username}!</h2>
|
||||||
|
<p>
|
||||||
|
You can now start playing CheSSH by using any of your imported{" "}
|
||||||
|
<Link to="/keys">public keys</Link>, or by{" "}
|
||||||
|
<Link to="/user">creating a password</Link>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2>Getting Started</h2>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Add the following to your ssh config (normally in ~/.ssh/config):
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<CopyBlock
|
||||||
|
theme={dracula}
|
||||||
|
text={sshConfig}
|
||||||
|
showLineNumbers={true}
|
||||||
|
wrapLines
|
||||||
|
codeBlock
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<li>And connect with:</li>
|
||||||
|
<CopyBlock
|
||||||
|
theme={dracula}
|
||||||
|
text={`ssh -t chessh`}
|
||||||
|
language={"shell"}
|
||||||
|
showLineNumbers={false}
|
||||||
|
codeBlock
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ol>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Welcome home, {signedIn ? username : "guest"}!</h1>
|
<h1>CheSSH</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
104
front/src/routes/keys.jsx
Normal file
104
front/src/routes/keys.jsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuthContext } from "../context/auth_context";
|
||||||
|
|
||||||
|
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 = ({ props }) => {
|
||||||
|
const { id, name, key } = props;
|
||||||
|
|
||||||
|
const deleteThisKey = () => {
|
||||||
|
fetch(`/api/keys/${id}`, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => d.success); //&& onDelete());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="key-card">
|
||||||
|
<h4>{name}</h4>
|
||||||
|
<p>{minimizeKey(key)}</p>
|
||||||
|
|
||||||
|
<button className="button red" onClick={deleteThisKey}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddKey = () => {
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const createKey = () => {
|
||||||
|
fetch(`/api/player/keys`, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => {
|
||||||
|
if (d.success) {
|
||||||
|
setName("");
|
||||||
|
setKey("");
|
||||||
|
} else {
|
||||||
|
setError(d.errors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="key-card">
|
||||||
|
<input onChange={(e) => setName(e.target.value)} />
|
||||||
|
<textarea onChange={(e) => setKey(e.target.value)} />
|
||||||
|
<button className="button gold" onClick={createKey}>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Keys = () => {
|
||||||
|
const { userId } = useAuthContext();
|
||||||
|
const [keys, setKeys] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) {
|
||||||
|
fetch(`/api/player/${userId}/keys`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((keys) => setKeys(keys));
|
||||||
|
}
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (!keys) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
if (Array.isArray(keys)) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddKey />
|
||||||
|
{keys.length ? (
|
||||||
|
keys.map((key) => <KeyCard key={key.id} props={key} />)
|
||||||
|
) : (
|
||||||
|
<p>No keys</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -10,6 +10,11 @@ module.exports = function (app) {
|
|||||||
pathRewrite: (path, _req) => {
|
pathRewrite: (path, _req) => {
|
||||||
return path.replace("/api", "");
|
return path.replace("/api", "");
|
||||||
},
|
},
|
||||||
|
onProxyRes: function (proxyRes, req, res) {
|
||||||
|
proxyRes.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
|
proxyRes.headers["Access-Control-Allow-Methods"] =
|
||||||
|
"GET,PUT,POST,DELETE,PATCH,OPTIONS";
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -118,13 +118,19 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
|> send_resp(status, Jason.encode!(body))
|
|> send_resp(status, Jason.encode!(body))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/player/logout" do
|
||||||
|
conn
|
||||||
|
|> delete_resp_cookie("jwt")
|
||||||
|
|> send_resp(200, Jason.encode!(%{success: true}))
|
||||||
|
end
|
||||||
|
|
||||||
post "/player/keys" do
|
post "/player/keys" do
|
||||||
player = get_player_from_jwt(conn)
|
player = get_player_from_jwt(conn)
|
||||||
|
|
||||||
{status, body} =
|
{status, body} =
|
||||||
case conn.body_params do
|
case conn.body_params do
|
||||||
%{"key" => key, "name" => name} ->
|
%{"key" => key, "name" => name} ->
|
||||||
case Key.changeset(%Key{}, %{player_id: player.id, key: key, name: name})
|
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
||||||
|> Repo.insert() do
|
|> Repo.insert() do
|
||||||
{:ok, _new_key} ->
|
{:ok, _new_key} ->
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user