Web Client #11
@ -7,4 +7,8 @@ GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
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,
|
||||
ecto_repos: [Chessh.Repo],
|
||||
key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
|
||||
port: 42_069,
|
||||
max_sessions: 255,
|
||||
ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json")
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
import Config
|
||||
|
||||
config :chessh,
|
||||
port: String.to_integer(System.get_env("SSH_PORT", "42069"))
|
||||
|
||||
config :chessh, Web,
|
||||
github_client_id: System.get_env("GITHUB_CLIENT_ID"),
|
||||
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",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"react": "^18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
|
@ -21,13 +21,6 @@ export const AuthProvider = ({ children }) => {
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [username, setUsername] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!signedIn) {
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
}
|
||||
}, [signedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
localStorage.setItem("userId", userId.toString());
|
||||
|
@ -27,6 +27,7 @@ body {
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
color: var(--main-bg-color);
|
||||
text-decoration: none;
|
||||
@ -34,6 +35,8 @@ body {
|
||||
border: var(--primary-text-color) solid 1px;
|
||||
background-color: var(--success-color);
|
||||
padding: 0.5rem;
|
||||
|
||||
font-family: "DM Mono";
|
||||
}
|
||||
.button:hover {
|
||||
background-color: var(--success-color-hover);
|
||||
@ -44,6 +47,13 @@ body {
|
||||
.gold: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 {
|
||||
width: 6rem;
|
||||
@ -63,17 +73,20 @@ body {
|
||||
border: var(--purple-color) solid 1px;
|
||||
}
|
||||
|
||||
.link {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
font-size: 1.25rem;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--success-color-hover);
|
||||
a:hover {
|
||||
background-color: var(--success-color-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -119,3 +132,23 @@ body {
|
||||
-webkit-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 { Demo } from "./routes/demo";
|
||||
import { Home } from "./routes/home";
|
||||
import { Keys } from "./routes/keys";
|
||||
import { AuthSuccessful } from "./routes/auth_successful";
|
||||
|
||||
import "./index.css";
|
||||
@ -26,13 +27,17 @@ const router = createBrowserRouter([
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: "auth-successful",
|
||||
element: <AuthSuccessful />,
|
||||
path: "keys",
|
||||
element: <Keys />,
|
||||
},
|
||||
{
|
||||
path: "keys",
|
||||
path: "faq",
|
||||
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";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="navbar">
|
||||
<div>
|
||||
<div className="flex-row-around">
|
||||
<Link to="/home">
|
||||
<img src={logo} className="logo" alt="CheSSH Logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="nav">
|
||||
<Link className="link" to="/faq">
|
||||
FAQ
|
||||
</Link>
|
||||
{signedIn ? (
|
||||
<>
|
||||
<Link className="link" to="/user">
|
||||
@ -24,11 +39,7 @@ export const Root = () => {
|
||||
<Link className="link" to="/keys">
|
||||
Keys
|
||||
</Link>
|
||||
<Link
|
||||
className="button"
|
||||
onClick={() => setSignedIn(false)}
|
||||
to="/"
|
||||
>
|
||||
<Link className="button" onClick={signOut} to="/">
|
||||
Sign Out
|
||||
</Link>
|
||||
</>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useAuthContext } from "../context/auth_context";
|
||||
|
||||
export const AuthSuccessful = () => {
|
||||
@ -23,10 +25,22 @@ export const AuthSuccessful = () => {
|
||||
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 (
|
||||
<>
|
||||
<h1>Successful Auth</h1>
|
||||
{signedIn ? <p>Hello there, {username || ""}</p> : <p>Loading...</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";
|
||||
|
||||
export const Home = () => {
|
||||
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 (
|
||||
<div>
|
||||
<h1>Welcome home, {signedIn ? username : "guest"}!</h1>
|
||||
<h1>CheSSH</h1>
|
||||
</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) => {
|
||||
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))
|
||||
end
|
||||
|
||||
get "/player/logout" do
|
||||
conn
|
||||
|> delete_resp_cookie("jwt")
|
||||
|> send_resp(200, Jason.encode!(%{success: true}))
|
||||
end
|
||||
|
||||
post "/player/keys" do
|
||||
player = get_player_from_jwt(conn)
|
||||
|
||||
{status, body} =
|
||||
case conn.body_params do
|
||||
%{"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
|
||||
{:ok, _new_key} ->
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user