mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-01-05 04:29:44 -06:00
Merge pull request #255 from gnmyt/optimizations/1.0.7
🆕 Version 1.0.7 - Update
This commit is contained in:
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 <></>;
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "myspeed",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"scripts": {
|
||||
"client": "cd client && npm run dev",
|
||||
"server": "nodemon server",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user