Add local and public QR sharing for training profiles
This commit is contained in:
49
server.js
49
server.js
@@ -1,8 +1,10 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const net = require("net");
|
||||
const { spawn } = require("child_process");
|
||||
const express = require("express");
|
||||
const QRCode = require("qrcode");
|
||||
const Database = require("better-sqlite3");
|
||||
const publicTrainingProfile = require("./server/public_training_profile");
|
||||
const { createBackupService } = require("./server/backup_service");
|
||||
@@ -101,6 +103,19 @@ app.get("/api/app-version", (_req, res) => {
|
||||
res.json(appVersion);
|
||||
});
|
||||
|
||||
app.get("/api/network/public-address", async (_req, res) => {
|
||||
try {
|
||||
const response = await fetch("https://api.ipify.org?format=json", { signal: AbortSignal.timeout(5000) });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const address = String((await response.json())?.ip || "").trim();
|
||||
if (!net.isIP(address)) throw new Error("Invalid public address response");
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.json({ address, suggestedBaseUrl: `https://${address}` });
|
||||
} catch (error) {
|
||||
res.status(502).json({ error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/backups", (_req, res) => {
|
||||
try {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
@@ -356,6 +371,38 @@ app.get("/public-training/api/:shareToken", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/public-training/qr/:shareToken", async (req, res) => {
|
||||
try {
|
||||
const stored = readStoredAppState();
|
||||
const token = String(req.params.shareToken || "");
|
||||
if (!stored || !publicTrainingProfile.hasPublicTrainingShare(stored.state, token)) {
|
||||
res.status(404).type("text/plain").send("Profile not found");
|
||||
return;
|
||||
}
|
||||
const requestedBase = String(req.query.baseUrl || "").trim();
|
||||
const configuredBase = String(stored.state?.settings?.publicBaseUrl || "").trim();
|
||||
const requestBase = `${req.protocol}://${req.get("host")}`;
|
||||
const lanUrls = getLanUrls(PORT);
|
||||
const localBase = lanUrls.find((url) => /^http:\/\/(?:192\.168\.|10\.)/.test(url)) || lanUrls[0] || requestBase;
|
||||
let baseUrl;
|
||||
try {
|
||||
const parsed = new URL(requestedBase === "local" ? localBase : requestedBase || configuredBase || requestBase);
|
||||
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Unsupported protocol");
|
||||
baseUrl = parsed.origin;
|
||||
} catch {
|
||||
baseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
}
|
||||
const language = req.query.lang === "en" ? "en" : "sv";
|
||||
const profileUrl = new URL(`/public-training/${encodeURIComponent(token)}`, baseUrl);
|
||||
profileUrl.searchParams.set("lang", language);
|
||||
const svg = await QRCode.toString(profileUrl.toString(), { type: "svg", errorCorrectionLevel: "M", margin: 2, width: 320 });
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.type("image/svg+xml").send(svg);
|
||||
} catch (error) {
|
||||
res.status(500).type("text/plain").send(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/public-training/:shareToken", (req, res) => {
|
||||
try {
|
||||
const stored = readStoredAppState();
|
||||
@@ -389,7 +436,7 @@ app.use(
|
||||
|
||||
app.use("/docs", express.static(path.join(__dirname, "docs")));
|
||||
|
||||
app.get("*", (_req, res) => {
|
||||
app.use((_req, res) => {
|
||||
res.sendFile(path.join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user