Merge pull request #255 from gnmyt/optimizations/1.0.7

🆕 Version 1.0.7 - Update
This commit is contained in:
Mathias Wagner
2023-04-06 03:51:51 +02:00
committed by GitHub
18 changed files with 97 additions and 54 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "client",
"version": "1.0.6",
"version": "1.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "client",
"version": "1.0.6",
"version": "1.0.7",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "client",
"version": "1.0.6",
"version": "1.0.7",
"scripts": {
"dev": "vite",
"build": "vite build",

View File

@@ -12,7 +12,8 @@
"password": {
"title": "Passwort erforderlich",
"placeholder": "Dein Passwort",
"wrong": "Das von dir eingegebene Passwort ist falsch"
"wrong": "Das von dir eingegebene Passwort ist falsch",
"unlock": "Sperre aufheben"
},
"accept": {
"title": "Wir brauchen deine Genehmigung",
@@ -160,6 +161,7 @@
},
"test": {
"not_available": "Es liegen aktuell keine Tests vor",
"no_latest": "Es liegt kein aktueller Test vor. Bitte führe einen Test durch oder warte, bis der nächste Test durchgeführt wurde.",
"unknown_error": "Unbekannter Fehler:",
"failed": "Test fehlgeschlagen",
"recheck": "Bitte überprüfe weitestgehend, ob das öfter passiert.",

View File

@@ -12,7 +12,8 @@
"password": {
"title": "Password required",
"placeholder": "Your password",
"wrong": "The password you entered is incorrect"
"wrong": "The password you entered is incorrect",
"unlock": "Unlock"
},
"accept": {
"title": "We need your permission",
@@ -129,7 +130,7 @@
"healthchecks": "MySpeed uses <HCLink>HealthChecks</HCLink> to notify you when your internet is down. To enable this, put your ping URL in the text box. Read more <WIKILink>here</WIKILink>",
"credits": "<Link>MySpeed</Link> is provided by GNMYT and uses the <CLILink>Speedtest CLI</CLILink> from Ookla.",
"recommendations_error": "You have to do at least 10 tests to get an average. It doesn't matter if the tests were done manually or automatically.",
"recommendations_info": "Based on the last 10 tests, it was found that the optimal ping was <Bold>{{ping}} ms</Bold>, the download at <Bold>{{down}} Mbit/s</Bold> and the upload at <Bold>{{up}} Mbit/s</Bold>. It is best to orientate yourself on your internet contract and only adopt it if it matches that.",
"recommendations_info": "Based on the last 10 tests, it was found that the optimal ping was <Bold>{{ping}} ms</Bold>, the download at <Bold>{{down}} Mbps</Bold> and the upload at <Bold>{{up}} Mbps</Bold>. It is best to orientate yourself on your internet contract and only adopt it if it matches that.",
"update": "An update to version {{version}} is available. See <Changes>the changes</Changes> and <DLLink>download the update</DLLink>.",
"down": {
"title": "Download speed",
@@ -160,6 +161,7 @@
},
"test": {
"not_available": "There are currently no tests available",
"no_latest": "There is no current test available. Please perform a test or wait until the next test is performed.",
"unknown_error": "Unknown error:",
"failed": "Test failed",
"recheck": "Please check as far as possible if this happens often.",

View File

@@ -57,6 +57,8 @@ export const toggleDropdown = (setIcon) => {
function DropdownComponent() {
const [config, reloadConfig] = useContext(ConfigContext);
const [status, updateStatus] = useContext(StatusContext);
const findNode = useContext(NodeContext)[4];
const updateNodes = useContext(NodeContext)[1];
const currentNode = useContext(NodeContext)[2];
const updateTests = useContext(SpeedtestContext)[1];
const updateToast = useContext(ToastNotificationContext);
@@ -145,19 +147,24 @@ function DropdownComponent() {
const updatePassword = async () => {
toggleDropdown();
const passwordSet = currentNode !== 0 ? findNode(currentNode).password : localStorage.getItem("password") != null;
setDialog({
title: <>{t("update.new_password")} » <a onClick={updatePasswordLevel}>{t("update.level")}</a></>,
placeholder: t("update.password_placeholder"),
type: "password",
unsetButton: localStorage.getItem("password") != null ? "Sperre aufheben" : undefined,
unsetButton: passwordSet ? t("dialog.password.unlock") : undefined,
onClear: () => patchRequest("/config/password", {value: "none"})
.then(() => showFeedback("update.password_removed", false))
.then(() => localStorage.removeItem("password")),
.then(() => {
currentNode !== 0 ? baseRequest("/nodes/" + currentNode + "/password", "PATCH",
{password: "none"}).then(() => updateNodes()) : localStorage.removeItem("password");
}),
onSuccess: (value) => patchRequest("/config/password", {value})
.then(() => showFeedback(undefined, false))
.then(() => {
currentNode !== 0 ? baseRequest("/nodes/" + currentNode + "/password", "PATCH",
{password: value}) : localStorage.setItem("password", value);
{password: value}).then(() => updateNodes()) : localStorage.setItem("password", value);
})
})
}

View File

@@ -20,7 +20,7 @@ import {SpeedtestDialog} from "@/common/components/SpeedtestDialog";
import {NodeContext} from "@/common/contexts/Node";
function HeaderComponent(props) {
const nodes = useContext(NodeContext)[0];
const findNode = useContext(NodeContext)[4];
const currentNode = useContext(NodeContext)[2];
const [setDialog] = useContext(InputDialogContext);
@@ -82,8 +82,7 @@ function HeaderComponent(props) {
if (!config.viewMode) updateVersion();
}, [config]);
const getNodeName = () =>
currentNode === "0" ? t("header.title") : nodes?.find(node => node.id === currentNode)?.name || t("header.title");
const getNodeName = () => currentNode === "0" ? t("header.title") : findNode(currentNode)?.name || t("header.title");
if (Object.keys(config).length === 0) return <></>;

View File

@@ -24,8 +24,10 @@ export const NodeProvider = (props) => {
setCurrentNode(parseInt(node));
}
const findNode = (nodeId) => nodes?.find(node => node.id === nodeId);
return (
<NodeContext.Provider value={[nodes, updateNodes, currentNode, updateCurrentNode]}>
<NodeContext.Provider value={[nodes, updateNodes, currentNode, updateCurrentNode, findNode]}>
{props.children}
</NodeContext.Provider>
)

View File

@@ -14,9 +14,12 @@ const getHeaders = () => {
// Run a plain request with all default values using the base path
export const baseRequest = async (path, method = "GET", body = {}, headers = {}) => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
return await fetch("/api" + path, {
headers: {...getHeaders(), ...headers}, method,
body: method !== "GET" ? JSON.stringify(body) : undefined
body: method !== "GET" ? JSON.stringify(body) : undefined,
signal: controller.signal
});
}

View File

@@ -13,7 +13,7 @@ import {t} from "i18next";
function LatestTestComponent() {
const status = useContext(StatusContext)[0];
const [latest, setLatest] = useState({});
const [latest, setLatest] = useState(null);
const [latestTestTime, setLatestTestTime] = useState("N/A");
const [setDialog] = useContext(InputDialogContext);
const [speedtests] = useContext(SpeedtestContext);
@@ -30,6 +30,7 @@ function LatestTestComponent() {
}, [latest]);
if (Object.entries(config).length === 0) return (<></>);
if (latest === null) return (<></>);
return (
<div className={"analyse-area " + (status.paused ? "tests-paused" : "pulse")}>
@@ -38,7 +39,8 @@ function LatestTestComponent() {
<div className="container-header">
<FontAwesomeIcon onClick={() => setDialog(pingInfo())} icon={faPingPongPaddleBall}
className={"container-icon help-icon icon-" + getIconBySpeed(latest.ping, config.ping, false)}/>
<h2 className="container-text">{t("latest.ping")}<span className="container-subtext">{t("latest.ping_unit")}</span></h2>
<h2 className="container-text">{t("latest.ping")}<span
className="container-subtext">{t("latest.ping_unit")}</span></h2>
</div>
<div className="container-main">
<h2>{latest.ping === -1 ? "N/A" : latest.ping}</h2>
@@ -50,7 +52,8 @@ function LatestTestComponent() {
<div className="container-header">
<FontAwesomeIcon onClick={() => setDialog(downloadInfo())} icon={faArrowDown}
className={"container-icon help-icon icon-" + getIconBySpeed(latest.download, config.download, true)}/>
<h2 className="container-text">{t("latest.down")}<span className="container-subtext">{t("latest.speed_unit")}</span></h2>
<h2 className="container-text">{t("latest.down")}<span
className="container-subtext">{t("latest.speed_unit")}</span></h2>
</div>
<div className="container-main">
<h2>{latest.download === -1 ? "N/A" : latest.download}</h2>
@@ -64,7 +67,8 @@ function LatestTestComponent() {
<div className="container-header">
<FontAwesomeIcon onClick={() => setDialog(uploadInfo())} icon={faArrowUp}
className={"container-icon help-icon icon-" + getIconBySpeed(latest.upload, config.upload, true)}/>
<h2 className="container-text">{t("latest.up")}<span className="container-subtext">{t("latest.speed_unit")}</span></h2>
<h2 className="container-text">{t("latest.up")}<span
className="container-subtext">{t("latest.speed_unit")}</span></h2>
</div>
<div className="container-main">
<h2>{latest.upload === -1 ? "N/A" : latest.upload}</h2>
@@ -76,7 +80,8 @@ function LatestTestComponent() {
<div className="container-header">
<FontAwesomeIcon onClick={() => setDialog(latestTestInfo(latest))} icon={faClockRotateLeft}
className="container-icon icon-blue help-icon"/>
<h2 className="container-text">{t("latest.latest")}<span className="container-subtext">{t("latest.before")}</span></h2>
<h2 className="container-text">{t("latest.latest")}<span
className="container-subtext">{t("latest.before")}</span></h2>
</div>
<div className="container-main">
<h2>{latestTestTime}</h2>

View File

@@ -9,8 +9,8 @@ export const uploadInfo = () => ({title: t("info.up.title"), description: t("inf
export const latestTestInfo = (latest) => ({
title: t("info.latest.title"),
description: <Trans components={{Bold: <span className="dialog-value"/>}} values={{date: new Date(latest.created).toLocaleDateString(),
description: latest.created ? <Trans components={{Bold: <span className="dialog-value"/>}} values={{date: new Date(latest.created).toLocaleDateString(),
time: new Date(latest.created).toLocaleTimeString(undefined, {hour: "2-digit", minute: "2-digit"})}}>
info.latest.description</Trans>,
info.latest.description</Trans> : t("test.no_latest"),
buttonText: t("dialog.okay")
});

View File

@@ -19,7 +19,7 @@ export const Nodes = (props) => {
{createDialogOpen && <CreateNodeDialog onClose={() => setCreateDialogOpen(false)}/>}
<NodeHeader/>
<div className="node-area">
<NodeContainer name={t("nodes.this_server")} url={location.href} currentNode={true}
<NodeContainer name={t("nodes.this_server")} url={location.host} currentNode={true}
setShowNodePage={props.setShowNodePage} id={0}/>
{nodes.map(node => <NodeContainer {...node} key={node.id} setShowNodePage={props.setShowNodePage} />)}

View File

@@ -25,6 +25,7 @@
border: 2px dashed #696C73
border-radius: 15px
cursor: pointer
user-select: none
.node-add h1
font-size: 24pt

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "myspeed",
"version": "1.0.6",
"version": "1.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "myspeed",
"version": "1.0.6",
"version": "1.0.7",
"dependencies": {
"axios": "^1.3.5",
"bcrypt": "^5.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "myspeed",
"version": "1.0.6",
"version": "1.0.7",
"scripts": {
"client": "cd client && npm run dev",
"server": "nodemon server",

View File

@@ -1,8 +1,9 @@
const axios = require('axios');
const nodes = require('../models/Node');
// Gets all node entries
module.exports.list = async (excludePassword) => {
return await nodes.findAll({attributes: {exclude: excludePassword ? ['password'] : []}});
module.exports.list = async () => {
return await nodes.findAll().then((result) => result.map((node) => ({...node, password: node.password !== null})));
}
// Create a new node entry
@@ -28,4 +29,37 @@ module.exports.updateName = async (nodeId, name) => {
// Update the password of the node entry
module.exports.updatePassword = async (nodeId, password) => {
return await nodes.update({password: password}, {where: {id: nodeId}});
}
module.exports.checkNode = async (url, password) => {
if (password === "none") password = undefined;
const api = await axios.get(url + "/api/config", {headers: {password: password}}).catch(() => {
return "INVALID_URL";
});
if (api === "INVALID_URL" || api.status !== 200) return "INVALID_URL";
if (!api.data.ping) return "INVALID_URL";
if (api.data.viewMode) return "PASSWORD_REQUIRED";
return "NODE_VALID";
}
module.exports.proxyRequest = async (url, req, res) => {
const response = await axios(url, {
method: req.method,
headers: req.headers,
data: req.method === "GET" ? undefined : JSON.stringify(req.body),
signal: req.signal,
validateStatus: (status) => status >= 200 && status < 400
}).catch(() => "INVALID_URL");
if (response === "INVALID_URL")
return res.status(500).json({message: "Internal server error"});
if (response.headers["content-disposition"])
res.setHeader("content-disposition", response.headers["content-disposition"]);
res.status(response.status).json(response.data);
}

View File

@@ -4,6 +4,9 @@ const timerTask = require('./tasks/timer');
const healthCheckTask = require('./tasks/healthchecks');
const app = express();
app.disable('x-powered-by');
const port = process.env.port || 5216;
// Create the data folder and the servers file

View File

@@ -10,6 +10,8 @@ app.get("/json", password(false), async (req, res) => {
app.get("/csv", password(false), async (req, res) => {
res.set({"Content-Disposition": "attachment; filename=\"speedtests.csv\""});
let list = await tests.list();
if (list.length === 0) return res.send("");
let fields = Object.keys(list[0]);
let replacer = (key, value) => value === null ? '' : value;

View File

@@ -1,10 +1,11 @@
const app = require('express').Router();
const nodes = require('../controller/node');
const password = require("../middlewares/password");
const {checkNode, proxyRequest} = require("../controller/node");
// List all nodes
app.get("/", password(false), async (req, res) => {
return res.json(await nodes.list(true));
return res.json(await nodes.list());
});
// Create a node
@@ -13,18 +14,14 @@ app.put("/", password(false), async (req, res) => {
const url = req.body.url.replace(/\/+$/, "");
const headers = req.body.password ? {password: req.body.password} : {};
fetch(url + "/api/config", {headers}).then(async api => {
if (api.status !== 200)
checkNode(url, req.body.password).then(async (result) => {
if (result === "INVALID_URL")
return res.status(400).json({message: "Invalid URL", type: "INVALID_URL"});
if ((await api.json()).viewMode)
if (result === "PASSWORD_REQUIRED")
return res.status(400).json({message: "Invalid password", type: "PASSWORD_REQUIRED"});
res.json({id: (await nodes.create(req.body.name, url, req.body.password)).id, type: "NODE_CREATED"});
}).catch(async () => {
res.status(400).json({message: "Invalid URL", type: "INVALID_URL"});
});
});
@@ -55,17 +52,15 @@ app.patch("/:nodeId/password", password(false), async (req, res) => {
const node = await nodes.get(req.params.nodeId);
if (node === null) return res.status(404).json({message: "Node not found"});
fetch(node.url + "/api/config", {headers: {password: req.body.password}}).then(async api => {
if (api.status !== 200)
checkNode(node.url, req.body.password).then(async (result) => {
if (result === "INVALID_URL")
return res.status(400).json({message: "Invalid URL", type: "INVALID_URL"});
if ((await api.json()).viewMode)
if (result === "PASSWORD_REQUIRED")
return res.status(400).json({message: "Invalid password", type: "PASSWORD_REQUIRED"});
await nodes.updatePassword(req.params.nodeId, req.body.password);
await nodes.updatePassword(req.params.nodeId, req.body.password === "none" ? null : req.body.password);
res.json({message: "Node password successfully updated", type: "PASSWORD_UPDATED"});
}).catch(async () => {
res.status(400).json({message: "Invalid URL", type: "INVALID_URL"});
});
});
@@ -79,19 +74,7 @@ app.all("/:nodeId/*", password(false), async (req, res) => {
req.headers['password'] = node.password;
delete req.headers['host'];
fetch(url, {
method: req.method,
headers: req.headers,
body: req.method === "GET" ? undefined : JSON.stringify(req.body),
signal: req.signal
}).then(async api => {
if (api.headers.get("content-disposition"))
res.setHeader("content-disposition", api.headers.get("content-disposition"));
res.status(api.status).json(await api.json());
}).catch(() => {
res.status(500).json({message: "Internal server error"});
});
await proxyRequest(url, req, res);
});
module.exports = app;