Web Client #11
@ -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 />,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
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)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user