Web Client #11
@ -15,7 +15,8 @@ config :chessh, RateLimits,
|
||||
jail_attempt_threshold: 15,
|
||||
max_concurrent_user_sessions: 5,
|
||||
player_session_message_burst_ms: 500,
|
||||
player_session_message_burst_rate: 8
|
||||
player_session_message_burst_rate: 8,
|
||||
player_public_keys: 15
|
||||
|
||||
config :chessh, Web,
|
||||
port: 8080,
|
||||
|
66
front/package-lock.json
generated
66
front/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
@ -7836,6 +7837,11 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/exenv": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||
"integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
|
||||
},
|
||||
"node_modules/exit": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||
@ -14533,6 +14539,29 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-modal": {
|
||||
"version": "3.16.1",
|
||||
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
|
||||
"integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
|
||||
"dependencies": {
|
||||
"exenv": "^1.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-lifecycles-compat": "^3.0.0",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
|
||||
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
@ -16620,6 +16649,14 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
@ -23111,6 +23148,11 @@
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"exenv": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||
"integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
|
||||
},
|
||||
"exit": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||
@ -27768,6 +27810,22 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-modal": {
|
||||
"version": "3.16.1",
|
||||
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
|
||||
"integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
|
||||
"requires": {
|
||||
"exenv": "^1.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-lifecycles-compat": "^3.0.0",
|
||||
"warning": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
@ -29304,6 +29362,14 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
@ -10,6 +10,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState, createContext, useEffect } from "react";
|
||||
import React, { useContext, useState, createContext } from "react";
|
||||
|
||||
export const DEFAULT_EXPIRY_TIME_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
@ -21,63 +21,6 @@ export const AuthProvider = ({ children }) => {
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [username, setUsername] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
localStorage.setItem("userId", userId.toString());
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
localStorage.setItem("username", username);
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
useEffect(() => {
|
||||
let expiry = localStorage.getItem("expiry");
|
||||
if (expiry) {
|
||||
expiry = new Date(expiry);
|
||||
if (Date.now() < expiry.getTime()) {
|
||||
setSignedIn(true);
|
||||
setSessionOver(expiry);
|
||||
// We don't have access to the JWT token as it is an HTTP only cookie -
|
||||
// so we store user info in local storage
|
||||
((username) => {
|
||||
if (username) {
|
||||
setUsername(username);
|
||||
}
|
||||
})(localStorage.getItem("username"));
|
||||
|
||||
((id) => {
|
||||
if (id) {
|
||||
setUserId(parseInt(id, 10));
|
||||
}
|
||||
})(localStorage.getItem("userId"));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("expiry", sessionOver.toISOString());
|
||||
setTimeout(() => {
|
||||
setSessionOver((sessionOver) => {
|
||||
if (Date.now() >= sessionOver.getTime()) {
|
||||
setSignedIn((signedIn) => {
|
||||
if (signedIn) {
|
||||
alert(
|
||||
"Session expired. Any further privileged requests will fail until signed in again."
|
||||
);
|
||||
["userId", "userName"].map((x) => localStorage.removeItem(x));
|
||||
return false;
|
||||
}
|
||||
return signedIn;
|
||||
});
|
||||
}
|
||||
return sessionOver;
|
||||
});
|
||||
}, sessionOver.getTime() - Date.now());
|
||||
}, [sessionOver]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
|
@ -1,3 +1,9 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--main-bg-color: #282828;
|
||||
--primary-text-color: #ebdbb2;
|
||||
@ -66,6 +72,7 @@ body {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem;
|
||||
padding-left: 2rem;
|
||||
@ -103,6 +110,13 @@ a:hover {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.flex-end-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: 1rem;
|
||||
max-width: 1200px;
|
||||
@ -138,10 +152,48 @@ a:hover {
|
||||
justify-content: space-around;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
border-radius: 12px;
|
||||
border: solid 1px var(--gold-color);
|
||||
margin-top: 12px;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.key-card-collection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
font-family: "DM Mono";
|
||||
color: var(--primary-text-color);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--primary-text-color);
|
||||
}
|
||||
input,textarea: focus {
|
||||
border: 1px solid var(--gold-color);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
padding: 3rem;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-moz-transform: translateX(-50%) translateY(-50%);
|
||||
-webkit-transform: translateX(-50%) translateY(-50%);
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
position: absolute;
|
||||
|
||||
border-radius: 12px;
|
||||
border: solid 1px var(--purple-color);
|
||||
background-color: var(--main-bg-color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
@ -151,4 +203,22 @@ a:hover {
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
.key-card {
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
gap: 0;
|
||||
align-items: start;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.flex-row-around {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.key-card-collection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Link, Outlet } from "react-router-dom";
|
||||
|
||||
import logo from "./assets/chessh_sm.svg";
|
||||
|
||||
import { useAuthContext, DEFAULT_EXPIRY_TIME_MS } from "./context/auth_context";
|
||||
import { useAuthContext } from "./context/auth_context";
|
||||
|
||||
export const Root = () => {
|
||||
const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext();
|
||||
@ -45,15 +45,7 @@ export const Root = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
onClick={() =>
|
||||
setSessionOver(
|
||||
new Date(Date.now() + DEFAULT_EXPIRY_TIME_MS)
|
||||
)
|
||||
}
|
||||
href={process.env.REACT_APP_GITHUB_OAUTH}
|
||||
className="button"
|
||||
>
|
||||
<a href={process.env.REACT_APP_GITHUB_OAUTH} className="button">
|
||||
🐙 Login w/ GitHub 🐙
|
||||
</a>
|
||||
</>
|
||||
|
@ -1,29 +1,34 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useAuthContext } from "../context/auth_context";
|
||||
import {
|
||||
useAuthContext,
|
||||
DEFAULT_EXPIRY_TIME_MS,
|
||||
} from "../context/auth_context";
|
||||
|
||||
export const AuthSuccessful = () => {
|
||||
const { username, signedIn, setSignedIn, setUserId, setUsername } =
|
||||
useAuthContext();
|
||||
|
||||
const fetchMyself = useCallback(
|
||||
() =>
|
||||
fetch("/api/player/me", {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((player) => {
|
||||
setSignedIn(!!player);
|
||||
setUserId(player.id);
|
||||
setUsername(player.username);
|
||||
}),
|
||||
[setSignedIn, setUserId, setUsername]
|
||||
);
|
||||
const {
|
||||
username,
|
||||
userId,
|
||||
sessionOver,
|
||||
signedIn,
|
||||
setSignedIn,
|
||||
setUserId,
|
||||
setUsername,
|
||||
setSessionOver,
|
||||
} = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
fetchMyself();
|
||||
}, [fetchMyself]);
|
||||
fetch("/api/player/me", {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((player) => {
|
||||
setSignedIn(!!player);
|
||||
setUserId(player.id);
|
||||
setUsername(player.username);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (signedIn) {
|
||||
return (
|
||||
|
@ -35,8 +35,8 @@ export const Demo = () => {
|
||||
</h1>
|
||||
<div className="flex-row-around">
|
||||
<p>
|
||||
CheSSH is a multiplayer, scalable, free, open source, and potentially
|
||||
passwordless game of Chess over the SSH protocol.
|
||||
CheSSH is a multiplayer, scalable, free, open source, and (optionally)
|
||||
passwordless game of Chess over the SSH protocol, written in Elixir.
|
||||
</p>
|
||||
<a
|
||||
className="button gold"
|
||||
@ -44,7 +44,7 @@ export const Demo = () => {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
🌟 Star 🌟
|
||||
🌟 Star Repo 🌟
|
||||
</a>
|
||||
</div>
|
||||
<hr />
|
||||
@ -53,7 +53,7 @@ export const Demo = () => {
|
||||
<div className="flex-row-around">
|
||||
<h3>Would you like to play a game?</h3>
|
||||
<Link className="button" to="/home">
|
||||
Yes, Joshua ⇒
|
||||
Yes, Falken ⇒
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,8 +9,8 @@ export const Home = () => {
|
||||
if (signedIn) {
|
||||
const sshConfig = `Host chessh
|
||||
Hostname ${process.env.REACT_APP_SSH_SERVER}
|
||||
User ${username}
|
||||
Port ${process.env.REACT_APP_SSH_PORT}
|
||||
User ${username}
|
||||
PubkeyAuthentication yes`;
|
||||
return (
|
||||
<>
|
||||
@ -37,10 +37,10 @@ export const Home = () => {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<li>And connect with:</li>
|
||||
<li>Then, connect with:</li>
|
||||
<CopyBlock
|
||||
theme={dracula}
|
||||
text={`ssh -t chessh`}
|
||||
text={"ssh -t chessh"}
|
||||
language={"shell"}
|
||||
showLineNumbers={false}
|
||||
codeBlock
|
||||
@ -54,6 +54,9 @@ export const Home = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>CheSSH</h1>
|
||||
<p>Hello!</p>
|
||||
<p>Looks like you're not signed in 👀. </p>
|
||||
<p>Please link your GitHub account above!</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Modal from "react-modal";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAuthContext } from "../context/auth_context";
|
||||
|
||||
Modal.setAppElement("#root");
|
||||
|
||||
const MINIMIZE_KEY_LEN = 40;
|
||||
const minimizeKey = (key) => {
|
||||
const n = key.length;
|
||||
@ -11,7 +14,7 @@ const minimizeKey = (key) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const KeyCard = ({ props }) => {
|
||||
const KeyCard = ({ onDelete, props }) => {
|
||||
const { id, name, key } = props;
|
||||
|
||||
const deleteThisKey = () => {
|
||||
@ -20,25 +23,41 @@ const KeyCard = ({ props }) => {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => d.success); //&& onDelete());
|
||||
.then((d) => d.success && onDelete && onDelete());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="key-card">
|
||||
<h4>{name}</h4>
|
||||
<p>{minimizeKey(key)}</p>
|
||||
<h4 style={{ flex: 1 }}>{name}</h4>
|
||||
<p style={{ flex: 4 }}>{minimizeKey(key)}</p>
|
||||
|
||||
<button className="button red" onClick={deleteThisKey}>
|
||||
<button
|
||||
style={{ flex: 0 }}
|
||||
className="button red"
|
||||
onClick={deleteThisKey}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AddKey = () => {
|
||||
const [key, setKey] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const AddKeyButton = ({ onSave }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState({ value: "", error: "" });
|
||||
const [key, setKey] = useState({ value: "", error: "" });
|
||||
const [errors, setErrors] = useState(null);
|
||||
|
||||
const setDefaults = () => {
|
||||
setName({ value: "", error: "" });
|
||||
setKey({ value: "", error: "" });
|
||||
setErrors(null);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setDefaults();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const createKey = () => {
|
||||
fetch(`/api/player/keys`, {
|
||||
@ -48,28 +67,92 @@ const AddKey = () => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key,
|
||||
name,
|
||||
key: key.value,
|
||||
name: name.value,
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) {
|
||||
setName("");
|
||||
setKey("");
|
||||
} else {
|
||||
setError(d.errors);
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
setDefaults();
|
||||
close();
|
||||
} else if (d.errors) {
|
||||
if (typeof d.errors === "object") {
|
||||
setErrors(
|
||||
Object.keys(d.errors).map(
|
||||
(field) => `${field}: ${d.errors[field].join(",")}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setErrors([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
|
||||
<div>
|
||||
<button className="button" onClick={() => setOpen(true)}>
|
||||
+ Add Key
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={open}
|
||||
onRequestClose={close}
|
||||
className="modal"
|
||||
contentLabel="Add Key"
|
||||
>
|
||||
<div>
|
||||
<h3>Add SSH Key</h3>
|
||||
<p>
|
||||
Not sure about this? Check{" "}
|
||||
<a
|
||||
href="https://www.ssh.com/academy/ssh/keygen"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
for help!
|
||||
</p>
|
||||
<hr />
|
||||
<p>Key Name *</p>
|
||||
<input
|
||||
value={name.value}
|
||||
onChange={(e) => setName({ ...name, value: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>SSH Key *</p>
|
||||
<textarea
|
||||
cols={40}
|
||||
rows={5}
|
||||
value={key.value}
|
||||
onChange={(e) => setKey({ ...key, value: 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">
|
||||
<button className="button" onClick={createKey}>
|
||||
Add
|
||||
</button>
|
||||
<button className="button red" onClick={close}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -78,13 +161,19 @@ export const Keys = () => {
|
||||
const { userId } = useAuthContext();
|
||||
const [keys, setKeys] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
const refreshKeys = useCallback(
|
||||
() =>
|
||||
fetch(`/api/player/${userId}/keys`)
|
||||
.then((r) => r.json())
|
||||
.then((keys) => setKeys(keys));
|
||||
.then((keys) => setKeys(keys)),
|
||||
[userId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
refreshKeys();
|
||||
}
|
||||
}, [userId]);
|
||||
}, [userId, refreshKeys]);
|
||||
|
||||
if (!keys) {
|
||||
return <p>Loading...</p>;
|
||||
@ -92,12 +181,17 @@ export const Keys = () => {
|
||||
if (Array.isArray(keys)) {
|
||||
return (
|
||||
<>
|
||||
<AddKey />
|
||||
{keys.length ? (
|
||||
keys.map((key) => <KeyCard key={key.id} props={key} />)
|
||||
) : (
|
||||
<p>No keys</p>
|
||||
)}
|
||||
<h2>My Keys</h2>
|
||||
<AddKeyButton onSave={refreshKeys} />
|
||||
<div className="key-card-collection">
|
||||
{keys.length ? (
|
||||
keys.map((key) => (
|
||||
<KeyCard key={key.id} onDelete={refreshKeys} props={key} />
|
||||
))
|
||||
) : (
|
||||
<p>Looks like you've got no keys, try adding some!</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ defmodule Chessh.Key do
|
||||
|> cast(update_encode_key(attrs, :key), [:key, :player_id])
|
||||
|> cast(attrs, [:name])
|
||||
|> validate_required([:key, :name])
|
||||
|> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid public ssh key")
|
||||
|> validate_format(:key, ~r/^[\-\w\d]+ [^ ]+$/, message: "invalid public ssh key")
|
||||
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
|
||||
|> unique_constraint([:player_id, :key], message: "Player already has that key")
|
||||
end
|
||||
|
@ -3,6 +3,7 @@ defmodule Chessh.Web.Endpoint do
|
||||
alias Chessh.Web.Token
|
||||
use Plug.Router
|
||||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
plug(Plug.Logger)
|
||||
plug(:match)
|
||||
@ -127,26 +128,35 @@ defmodule Chessh.Web.Endpoint do
|
||||
post "/player/keys" do
|
||||
player = get_player_from_jwt(conn)
|
||||
|
||||
player_key_count =
|
||||
Repo.aggregate(from(k in Key, where: k.player_id == ^player.id), :count, :id)
|
||||
|
||||
max_key_count = Application.get_env(:chessh, RateLimits)[:player_public_keys]
|
||||
|
||||
{status, body} =
|
||||
case conn.body_params do
|
||||
%{"key" => key, "name" => name} ->
|
||||
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
||||
|> Repo.insert() do
|
||||
{:ok, _new_key} ->
|
||||
{
|
||||
200,
|
||||
%{
|
||||
success: true
|
||||
if player_key_count > max_key_count do
|
||||
{400, %{errors: "Player has reached threshold of #{max_key_count} keys."}}
|
||||
else
|
||||
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
||||
|> Repo.insert() do
|
||||
{:ok, _new_key} ->
|
||||
{
|
||||
200,
|
||||
%{
|
||||
success: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{:error, %{valid?: false} = changeset} ->
|
||||
{
|
||||
400,
|
||||
%{
|
||||
errors: format_errors(changeset)
|
||||
{:error, %{valid?: false} = changeset} ->
|
||||
{
|
||||
400,
|
||||
%{
|
||||
errors: format_errors(changeset)
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
|
Loading…
Reference in New Issue
Block a user