Web Client #11
@ -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 />,
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
144
front/src/routes/password.jsx
Normal file
144
front/src/routes/password.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -16,4 +16,3 @@ defmodule Chessh.Release do
|
|||||||
Application.fetch_env!(@app, :ecto_repos)
|
Application.fetch_env!(@app, :ecto_repos)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user