Add local and public QR sharing for training profiles

This commit is contained in:
larssand
2026-09-03 20:15:13 +02:00
parent 4f847610dc
commit 2d56b74731
15 changed files with 965 additions and 226 deletions

View File

@@ -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"));
});