Add keys as user

This commit is contained in:
Simponic 2023-01-18 15:20:47 -07:00
parent d9101b36e7
commit 7e2d565ae9
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
14 changed files with 939 additions and 27 deletions

View File

@ -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

View File

@ -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")

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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 />,
},
], ],
}, },
]); ]);

View File

@ -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>
</> </>

View File

@ -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 ( return (
<> <>
<h1>Successful Auth</h1> <h1>Authentication Successful</h1>
{signedIn ? <p>Hello there, {username || ""}</p> : <p>Loading...</p>} <div>
<span>Hello there, {username || ""}! </span>
<Link to="/home" className="button">
Go Home{" "}
</Link>
</div>
</>
);
}
return (
<>
<p>Loading...</p>
</> </>
); );
}; };

View File

@ -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
View 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>
)}
</>
);
}
};

View File

@ -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";
},
}) })
); );
} }

View File

@ -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} ->
{ {