From 67920840e180ae245307a763ef1d37c22bbb125d Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Fri, 22 May 2026 15:29:43 -0600 Subject: [PATCH] Add session manager and verification list to the server config editor Adds two interactive admin tools to the matrix-server-config node so device verification and session management can be done from the editor without building a flow. Both open as modal dialogs and are backed by new flows.write-protected admin endpoints. Pending verifications - New "Pending verification requests" button opens a modal listing incoming and in-progress verification requests, refreshed every 5 seconds. - Each entry shows the type (device or room), who it is from, a live age, and a live expiry countdown; the list is capped at the newest 20 with a hidden-count note. - Clicking an entry drives the SAS flow (accept, start, show emoji, confirm/cancel) within the modal. - New /matrix-chat/verification endpoint with list, advance, confirm, mismatch, and cancel actions. - trackVerificationRequest now records a seen-at timestamp and keeps finished requests for two minutes so their outcome can still be reported to the editor. Sessions - New "Manage sessions" button opens a modal listing the account's sessions, modelled on Element's session manager: the current session with a verified / not-verified status box, then other sessions, each with a green or red shield, last activity, and IP address. - Clicking a session shows its details (session ID, last activity, IP), a Rename action, and a password-confirmed Remove. Unverified sessions offer a Verify action that hands off into the verification modal. - New /matrix-chat/sessions endpoint with list, rename, remove, and verify actions. --- src/matrix-server-config.html | 551 ++++++++++++++++++++++++++++++++++ src/matrix-server-config.js | 252 +++++++++++++++- 2 files changed, 801 insertions(+), 2 deletions(-) diff --git a/src/matrix-server-config.html b/src/matrix-server-config.html index 7d9a29b..801e350 100644 --- a/src/matrix-server-config.html +++ b/src/matrix-server-config.html @@ -204,6 +204,539 @@ $("#matrix-sb-overlay").data("sbStatusFn")(); }); + // --- Verification list (modal) --- + // Built once and reused; the node id is stored on the overlay. + if (!document.getElementById("matrix-vl-overlay")) { + $('').appendTo("head"); + + $('
' + + '
Device Verification' + + '×
' + + '
' + + '
' + + '
Pending verification requests — this list refreshes every 5 seconds. Click a request to verify it.
' + + '
' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
').appendTo(document.body); + + var vlListTimer = null, vlTickTimer = null, vlDetailTimer = null; + var vlEsc = function(s) { return $("
").text(s == null ? "" : String(s)).html(); }; + var vlId = function() { return $("#matrix-vl-overlay").data("matrixNodeId"); }; + var vlCurId = function() { return $("#matrix-vl-overlay").data("vlCurrentId"); }; + var vlCall = function(body) { + return $.ajax({ + url: "matrix-chat/verification", type: "POST", + contentType: "application/json", data: JSON.stringify(body), + }); + }; + var vlClearTimers = function() { + if(vlListTimer) { clearInterval(vlListTimer); vlListTimer = null; } + if(vlTickTimer) { clearInterval(vlTickTimer); vlTickTimer = null; } + if(vlDetailTimer) { clearInterval(vlDetailTimer); vlDetailTimer = null; } + }; + var vlDur = function(ms) { + var s = Math.max(0, Math.round(ms / 1000)); + if(s < 60) { return s + "s"; } + var m = Math.floor(s / 60), r = s % 60; + return m + "m " + (r < 10 ? "0" : "") + r + "s"; + }; + var vlState = function(icon, color, html) { + $("#matrix-vl-d-state").html('' + html + ''); + }; + + var vlTick = function() { + var now = Date.now(); + $("#matrix-vl-items .matrix-vl-item").each(function() { + var $it = $(this); + var seen = parseInt($it.attr("data-seen"), 10); + var expires = $it.attr("data-expires"); + $it.find(".matrix-vl-age").text(isNaN(seen) ? "" : "age " + vlDur(now - seen)); + if(expires) { + var left = parseInt(expires, 10) - now; + $it.find(".matrix-vl-exp").text(left > 0 ? "expires in " + vlDur(left) : "expired"); + } + }); + }; + var vlRenderList = function(data) { + if(data.result !== "ok") { + $("#matrix-vl-items").html('
' + vlEsc(data.message) + '
'); + $("#matrix-vl-empty,#matrix-vl-more").hide(); + return; + } + var now = Date.now(); + var $items = $("#matrix-vl-items").empty(); + (data.verifications || []).forEach(function(v) { + var typeLabel = v.type === "room" ? "Room verification" : "Device verification"; + var sub = "from " + vlEsc(v.userId || "unknown"); + if(v.type === "device" && v.deviceId) { sub += " · device " + vlEsc(v.deviceId); } + else if(v.type === "room" && v.roomId) { sub += " · in room " + vlEsc(v.roomId); } + if(v.isSelfVerification) { sub += " · your own session"; } + $items.append($('
') + .attr("data-vid", v.verificationId) + .attr("data-seen", now - (v.ageMs || 0)) + .attr("data-expires", (v.expiresInMs != null) ? (now + v.expiresInMs) : "") + .html('
' + vlEsc(typeLabel) + '
' + + '
' + sub + '
' + + '
' + + '
')); + }); + $("#matrix-vl-empty").toggle(!(data.verifications || []).length); + if(data.hidden > 0) { + $("#matrix-vl-more").text(data.hidden + " older request" + (data.hidden === 1 ? "" : "s") + " hidden.").show(); + } else { + $("#matrix-vl-more").hide(); + } + vlTick(); + }; + var vlLoadList = function() { + vlCall({ id: vlId(), action: "list" }) + .done(vlRenderList) + .fail(function() { vlRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); }); + }; + var vlShowList = function() { + vlClearTimers(); + $("#matrix-vl-overlay").data("vlConfirmed", false); + $("#matrix-vl-detailview").hide(); + $("#matrix-vl-listview").show(); + $("#matrix-vl-back,#matrix-vl-cancel").hide(); + $("#matrix-vl-items").html('
Loading…
'); + $("#matrix-vl-empty,#matrix-vl-more").hide(); + vlLoadList(); + vlListTimer = setInterval(vlLoadList, 5000); + vlTickTimer = setInterval(vlTick, 1000); + }; + + var vlRenderSas = function(sas) { + var $sas = $("#matrix-vl-d-sas").empty(); + if(sas.emoji && sas.emoji.length) { + sas.emoji.forEach(function(pair) { + $sas.append('
' + vlEsc(pair[0]) + + '
' + vlEsc(pair[1]) + '
'); + }); + } else if(sas.decimal) { + $sas.append('
' + vlEsc(sas.decimal.join(" ")) + '
'); + } + }; + var vlRenderDetail = function(v) { + var typeLabel = v.type === "room" ? "Room verification" : "Device verification"; + var head = vlEsc(typeLabel); + if(v.userId) { head += " — " + vlEsc(v.userId); } + if(v.deviceId) { head += " (device " + vlEsc(v.deviceId) + ")"; } + $("#matrix-vl-d-head").html(head); + + if(v.phase === "done") { + vlClearTimers(); + vlState("fa-check-circle", "#3a9a4e", "Verified. This session is now verified."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if(v.phase === "cancelled") { + vlClearTimers(); + var why = v.cancellationCode ? (" (" + vlEsc(v.cancellationCode) + ")") : ""; + vlState("fa-times-circle", "#c9302c", "Verification was cancelled" + why + "."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if(v.phase === "gone") { + vlClearTimers(); + vlState("fa-exclamation-triangle", "#888", "This verification is no longer available."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if($("#matrix-vl-overlay").data("vlConfirmed")) { + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Waiting for the other device to confirm…"); + return; + } + if(v.sas && (v.sas.emoji || v.sas.decimal)) { + vlState("fa-key", "#d18a1b", "Compare these emoji with the other device, then choose below."); + vlRenderSas(v.sas); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").show(); + return; + } + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Waiting for the verification to start…"); + }; + var vlPollDetail = function() { + vlCall({ id: vlId(), action: "advance", verificationId: vlCurId() }) + .done(function(data) { + if(data.result !== "ok") { + vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message)); + return; + } + vlRenderDetail(data.verification); + }) + .fail(function() { vlState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?"); }); + }; + var vlShowDetail = function(vid) { + vlClearTimers(); + $("#matrix-vl-overlay").data("vlCurrentId", vid).data("vlConfirmed", false); + $("#matrix-vl-listview").hide(); + $("#matrix-vl-detailview").show(); + $("#matrix-vl-d-sas,#matrix-vl-d-actions,#matrix-vl-d-result").hide(); + $("#matrix-vl-d-head").text(""); + vlState("fa-spinner fa-spin", "#888", "Starting verification…"); + $("#matrix-vl-back,#matrix-vl-cancel").show(); + vlPollDetail(); + vlDetailTimer = setInterval(vlPollDetail, 1500); + }; + + var vlClose = function() { vlClearTimers(); $("#matrix-vl-overlay").fadeOut(120); }; + + $("#matrix-vl-x,#matrix-vl-close").on("click", vlClose); + $("#matrix-vl-overlay").on("mousedown", function(e) { if(e.target === this) { vlClose(); } }); + $(document).on("keydown.matrixvl", function(e) { + if(e.key === "Escape" && $("#matrix-vl-overlay").is(":visible")) { vlClose(); } + }); + $("#matrix-vl-back").on("click", vlShowList); + $("#matrix-vl-items").on("click", ".matrix-vl-item", function() { + vlShowDetail($(this).attr("data-vid")); + }); + $("#matrix-vl-cancel").on("click", function() { + vlCall({ id: vlId(), action: "cancel", verificationId: vlCurId() }).always(vlPollDetail); + }); + $("#matrix-vl-d-confirm").on("click", function() { + $("#matrix-vl-overlay").data("vlConfirmed", true); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Confirming…"); + vlCall({ id: vlId(), action: "confirm", verificationId: vlCurId() }) + .done(function(data) { + if(data.result !== "ok") { + $("#matrix-vl-overlay").data("vlConfirmed", false); + vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message)); + $("#matrix-vl-d-actions").show(); + } + }); + }); + $("#matrix-vl-d-mismatch").on("click", function() { + $("#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Cancelling…"); + vlCall({ id: vlId(), action: "mismatch", verificationId: vlCurId() }).always(vlPollDetail); + }); + + $("#matrix-vl-overlay").data("vlShowListFn", vlShowList); + $("#matrix-vl-overlay").data("vlShowDetailFn", vlShowDetail); + } + + $("#matrix-verification-list-btn").on("click", function() { + $("#matrix-vl-overlay").data("matrixNodeId", nodeId).fadeIn(120); + $("#matrix-vl-overlay").data("vlShowListFn")(); + }); + + // --- Sessions (modal) --- + // Built once and reused; the node id is stored on the overlay. + if (!document.getElementById("matrix-ss-overlay")) { + $('').appendTo("head"); + + $('
' + + '
Sessions' + + '×
' + + '
' + + '
' + + '
Current session
' + + '
' + + '
' + + '
Other sessions
' + + '
' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '' + + '' + + '' + + '
').appendTo(document.body); + + var ssEsc = function(s) { return $("
").text(s == null ? "" : String(s)).html(); }; + var ssId = function() { return $("#matrix-ss-overlay").data("matrixNodeId"); }; + var ssCall = function(body) { + return $.ajax({ + url: "matrix-chat/sessions", type: "POST", + contentType: "application/json", data: JSON.stringify(body), + }); + }; + var ssRelative = function(ts) { + if(!ts) { return "activity unknown"; } + var days = Math.floor((Date.now() - ts) / 86400000); + if(days <= 0) { return "active today"; } + if(days === 1) { return "active yesterday"; } + if(days < 90) { return "active " + days + " days ago"; } + return "inactive for 90+ days"; + }; + var ssFullDate = function(ts) { + return ts ? new Date(ts).toLocaleString() : "unknown"; + }; + var ssShield = function(verified) { + return ''; + }; + var ssCard = function(d, isCurrent) { + var sub = (d.verified ? 'Verified' : 'Not verified') + + ' · ' + ssRelative(d.lastSeenTs) + + (d.lastSeenIp ? (' · ' + d.lastSeenIp) : ''); + return $('
') + .data("device", d).data("isCurrent", isCurrent) + .html(ssShield(d.verified) + + '
' + + ssEsc(d.displayName || d.deviceId) + '
' + + '
' + ssEsc(sub) + '
' + + ''); + }; + var ssResult = function(ok, text) { + $("#matrix-ss-d-result").removeClass("ok err").addClass(ok ? "ok" : "err").text(text).show(); + }; + var ssRenderList = function(data) { + if(data.result !== "ok") { + $("#matrix-ss-current").html('
' + ssEsc(data.message) + '
'); + $("#matrix-ss-currentmsg,#matrix-ss-others").empty(); + $("#matrix-ss-others-empty,#matrix-ss-more").hide(); + return; + } + $("#matrix-ss-current").empty().append(ssCard(data.current, true)); + $("#matrix-ss-currentmsg").html(data.current.verified + ? '
Verified session
' + + 'This session is cross-signed and ready for secure messaging.
' + : '
Not verified
' + + 'This session is not cross-signed. Use the Set up secure backup & cross-signing ' + + 'button to verify it.
'); + var $others = $("#matrix-ss-others").empty(); + (data.others || []).forEach(function(d) { $others.append(ssCard(d, false)); }); + $("#matrix-ss-others-empty").toggle(!(data.others || []).length); + if(data.hidden > 0) { + $("#matrix-ss-more").text(data.hidden + " more session" + (data.hidden === 1 ? "" : "s") + " hidden.").show(); + } else { + $("#matrix-ss-more").hide(); + } + }; + var ssShowList = function() { + $("#matrix-ss-detailview").hide(); + $("#matrix-ss-listview").show(); + $("#matrix-ss-back").hide(); + $("#matrix-ss-current").html('
Loading…
'); + $("#matrix-ss-currentmsg,#matrix-ss-others").empty(); + $("#matrix-ss-others-empty,#matrix-ss-more").hide(); + ssCall({ id: ssId(), action: "list" }) + .done(ssRenderList) + .fail(function() { ssRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); }); + }; + var ssDetailRow = function(k, v) { + return '
' + ssEsc(k) + '' + ssEsc(v) + '
'; + }; + var ssShowDetail = function(d, isCurrent) { + $("#matrix-ss-overlay").data("ssDevice", d).data("ssIsCurrent", isCurrent); + $("#matrix-ss-listview").hide(); + $("#matrix-ss-detailview").show(); + $("#matrix-ss-back").show(); + $("#matrix-ss-d-result,#matrix-ss-d-removeconfirm").hide(); + $("#matrix-ss-d-name").text(d.displayName || d.deviceId); + $("#matrix-ss-d-status").html(d.verified + ? '
Verified session
' + + 'This session is ready for secure messaging.
' + : '
Not verified
' + + (isCurrent + ? 'This session is not cross-signed. Use the Set up secure backup & cross-signing button to verify it.' + : 'Verify this session to confirm it is trusted.') + + '
'); + $("#matrix-ss-d-details").html( + ssDetailRow("Session ID", d.deviceId) + + ssDetailRow("Last activity", ssFullDate(d.lastSeenTs)) + + ssDetailRow("IP address", d.lastSeenIp || "unknown")); + $("#matrix-ss-d-verifywrap").toggle(!isCurrent && !d.verified); + $("#matrix-ss-d-removewrap").toggle(!isCurrent); + }; + var ssClose = function() { $("#matrix-ss-overlay").fadeOut(120); }; + + $("#matrix-ss-x,#matrix-ss-close").on("click", ssClose); + $("#matrix-ss-overlay").on("mousedown", function(e) { if(e.target === this) { ssClose(); } }); + $(document).on("keydown.matrixss", function(e) { + if(e.key === "Escape" && $("#matrix-ss-overlay").is(":visible")) { ssClose(); } + }); + $("#matrix-ss-back").on("click", ssShowList); + $("#matrix-ss-current,#matrix-ss-others").on("click", ".matrix-ss-item", function() { + ssShowDetail($(this).data("device"), $(this).data("isCurrent")); + }); + $("#matrix-ss-d-rename").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + var name = prompt("Session display name:", d.displayName || ""); + if(name === null) { return; } + ssCall({ id: ssId(), action: "rename", deviceId: d.deviceId, displayName: name }) + .done(function(r) { + if(r.result !== "ok") { ssResult(false, r.message); return; } + d.displayName = name; + $("#matrix-ss-d-name").text(name || d.deviceId); + ssResult(true, "Session renamed."); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + $("#matrix-ss-d-verify").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + $("#matrix-ss-d-result").removeClass("ok err").html(' Starting verification…').show(); + ssCall({ id: ssId(), action: "verify", deviceId: d.deviceId }) + .done(function(r) { + if(r.result !== "ok" || !r.verificationId) { + ssResult(false, r.message || "Could not start verification."); + return; + } + // hand off to the verification modal's detail view + $("#matrix-ss-overlay").fadeOut(120); + $("#matrix-vl-overlay").data("matrixNodeId", ssId()).fadeIn(120); + $("#matrix-vl-overlay").data("vlShowDetailFn")(r.verificationId); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + $("#matrix-ss-d-removelink").on("click", function() { + $("#matrix-ss-d-password").val(""); + $("#matrix-ss-d-removeconfirm").show(); + }); + $("#matrix-ss-d-removeconfirmbtn").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + $("#matrix-ss-d-result").removeClass("ok err").html(' Removing session…').show(); + ssCall({ id: ssId(), action: "remove", deviceId: d.deviceId, password: $("#matrix-ss-d-password").val() }) + .done(function(r) { + if(r.result !== "ok") { ssResult(false, r.message); return; } + ssShowList(); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + + $("#matrix-ss-overlay").data("ssShowListFn", ssShowList); + } + + $("#matrix-sessions-btn").on("click", function() { + $("#matrix-ss-overlay").data("matrixNodeId", nodeId).fadeIn(120); + $("#matrix-ss-overlay").data("ssShowListFn")(); + }); + // --- Login: fetch a fresh access token & device id --- $("#matrix-login-btn").on("click", function() { function prettyPrintJson(json) { @@ -372,6 +905,24 @@ Sets up cross-signing so the bot's own device shows as verified. The server configuration must be deployed and connected first.
+ +
+ + +
+
+ Review and complete incoming device verification requests without building a flow. + The server configuration must be deployed and connected first. +
+ +
+ + +
+
+ View the account's logged-in sessions, verify them, or remove ones you don't recognize. + The server configuration must be deployed and connected first. +