Web Client #11
@ -15,7 +15,8 @@ config :chessh, RateLimits,
|
|||||||
jail_attempt_threshold: 15,
|
jail_attempt_threshold: 15,
|
||||||
max_concurrent_user_sessions: 5,
|
max_concurrent_user_sessions: 5,
|
||||||
player_session_message_burst_ms: 500,
|
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,
|
config :chessh, Web,
|
||||||
port: 8080,
|
port: 8080,
|
||||||
|
66
front/package-lock.json
generated
66
front/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-code-blocks": "^0.0.9-0",
|
"react-code-blocks": "^0.0.9-0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@ -7836,6 +7837,11 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"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": {
|
"node_modules/exit": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
@ -16620,6 +16649,14 @@
|
|||||||
"makeerror": "1.0.12"
|
"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": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
@ -23111,6 +23148,11 @@
|
|||||||
"strip-final-newline": "^2.0.0"
|
"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": {
|
"exit": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
"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": {
|
"react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
@ -29304,6 +29362,14 @@
|
|||||||
"makeerror": "1.0.12"
|
"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": {
|
"watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-code-blocks": "^0.0.9-0",
|
"react-code-blocks": "^0.0.9-0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"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;
|
export const DEFAULT_EXPIRY_TIME_MS = 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@ -21,63 +21,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [userId, setUserId] = useState(null);
|
const [userId, setUserId] = useState(null);
|
||||||
const [username, setUsername] = 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 (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-bg-color: #282828;
|
--main-bg-color: #282828;
|
||||||
--primary-text-color: #ebdbb2;
|
--primary-text-color: #ebdbb2;
|
||||||
@ -66,6 +72,7 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
@ -103,6 +110,13 @@ a:hover {
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-end-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@ -138,10 +152,48 @@ a:hover {
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2rem;
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: solid 1px var(--gold-color);
|
border: solid 1px var(--gold-color);
|
||||||
margin-top: 12px;
|
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) {
|
@media screen and (max-width: 680px) {
|
||||||
@ -151,4 +203,22 @@ a:hover {
|
|||||||
.navbar {
|
.navbar {
|
||||||
flex-direction: column;
|
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 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 = () => {
|
export const Root = () => {
|
||||||
const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext();
|
const { signedIn, setUserId, setSignedIn, setSessionOver } = useAuthContext();
|
||||||
@ -45,15 +45,7 @@ export const Root = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<a
|
<a href={process.env.REACT_APP_GITHUB_OAUTH} className="button">
|
||||||
onClick={() =>
|
|
||||||
setSessionOver(
|
|
||||||
new Date(Date.now() + DEFAULT_EXPIRY_TIME_MS)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
href={process.env.REACT_APP_GITHUB_OAUTH}
|
|
||||||
className="button"
|
|
||||||
>
|
|
||||||
🐙 Login w/ GitHub 🐙
|
🐙 Login w/ GitHub 🐙
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
@ -1,29 +1,34 @@
|
|||||||
import { useEffect, useCallback } from "react";
|
import { useEffect, useCallback } from "react";
|
||||||
import { Link } from "react-router-dom";
|
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 = () => {
|
export const AuthSuccessful = () => {
|
||||||
const { username, signedIn, setSignedIn, setUserId, setUsername } =
|
const {
|
||||||
useAuthContext();
|
username,
|
||||||
|
userId,
|
||||||
const fetchMyself = useCallback(
|
sessionOver,
|
||||||
() =>
|
signedIn,
|
||||||
fetch("/api/player/me", {
|
setSignedIn,
|
||||||
credentials: "same-origin",
|
setUserId,
|
||||||
})
|
setUsername,
|
||||||
.then((r) => r.json())
|
setSessionOver,
|
||||||
.then((player) => {
|
} = useAuthContext();
|
||||||
setSignedIn(!!player);
|
|
||||||
setUserId(player.id);
|
|
||||||
setUsername(player.username);
|
|
||||||
}),
|
|
||||||
[setSignedIn, setUserId, setUsername]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMyself();
|
fetch("/api/player/me", {
|
||||||
}, [fetchMyself]);
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((player) => {
|
||||||
|
setSignedIn(!!player);
|
||||||
|
setUserId(player.id);
|
||||||
|
setUsername(player.username);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,8 +35,8 @@ export const Demo = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="flex-row-around">
|
<div className="flex-row-around">
|
||||||
<p>
|
<p>
|
||||||
CheSSH is a multiplayer, scalable, free, open source, and potentially
|
CheSSH is a multiplayer, scalable, free, open source, and (optionally)
|
||||||
passwordless game of Chess over the SSH protocol.
|
passwordless game of Chess over the SSH protocol, written in Elixir.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
className="button gold"
|
className="button gold"
|
||||||
@ -44,7 +44,7 @@ export const Demo = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
🌟 Star 🌟
|
🌟 Star Repo 🌟
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
@ -53,7 +53,7 @@ export const Demo = () => {
|
|||||||
<div className="flex-row-around">
|
<div className="flex-row-around">
|
||||||
<h3>Would you like to play a game?</h3>
|
<h3>Would you like to play a game?</h3>
|
||||||
<Link className="button" to="/home">
|
<Link className="button" to="/home">
|
||||||
Yes, Joshua ⇒
|
Yes, Falken ⇒
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,8 +9,8 @@ export const Home = () => {
|
|||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
const sshConfig = `Host chessh
|
const sshConfig = `Host chessh
|
||||||
Hostname ${process.env.REACT_APP_SSH_SERVER}
|
Hostname ${process.env.REACT_APP_SSH_SERVER}
|
||||||
User ${username}
|
|
||||||
Port ${process.env.REACT_APP_SSH_PORT}
|
Port ${process.env.REACT_APP_SSH_PORT}
|
||||||
|
User ${username}
|
||||||
PubkeyAuthentication yes`;
|
PubkeyAuthentication yes`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -37,10 +37,10 @@ export const Home = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<li>And connect with:</li>
|
<li>Then, connect with:</li>
|
||||||
<CopyBlock
|
<CopyBlock
|
||||||
theme={dracula}
|
theme={dracula}
|
||||||
text={`ssh -t chessh`}
|
text={"ssh -t chessh"}
|
||||||
language={"shell"}
|
language={"shell"}
|
||||||
showLineNumbers={false}
|
showLineNumbers={false}
|
||||||
codeBlock
|
codeBlock
|
||||||
@ -54,6 +54,9 @@ export const Home = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>CheSSH</h1>
|
<h1>CheSSH</h1>
|
||||||
|
<p>Hello!</p>
|
||||||
|
<p>Looks like you're not signed in 👀. </p>
|
||||||
|
<p>Please link your GitHub account above!</p>
|
||||||
</div>
|
</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";
|
import { useAuthContext } from "../context/auth_context";
|
||||||
|
|
||||||
|
Modal.setAppElement("#root");
|
||||||
|
|
||||||
const MINIMIZE_KEY_LEN = 40;
|
const MINIMIZE_KEY_LEN = 40;
|
||||||
const minimizeKey = (key) => {
|
const minimizeKey = (key) => {
|
||||||
const n = key.length;
|
const n = key.length;
|
||||||
@ -11,7 +14,7 @@ const minimizeKey = (key) => {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeyCard = ({ props }) => {
|
const KeyCard = ({ onDelete, props }) => {
|
||||||
const { id, name, key } = props;
|
const { id, name, key } = props;
|
||||||
|
|
||||||
const deleteThisKey = () => {
|
const deleteThisKey = () => {
|
||||||
@ -20,25 +23,41 @@ const KeyCard = ({ props }) => {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => d.success); //&& onDelete());
|
.then((d) => d.success && onDelete && onDelete());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="key-card">
|
<div className="key-card">
|
||||||
<h4>{name}</h4>
|
<h4 style={{ flex: 1 }}>{name}</h4>
|
||||||
<p>{minimizeKey(key)}</p>
|
<p style={{ flex: 4 }}>{minimizeKey(key)}</p>
|
||||||
|
|
||||||
<button className="button red" onClick={deleteThisKey}>
|
<button
|
||||||
|
style={{ flex: 0 }}
|
||||||
|
className="button red"
|
||||||
|
onClick={deleteThisKey}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddKey = () => {
|
const AddKeyButton = ({ onSave }) => {
|
||||||
const [key, setKey] = useState("");
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState({ value: "", error: "" });
|
||||||
const [error, setError] = useState("");
|
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 = () => {
|
const createKey = () => {
|
||||||
fetch(`/api/player/keys`, {
|
fetch(`/api/player/keys`, {
|
||||||
@ -48,28 +67,92 @@ const AddKey = () => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key,
|
key: key.value,
|
||||||
name,
|
name: name.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
if (d.success) {
|
if (d.success) {
|
||||||
setName("");
|
if (onSave) {
|
||||||
setKey("");
|
onSave();
|
||||||
} else {
|
}
|
||||||
setError(d.errors);
|
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 (
|
return (
|
||||||
<div className="key-card">
|
<div>
|
||||||
<input onChange={(e) => setName(e.target.value)} />
|
<button className="button" onClick={() => setOpen(true)}>
|
||||||
<textarea onChange={(e) => setKey(e.target.value)} />
|
+ Add Key
|
||||||
<button className="button gold" onClick={createKey}>
|
|
||||||
Add
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -78,13 +161,19 @@ export const Keys = () => {
|
|||||||
const { userId } = useAuthContext();
|
const { userId } = useAuthContext();
|
||||||
const [keys, setKeys] = useState(null);
|
const [keys, setKeys] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshKeys = useCallback(
|
||||||
if (userId) {
|
() =>
|
||||||
fetch(`/api/player/${userId}/keys`)
|
fetch(`/api/player/${userId}/keys`)
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((keys) => setKeys(keys));
|
.then((keys) => setKeys(keys)),
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) {
|
||||||
|
refreshKeys();
|
||||||
}
|
}
|
||||||
}, [userId]);
|
}, [userId, refreshKeys]);
|
||||||
|
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
@ -92,12 +181,17 @@ export const Keys = () => {
|
|||||||
if (Array.isArray(keys)) {
|
if (Array.isArray(keys)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AddKey />
|
<h2>My Keys</h2>
|
||||||
{keys.length ? (
|
<AddKeyButton onSave={refreshKeys} />
|
||||||
keys.map((key) => <KeyCard key={key.id} props={key} />)
|
<div className="key-card-collection">
|
||||||
) : (
|
{keys.length ? (
|
||||||
<p>No keys</p>
|
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(update_encode_key(attrs, :key), [:key, :player_id])
|
||||||
|> cast(attrs, [:name])
|
|> cast(attrs, [:name])
|
||||||
|> validate_required([:key, :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")
|
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
|
||||||
|> unique_constraint([:player_id, :key], message: "Player already has that key")
|
|> unique_constraint([:player_id, :key], message: "Player already has that key")
|
||||||
end
|
end
|
||||||
|
@ -3,6 +3,7 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
alias Chessh.Web.Token
|
alias Chessh.Web.Token
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
require Logger
|
require Logger
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
plug(Plug.Logger)
|
plug(Plug.Logger)
|
||||||
plug(:match)
|
plug(:match)
|
||||||
@ -127,26 +128,35 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
post "/player/keys" do
|
post "/player/keys" do
|
||||||
player = get_player_from_jwt(conn)
|
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} =
|
{status, body} =
|
||||||
case conn.body_params do
|
case conn.body_params do
|
||||||
%{"key" => key, "name" => name} ->
|
%{"key" => key, "name" => name} ->
|
||||||
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
if player_key_count > max_key_count do
|
||||||
|> Repo.insert() do
|
{400, %{errors: "Player has reached threshold of #{max_key_count} keys."}}
|
||||||
{:ok, _new_key} ->
|
else
|
||||||
{
|
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
||||||
200,
|
|> Repo.insert() do
|
||||||
%{
|
{:ok, _new_key} ->
|
||||||
success: true
|
{
|
||||||
|
200,
|
||||||
|
%{
|
||||||
|
success: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{:error, %{valid?: false} = changeset} ->
|
{:error, %{valid?: false} = changeset} ->
|
||||||
{
|
{
|
||||||
400,
|
400,
|
||||||
%{
|
%{
|
||||||
errors: format_errors(changeset)
|
errors: format_errors(changeset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
Loading…
Reference in New Issue
Block a user