Add password form, some minor frontend changes as well

This commit is contained in:
Logan Hunt 2023-01-19 12:13:25 -07:00
parent 53041c74a5
commit ab653ad439
No known key found for this signature in database
GPG Key ID: 8AC6A4B840C0EC49
8 changed files with 220 additions and 88 deletions

View File

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

View File

@ -17,13 +17,10 @@ export const Root = () => {
</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="/password">
User Password
</Link> </Link>
<Link className="link" to="/keys"> <Link className="link" to="/keys">
Keys Keys

View File

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

View File

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

View File

@ -18,12 +18,18 @@ const KeyCard = ({ onDelete, props }) => {
const { id, name, key } = props; const { id, name, key } = props;
const deleteThisKey = () => { const deleteThisKey = () => {
fetch(`/api/keys/${id}`, { if (
credentials: "same-origin", window.confirm(
method: "DELETE", "Are you sure? This will close all your current ssh sessions."
}) )
.then((r) => r.json()) ) {
.then((d) => d.success && onDelete && onDelete()); fetch(`/api/keys/${id}`, {
credentials: "same-origin",
method: "DELETE",
})
.then((r) => r.json())
.then((d) => d.success && onDelete && onDelete());
}
}; };
return ( return (
@ -44,13 +50,13 @@ const KeyCard = ({ onDelete, props }) => {
const AddKeyButton = ({ onSave }) => { const AddKeyButton = ({ onSave }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState({ value: "", error: "" }); const [name, setName] = useState("");
const [key, setKey] = useState({ value: "", error: "" }); const [key, setKey] = useState("");
const [errors, setErrors] = useState(null); const [errors, setErrors] = useState(null);
const setDefaults = () => { const setDefaults = () => {
setName({ value: "", error: "" }); setName("");
setKey({ value: "", error: "" }); setKey("");
setErrors(null); setErrors(null);
}; };
@ -67,8 +73,8 @@ const AddKeyButton = ({ onSave }) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
key: key.value.trim(), key: key.trim(),
name: name.value, name: name.trim(),
}), }),
}) })
.then((r) => r.json()) .then((r) => r.json())
@ -119,8 +125,8 @@ const AddKeyButton = ({ onSave }) => {
<hr /> <hr />
<p>Key Name *</p> <p>Key Name *</p>
<input <input
value={name.value} value={name}
onChange={(e) => setName({ ...name, value: e.target.value })} onChange={(e) => setName(e.target.value)}
required required
/> />
</div> </div>
@ -129,8 +135,8 @@ const AddKeyButton = ({ onSave }) => {
<textarea <textarea
cols={40} cols={40}
rows={5} rows={5}
value={key.value} value={key}
onChange={(e) => setKey({ ...key, value: e.target.value })} onChange={(e) => setKey(e.target.value)}
required required
/> />
</div> </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) Application.fetch_env!(@app, :ecto_repos)
end end
end end

View File

@ -43,7 +43,25 @@ defmodule Chessh.Web.Endpoint do
|> assign_jwt_and_redirect_or_encode(status, body) |> assign_jwt_and_redirect_or_encode(status, body)
end 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) player = get_player_from_jwt(conn)
PlayerSession.close_all_player_sessions(player) PlayerSession.close_all_player_sessions(player)
@ -68,44 +86,6 @@ defmodule Chessh.Web.Endpoint do
|> send_resp(status, Jason.encode!(body)) |> send_resp(status, Jason.encode!(body))
end 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 get "/player/logout" do
conn conn
|> delete_resp_cookie("jwt") |> delete_resp_cookie("jwt")