Web Client #11

Merged
Simponic merged 19 commits from web into main 2023-01-19 16:04:10 -05:00
8 changed files with 220 additions and 88 deletions
Showing only changes of commit ab653ad439 - Show all commits

View File

@ -7,6 +7,7 @@ import { Root } from "./root";
import { Demo } from "./routes/demo";
import { Home } from "./routes/home";
import { Keys } from "./routes/keys";
import { Password } from "./routes/password";
import { AuthSuccessful } from "./routes/auth_successful";
import "./index.css";
@ -23,17 +24,13 @@ const router = createBrowserRouter([
element: <Home />,
},
{
path: "user",
element: <Home />,
path: "password",
element: <Password />,
},
{
path: "keys",
element: <Keys />,
},
{
path: "faq",
element: <Home />,
},
{
path: "auth-successful",
element: <AuthSuccessful />,

View File

@ -17,13 +17,10 @@ export const Root = () => {
</Link>
</div>
<div className="nav">
<Link className="link" to="/faq">
FAQ
</Link>
{signedIn ? (
<>
<Link className="link" to="/user">
User
<Link className="link" to="/password">
Password
</Link>
<Link className="link" to="/keys">
Keys

View File

@ -23,11 +23,17 @@ export const AuthSuccessful = () => {
if (signedIn) {
return (
<>
<h1>Authentication Successful</h1>
<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>
<span>Hello there, {player?.username || ""}! </span>
<Link to="/home" className="button">
Go Home{" "}
Go Home
</Link>
</div>
</>

View File

@ -14,27 +14,30 @@ export const Home = () => {
PubkeyAuthentication yes`;
return (
<>
<h2>Hello there, {player?.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>
<h2>Welcome, {player?.username}</h2>
<hr />
<h2>Getting Started</h2>
<h3>Getting Started</h3>
<ol>
<li>
Add the following to your ssh config (normally in ~/.ssh/config):
</li>
<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
/>
<CopyBlock
theme={dracula}
text={sshConfig}
showLineNumbers={true}
wrapLines
codeBlock
/>
</div>
<div>
<li>Then, connect with:</li>

View File

@ -18,12 +18,18 @@ const KeyCard = ({ onDelete, 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 && onDelete());
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 (
@ -44,13 +50,13 @@ const KeyCard = ({ onDelete, props }) => {
const AddKeyButton = ({ onSave }) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState({ value: "", error: "" });
const [key, setKey] = useState({ value: "", error: "" });
const [name, setName] = useState("");
const [key, setKey] = useState("");
const [errors, setErrors] = useState(null);
const setDefaults = () => {
setName({ value: "", error: "" });
setKey({ value: "", error: "" });
setName("");
setKey("");
setErrors(null);
};
@ -67,8 +73,8 @@ const AddKeyButton = ({ onSave }) => {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: key.value.trim(),
name: name.value,
key: key.trim(),
name: name.trim(),
}),
})
.then((r) => r.json())
@ -119,8 +125,8 @@ const AddKeyButton = ({ onSave }) => {
<hr />
<p>Key Name *</p>
<input
value={name.value}
onChange={(e) => setName({ ...name, value: e.target.value })}
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
@ -129,8 +135,8 @@ const AddKeyButton = ({ onSave }) => {
<textarea
cols={40}
rows={5}
value={key.value}
onChange={(e) => setKey({ ...key, value: e.target.value })}
value={key}
onChange={(e) => setKey(e.target.value)}
required
/>
</div>

View File

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

View File

@ -16,4 +16,3 @@ defmodule Chessh.Release do
Application.fetch_env!(@app, :ecto_repos)
end
end

View File

@ -43,7 +43,25 @@ defmodule Chessh.Web.Endpoint do
|> assign_jwt_and_redirect_or_encode(status, body)
end
put "/player/password" do
delete "/player/token/password" do
player = get_player_from_jwt(conn)
PlayerSession.close_all_player_sessions(player)
{status, body} =
case Repo.update(Ecto.Changeset.change(player, %{hashed_password: nil})) do
{:ok, _new_player} ->
{200, %{success: true}}
{:error, _} ->
{400, %{success: false}}
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
put "/player/token/password" do
player = get_player_from_jwt(conn)
PlayerSession.close_all_player_sessions(player)
@ -68,44 +86,6 @@ defmodule Chessh.Web.Endpoint do
|> send_resp(status, Jason.encode!(body))
end
post "/player/login" do
{status, body} =
case conn.body_params do
%{"username" => username, "password" => password} ->
player = Repo.get_by(Player, username: username)
case Player.valid_password?(player, password) do
true ->
{
200,
%{
token:
Token.generate_and_sign!(%{
"uid" => player.id
})
}
}
_ ->
{
400,
%{
errors: "Invalid credentials"
}
}
end
_ ->
{
400,
%{errors: "Username and password must be defined"}
}
end
conn
|> assign_jwt_and_redirect_or_encode(status, body)
end
get "/player/logout" do
conn
|> delete_resp_cookie("jwt")