mirror of
https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git
synced 2026-05-23 07:33:37 -06:00
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.
This commit is contained in:
@@ -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")) {
|
||||
$('<style>'
|
||||
+ '.matrix-vl-overlay{position:fixed;inset:0;z-index:3000;display:none;background:rgba(0,0,0,.45);}'
|
||||
+ '.matrix-vl-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
|
||||
+ 'width:560px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;'
|
||||
+ 'border-radius:6px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.45);'
|
||||
+ 'background:var(--red-ui-primary-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-vl-head{display:flex;justify-content:space-between;align-items:center;'
|
||||
+ 'padding:12px 16px;font-size:15px;font-weight:bold;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-vl-x{cursor:pointer;font-size:20px;line-height:1;opacity:.55;}'
|
||||
+ '.matrix-vl-x:hover{opacity:1;}'
|
||||
+ '.matrix-vl-body{padding:14px 16px;overflow:auto;}'
|
||||
+ '.matrix-vl-note{font-size:12px;color:var(--red-ui-secondary-text-color,#888);margin-bottom:10px;}'
|
||||
+ '.matrix-vl-item{display:flex;justify-content:space-between;gap:12px;padding:10px 12px;'
|
||||
+ 'margin-bottom:8px;border-radius:5px;cursor:pointer;'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);'
|
||||
+ 'background:var(--red-ui-secondary-background,#f7f7f7);}'
|
||||
+ '.matrix-vl-item:hover{border-color:var(--red-ui-node-border,#999);}'
|
||||
+ '.matrix-vl-item-l{flex:1;min-width:0;}'
|
||||
+ '.matrix-vl-item-title{font-weight:bold;}'
|
||||
+ '.matrix-vl-item-sub{font-size:12px;color:var(--red-ui-secondary-text-color,#888);'
|
||||
+ 'margin-top:2px;word-break:break-word;}'
|
||||
+ '.matrix-vl-item-r{text-align:right;font-size:12px;white-space:nowrap;}'
|
||||
+ '.matrix-vl-exp{color:#d18a1b;}'
|
||||
+ '.matrix-vl-empty,.matrix-vl-more{font-size:13px;'
|
||||
+ 'color:var(--red-ui-secondary-text-color,#888);padding:6px 2px;}'
|
||||
+ '.matrix-vl-d-state{display:flex;gap:10px;align-items:flex-start;font-size:14px;line-height:1.5;}'
|
||||
+ '.matrix-vl-d-state .fa{font-size:18px;}'
|
||||
+ '.matrix-vl-sas{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0;}'
|
||||
+ '.matrix-vl-emoji{width:88px;text-align:center;padding:8px 4px;border-radius:5px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-vl-emoji .e{font-size:30px;line-height:1.3;}'
|
||||
+ '.matrix-vl-emoji .n{font-size:11px;color:var(--red-ui-secondary-text-color,#888);text-transform:capitalize;}'
|
||||
+ '.matrix-vl-result{margin-top:12px;padding:10px 12px;border-radius:4px;font-size:13px;}'
|
||||
+ '.matrix-vl-foot{display:flex;align-items:center;gap:8px;padding:10px 16px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-top:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '</style>').appendTo("head");
|
||||
|
||||
$('<div id="matrix-vl-overlay" class="matrix-vl-overlay"><div class="matrix-vl-modal">'
|
||||
+ '<div class="matrix-vl-head"><span><i class="fa fa-check-circle"></i> Device Verification</span>'
|
||||
+ '<span class="matrix-vl-x" id="matrix-vl-x" title="Close">×</span></div>'
|
||||
+ '<div class="matrix-vl-body">'
|
||||
+ '<div id="matrix-vl-listview">'
|
||||
+ '<div class="matrix-vl-note">Pending verification requests — this list refreshes every 5 seconds. Click a request to verify it.</div>'
|
||||
+ '<div id="matrix-vl-items"></div>'
|
||||
+ '<div id="matrix-vl-empty" class="matrix-vl-empty" style="display:none;">No pending verification requests.</div>'
|
||||
+ '<div id="matrix-vl-more" class="matrix-vl-more" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-vl-detailview" style="display:none;">'
|
||||
+ '<div id="matrix-vl-d-head" class="matrix-vl-item-sub" style="margin-bottom:10px;"></div>'
|
||||
+ '<div id="matrix-vl-d-state" class="matrix-vl-d-state"></div>'
|
||||
+ '<div id="matrix-vl-d-sas" class="matrix-vl-sas" style="display:none;"></div>'
|
||||
+ '<div id="matrix-vl-d-actions" style="display:none;text-align:right;">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-d-mismatch">They don't match</button> '
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-d-confirm">They match</button>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-vl-d-result" class="matrix-vl-result" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-vl-foot">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-back" style="display:none;">← Back to list</button>'
|
||||
+ '<span style="flex:1;"></span>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-cancel" style="display:none;">Cancel verification</button>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-vl-close">Close</button>'
|
||||
+ '</div></div></div>').appendTo(document.body);
|
||||
|
||||
var vlListTimer = null, vlTickTimer = null, vlDetailTimer = null;
|
||||
var vlEsc = function(s) { return $("<div>").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('<i class="fa ' + icon + '" style="color:' + color + ';"></i><span>' + html + '</span>');
|
||||
};
|
||||
|
||||
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('<div class="matrix-vl-empty">' + vlEsc(data.message) + '</div>');
|
||||
$("#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($('<div class="matrix-vl-item">')
|
||||
.attr("data-vid", v.verificationId)
|
||||
.attr("data-seen", now - (v.ageMs || 0))
|
||||
.attr("data-expires", (v.expiresInMs != null) ? (now + v.expiresInMs) : "")
|
||||
.html('<div class="matrix-vl-item-l"><div class="matrix-vl-item-title">' + vlEsc(typeLabel) + '</div>'
|
||||
+ '<div class="matrix-vl-item-sub">' + sub + '</div></div>'
|
||||
+ '<div class="matrix-vl-item-r"><div class="matrix-vl-age"></div>'
|
||||
+ '<div class="matrix-vl-exp"></div></div>'));
|
||||
});
|
||||
$("#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('<div class="matrix-vl-empty">Loading…</div>');
|
||||
$("#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('<div class="matrix-vl-emoji"><div class="e">' + vlEsc(pair[0])
|
||||
+ '</div><div class="n">' + vlEsc(pair[1]) + '</div></div>');
|
||||
});
|
||||
} else if(sas.decimal) {
|
||||
$sas.append('<div style="font-size:24px;font-family:monospace;">' + vlEsc(sas.decimal.join(" ")) + '</div>');
|
||||
}
|
||||
};
|
||||
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", "<b>Verified.</b> 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")) {
|
||||
$('<style>'
|
||||
+ '.matrix-ss-overlay{position:fixed;inset:0;z-index:3000;display:none;background:rgba(0,0,0,.45);}'
|
||||
+ '.matrix-ss-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
|
||||
+ 'width:560px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;'
|
||||
+ 'border-radius:6px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.45);'
|
||||
+ 'background:var(--red-ui-primary-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);}'
|
||||
+ '.matrix-ss-head{display:flex;justify-content:space-between;align-items:center;'
|
||||
+ 'padding:12px 16px;font-size:15px;font-weight:bold;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '.matrix-ss-x{cursor:pointer;font-size:20px;line-height:1;opacity:.55;}'
|
||||
+ '.matrix-ss-x:hover{opacity:1;}'
|
||||
+ '.matrix-ss-body{padding:14px 16px;overflow:auto;}'
|
||||
+ '.matrix-ss-h{font-weight:bold;font-size:12px;text-transform:uppercase;'
|
||||
+ 'letter-spacing:.04em;color:var(--red-ui-secondary-text-color,#888);margin:2px 0 8px;}'
|
||||
+ '.matrix-ss-h.spaced{margin-top:20px;}'
|
||||
+ '.matrix-ss-item{display:flex;align-items:center;gap:10px;padding:10px 12px;'
|
||||
+ 'margin-bottom:8px;border-radius:5px;cursor:pointer;'
|
||||
+ 'border:1px solid var(--red-ui-secondary-border-color,#ddd);'
|
||||
+ 'background:var(--red-ui-secondary-background,#f7f7f7);}'
|
||||
+ '.matrix-ss-item:hover{border-color:var(--red-ui-node-border,#999);}'
|
||||
+ '.matrix-ss-item-l{flex:1;min-width:0;}'
|
||||
+ '.matrix-ss-item-title{font-weight:bold;word-break:break-word;}'
|
||||
+ '.matrix-ss-item-sub{font-size:12px;color:var(--red-ui-secondary-text-color,#888);'
|
||||
+ 'margin-top:2px;word-break:break-word;}'
|
||||
+ '.matrix-ss-shield{font-size:20px;width:22px;text-align:center;flex-shrink:0;}'
|
||||
+ '.matrix-ss-box{display:flex;gap:10px;align-items:flex-start;margin:10px 0;'
|
||||
+ 'padding:10px 12px;border-radius:4px;font-size:13px;line-height:1.45;}'
|
||||
+ '.matrix-ss-box.ok{background:#e7f4ea;border:1px solid #8fcea5;color:#1e6b33;}'
|
||||
+ '.matrix-ss-box.err{background:#fde7e9;border:1px solid #e8a0a8;color:#8a1f2b;}'
|
||||
+ '.matrix-ss-box .fa{font-size:18px;margin-top:1px;}'
|
||||
+ '.matrix-ss-details div{display:flex;font-size:13px;padding:5px 0;'
|
||||
+ 'border-bottom:1px solid var(--red-ui-secondary-border-color,#eee);}'
|
||||
+ '.matrix-ss-details .k{width:130px;flex-shrink:0;color:var(--red-ui-secondary-text-color,#888);}'
|
||||
+ '.matrix-ss-details .v{word-break:break-all;}'
|
||||
+ '.matrix-ss-empty,.matrix-ss-more{font-size:13px;'
|
||||
+ 'color:var(--red-ui-secondary-text-color,#888);padding:6px 2px;}'
|
||||
+ '.matrix-ss-removelink{color:#c9302c;cursor:pointer;font-weight:bold;}'
|
||||
+ '.matrix-ss-result{margin-top:12px;padding:10px 12px;border-radius:4px;font-size:13px;}'
|
||||
+ '.matrix-ss-result.ok{background:#e7f4ea;border:1px solid #8fcea5;color:#1e6b33;}'
|
||||
+ '.matrix-ss-result.err{background:#fde7e9;border:1px solid #e8a0a8;color:#8a1f2b;}'
|
||||
+ '.matrix-ss-foot{display:flex;align-items:center;gap:8px;padding:10px 16px;'
|
||||
+ 'background:var(--red-ui-secondary-background,#f3f3f3);'
|
||||
+ 'border-top:1px solid var(--red-ui-secondary-border-color,#ddd);}'
|
||||
+ '</style>').appendTo("head");
|
||||
|
||||
$('<div id="matrix-ss-overlay" class="matrix-ss-overlay"><div class="matrix-ss-modal">'
|
||||
+ '<div class="matrix-ss-head"><span><i class="fa fa-desktop"></i> Sessions</span>'
|
||||
+ '<span class="matrix-ss-x" id="matrix-ss-x" title="Close">×</span></div>'
|
||||
+ '<div class="matrix-ss-body">'
|
||||
+ '<div id="matrix-ss-listview">'
|
||||
+ '<div class="matrix-ss-h">Current session</div>'
|
||||
+ '<div id="matrix-ss-current"></div>'
|
||||
+ '<div id="matrix-ss-currentmsg"></div>'
|
||||
+ '<div class="matrix-ss-h spaced">Other sessions</div>'
|
||||
+ '<div id="matrix-ss-others"></div>'
|
||||
+ '<div id="matrix-ss-others-empty" class="matrix-ss-empty" style="display:none;">No other sessions.</div>'
|
||||
+ '<div id="matrix-ss-more" class="matrix-ss-more" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="matrix-ss-detailview" style="display:none;">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">'
|
||||
+ '<div id="matrix-ss-d-name" style="font-size:15px;font-weight:bold;word-break:break-word;"></div>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-d-rename">Rename</button></div>'
|
||||
+ '<div id="matrix-ss-d-status"></div>'
|
||||
+ '<div class="matrix-ss-h spaced">Session details</div>'
|
||||
+ '<div id="matrix-ss-d-details" class="matrix-ss-details"></div>'
|
||||
+ '<div id="matrix-ss-d-verifywrap" style="display:none;margin-top:14px;">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-d-verify">Verify this session</button></div>'
|
||||
+ '<div id="matrix-ss-d-removewrap" style="display:none;margin-top:16px;">'
|
||||
+ '<span class="matrix-ss-removelink" id="matrix-ss-d-removelink">Remove this session</span>'
|
||||
+ '<div id="matrix-ss-d-removeconfirm" style="display:none;margin-top:8px;">'
|
||||
+ '<div style="background:#fdf3e7;border:1px solid #f0c36d;color:#7a5b16;border-radius:4px;'
|
||||
+ 'padding:8px 10px;font-size:13px;margin-bottom:8px;">Removing a session signs it out. '
|
||||
+ 'Enter the account password to confirm.</div>'
|
||||
+ '<input type="password" id="matrix-ss-d-password" placeholder="Account password" '
|
||||
+ 'style="width:100%;box-sizing:border-box;padding:7px 9px;border-radius:4px;'
|
||||
+ 'border:1px solid var(--red-ui-form-input-border-color,#ccc);'
|
||||
+ 'background:var(--red-ui-form-input-background,#fff);color:var(--red-ui-primary-text-color,#2a2a2a);">'
|
||||
+ '<div style="text-align:right;margin-top:8px;"><button type="button" class="red-ui-button" '
|
||||
+ 'id="matrix-ss-d-removeconfirmbtn">Confirm removal</button></div>'
|
||||
+ '</div></div>'
|
||||
+ '<div id="matrix-ss-d-result" class="matrix-ss-result" style="display:none;"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="matrix-ss-foot">'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-back" style="display:none;">← Back</button>'
|
||||
+ '<span style="flex:1;"></span>'
|
||||
+ '<button type="button" class="red-ui-button" id="matrix-ss-close">Close</button>'
|
||||
+ '</div></div></div>').appendTo(document.body);
|
||||
|
||||
var ssEsc = function(s) { return $("<div>").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 '<i class="fa fa-shield matrix-ss-shield" style="color:'
|
||||
+ (verified ? '#3a9a4e' : '#c9302c') + ';" title="'
|
||||
+ (verified ? 'Verified' : 'Not verified') + '"></i>';
|
||||
};
|
||||
var ssCard = function(d, isCurrent) {
|
||||
var sub = (d.verified ? 'Verified' : 'Not verified')
|
||||
+ ' · ' + ssRelative(d.lastSeenTs)
|
||||
+ (d.lastSeenIp ? (' · ' + d.lastSeenIp) : '');
|
||||
return $('<div class="matrix-ss-item">')
|
||||
.data("device", d).data("isCurrent", isCurrent)
|
||||
.html(ssShield(d.verified)
|
||||
+ '<div class="matrix-ss-item-l"><div class="matrix-ss-item-title">'
|
||||
+ ssEsc(d.displayName || d.deviceId) + '</div>'
|
||||
+ '<div class="matrix-ss-item-sub">' + ssEsc(sub) + '</div></div>'
|
||||
+ '<i class="fa fa-angle-right" style="opacity:.5;"></i>');
|
||||
};
|
||||
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('<div class="matrix-ss-empty">' + ssEsc(data.message) + '</div>');
|
||||
$("#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
|
||||
? '<div class="matrix-ss-box ok"><i class="fa fa-shield"></i><div><b>Verified session</b><br>'
|
||||
+ 'This session is cross-signed and ready for secure messaging.</div></div>'
|
||||
: '<div class="matrix-ss-box err"><i class="fa fa-shield"></i><div><b>Not verified</b><br>'
|
||||
+ 'This session is not cross-signed. Use the Set up secure backup & cross-signing '
|
||||
+ 'button to verify it.</div></div>');
|
||||
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('<div class="matrix-ss-empty">Loading…</div>');
|
||||
$("#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 '<div><span class="k">' + ssEsc(k) + '</span><span class="v">' + ssEsc(v) + '</span></div>';
|
||||
};
|
||||
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
|
||||
? '<div class="matrix-ss-box ok"><i class="fa fa-shield"></i><div><b>Verified session</b><br>'
|
||||
+ 'This session is ready for secure messaging.</div></div>'
|
||||
: '<div class="matrix-ss-box err"><i class="fa fa-shield"></i><div><b>Not verified</b><br>'
|
||||
+ (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.')
|
||||
+ '</div></div>');
|
||||
$("#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('<i class="fa fa-spinner fa-spin"></i> 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('<i class="fa fa-spinner fa-spin"></i> 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.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-check-circle"></i> Verification</label>
|
||||
<button type="button" class="red-ui-button" id="matrix-verification-list-btn">Pending verification requests</button>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
Review and complete incoming device verification requests without building a flow.
|
||||
The server configuration must be deployed and connected first.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-desktop"></i> Sessions</label>
|
||||
<button type="button" class="red-ui-button" id="matrix-sessions-btn">Manage sessions</button>
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom: 12px;">
|
||||
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.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-server-config">
|
||||
|
||||
+250
-2
@@ -382,6 +382,7 @@ module.exports = function(RED) {
|
||||
return; // already tracked
|
||||
}
|
||||
node.verificationRequests.set(id, request);
|
||||
request.__nrSeenAt = Date.now();
|
||||
|
||||
let verifierHooked = false;
|
||||
const onChange = function() {
|
||||
@@ -399,8 +400,13 @@ module.exports = function(RED) {
|
||||
emitVerificationUpdate(request, false);
|
||||
if(request.phase === VerificationPhase.Done || request.phase === VerificationPhase.Cancelled) {
|
||||
request.off(VerificationRequestEvent.Change, onChange);
|
||||
node.verificationRequests.delete(id);
|
||||
node.verificationSas.delete(id);
|
||||
// Keep the finished request briefly so the config
|
||||
// editor's verification list can still report the
|
||||
// outcome; the /matrix-chat/verification "list"
|
||||
// action sweeps entries older than 2 minutes.
|
||||
if(!request.__nrEndedAt) {
|
||||
request.__nrEndedAt = Date.now();
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
node.error("Verification request handler error: " + e);
|
||||
@@ -933,6 +939,248 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Lists and drives device verification requests for the config editor's
|
||||
* "Verification" button (the verification list modal). Same flows.write
|
||||
* protection as the other admin endpoints, so it is not publicly exposed.
|
||||
*
|
||||
* Actions (on req.body.id, the server config node):
|
||||
* - list : the pending verification requests (newest 20)
|
||||
* - advance : accept / start SAS for one request and return its state
|
||||
* - confirm : confirm the SAS emoji match
|
||||
* - mismatch: declare the SAS emoji do not match
|
||||
* - cancel : cancel the verification
|
||||
*/
|
||||
RED.httpAdmin.post(
|
||||
"/matrix-chat/verification",
|
||||
RED.auth.needsPermission('flows.write'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
const serverNode = RED.nodes.getNode(req.body.id);
|
||||
if(!serverNode || !serverNode.matrixClient) {
|
||||
return res.json({ result: 'error', message: 'Server configuration not found. Save and deploy the server configuration node first.' });
|
||||
}
|
||||
if(typeof serverNode.isConnected !== 'function' || !serverNode.isConnected()) {
|
||||
return res.json({ result: 'error', message: 'The Matrix client is not connected.' });
|
||||
}
|
||||
if(!serverNode.matrixClient.getCrypto()) {
|
||||
return res.json({ result: 'error', message: 'End-to-end encryption is not enabled on this server configuration.' });
|
||||
}
|
||||
|
||||
const { VerificationPhase } = await cryptoApiPromise;
|
||||
const PHASE_NAMES = { 1: 'unsent', 2: 'requested', 3: 'ready', 4: 'started', 5: 'cancelled', 6: 'done' };
|
||||
const requests = serverNode.verificationRequests;
|
||||
const sasMap = serverNode.verificationSas;
|
||||
const action = req.body.action || 'list';
|
||||
|
||||
function safe(fn, fallback) {
|
||||
try { return fn(); } catch(e) { return fallback; }
|
||||
}
|
||||
function detailOf(vid, r) {
|
||||
const roomId = safe(function(){ return r.roomId; }, null) || null;
|
||||
const timeout = safe(function(){ return r.timeout; }, null);
|
||||
const sas = sasMap.get(vid);
|
||||
return {
|
||||
verificationId : vid,
|
||||
phase : PHASE_NAMES[safe(function(){ return r.phase; })] || 'unknown',
|
||||
userId : safe(function(){ return r.otherUserId; }, null),
|
||||
deviceId : safe(function(){ return r.otherDeviceId; }, null) || null,
|
||||
roomId : roomId,
|
||||
type : roomId ? 'room' : 'device',
|
||||
isSelfVerification: safe(function(){ return r.isSelfVerification; }, false),
|
||||
initiatedByMe : safe(function(){ return r.initiatedByMe; }, false),
|
||||
ageMs : Date.now() - (r.__nrSeenAt || Date.now()),
|
||||
expiresInMs : (typeof timeout === 'number') ? timeout : null,
|
||||
cancellationCode : safe(function(){ return r.cancellationCode; }, null),
|
||||
sas : (sas && sas.sas) ? { emoji: sas.sas.emoji || null, decimal: sas.sas.decimal || null } : null,
|
||||
};
|
||||
}
|
||||
|
||||
if(action === 'list') {
|
||||
const now = Date.now();
|
||||
// sweep finished verifications kept only for recent lookups
|
||||
for(const entry of Array.from(requests)) {
|
||||
if(entry[1].__nrEndedAt && (now - entry[1].__nrEndedAt) > 120000) {
|
||||
requests.delete(entry[0]);
|
||||
sasMap.delete(entry[0]);
|
||||
}
|
||||
}
|
||||
let items = [];
|
||||
for(const entry of requests) {
|
||||
const detail = detailOf(entry[0], entry[1]);
|
||||
if(detail.phase === 'done' || detail.phase === 'cancelled' || detail.phase === 'unsent') {
|
||||
continue;
|
||||
}
|
||||
items.push(detail);
|
||||
}
|
||||
items.sort(function(a, b) { return a.ageMs - b.ageMs; }); // newest first
|
||||
return res.json({
|
||||
result: 'ok',
|
||||
refreshSeconds: 5,
|
||||
total: items.length,
|
||||
hidden: Math.max(0, items.length - 20),
|
||||
verifications: items.slice(0, 20),
|
||||
});
|
||||
}
|
||||
|
||||
// remaining actions operate on a single verification
|
||||
const request = requests.get(req.body.verificationId);
|
||||
if(!request) {
|
||||
return res.json({ result: 'ok', verification: { verificationId: req.body.verificationId, phase: 'gone' } });
|
||||
}
|
||||
|
||||
if(action === 'advance') {
|
||||
try {
|
||||
const phase = safe(function(){ return request.phase; });
|
||||
if(phase === VerificationPhase.Requested
|
||||
&& !safe(function(){ return request.initiatedByMe; }, false)
|
||||
&& !safe(function(){ return request.accepting; }, false)) {
|
||||
await request.accept();
|
||||
} else if(phase === VerificationPhase.Ready && !safe(function(){ return request.verifier; })) {
|
||||
await request.startVerification("m.sas.v1");
|
||||
}
|
||||
const verifier = safe(function(){ return request.verifier; });
|
||||
if(verifier && !request.__nrVerifyCalled) {
|
||||
request.__nrVerifyCalled = true;
|
||||
verifier.verify().catch(function(){ /* completes/cancels elsewhere */ });
|
||||
}
|
||||
} catch(e) {
|
||||
serverNode.warn("Verification advance error: " + e);
|
||||
}
|
||||
return res.json({ result: 'ok', verification: detailOf(req.body.verificationId, request) });
|
||||
}
|
||||
|
||||
if(action === 'confirm' || action === 'mismatch') {
|
||||
const sas = sasMap.get(req.body.verificationId);
|
||||
if(!sas) {
|
||||
return res.json({ result: 'error', message: 'This verification has no SAS awaiting confirmation yet.' });
|
||||
}
|
||||
if(action === 'confirm') {
|
||||
await sas.confirm();
|
||||
} else {
|
||||
sas.mismatch();
|
||||
}
|
||||
return res.json({ result: 'ok', verification: detailOf(req.body.verificationId, request) });
|
||||
}
|
||||
|
||||
if(action === 'cancel') {
|
||||
await request.cancel();
|
||||
return res.json({ result: 'ok', verification: detailOf(req.body.verificationId, request) });
|
||||
}
|
||||
|
||||
return res.json({ result: 'error', message: 'Unknown action: ' + action });
|
||||
} catch(error) {
|
||||
res.json({ result: 'error', message: String(error && error.message || error) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Session (device) management for the config editor's "Sessions" button.
|
||||
* Same flows.write protection as the other admin endpoints.
|
||||
*
|
||||
* Actions (on req.body.id, the server config node):
|
||||
* - list : the account's sessions (current + others) with verification state
|
||||
* - rename : set a session's display name
|
||||
* - remove : delete a session (requires the account password)
|
||||
* - verify : start verifying a session; returns a verificationId to hand
|
||||
* off to the verification modal
|
||||
*/
|
||||
RED.httpAdmin.post(
|
||||
"/matrix-chat/sessions",
|
||||
RED.auth.needsPermission('flows.write'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
const serverNode = RED.nodes.getNode(req.body.id);
|
||||
if(!serverNode || !serverNode.matrixClient) {
|
||||
return res.json({ result: 'error', message: 'Server configuration not found. Save and deploy the server configuration node first.' });
|
||||
}
|
||||
if(typeof serverNode.isConnected !== 'function' || !serverNode.isConnected()) {
|
||||
return res.json({ result: 'error', message: 'The Matrix client is not connected.' });
|
||||
}
|
||||
const client = serverNode.matrixClient;
|
||||
const crypto = client.getCrypto();
|
||||
if(!crypto) {
|
||||
return res.json({ result: 'error', message: 'End-to-end encryption is not enabled on this server configuration.' });
|
||||
}
|
||||
const action = req.body.action || 'list';
|
||||
|
||||
if(action === 'list') {
|
||||
const currentDeviceId = client.getDeviceId();
|
||||
const devices = (await client.getDevices()).devices || [];
|
||||
const enriched = await Promise.all(devices.map(async function(d) {
|
||||
let verified = false;
|
||||
try {
|
||||
const status = await crypto.getDeviceVerificationStatus(serverNode.userId, d.device_id);
|
||||
verified = !!(status && status.isVerified());
|
||||
} catch(e) { /* unknown - treat as unverified */ }
|
||||
return {
|
||||
deviceId : d.device_id,
|
||||
displayName : d.display_name || null,
|
||||
lastSeenTs : d.last_seen_ts || null,
|
||||
lastSeenIp : d.last_seen_ip || null,
|
||||
verified : verified,
|
||||
};
|
||||
}));
|
||||
const current = enriched.find(function(d){ return d.deviceId === currentDeviceId; })
|
||||
|| { deviceId: currentDeviceId, displayName: null, lastSeenTs: null, lastSeenIp: null, verified: false };
|
||||
let others = enriched.filter(function(d){ return d.deviceId !== currentDeviceId; });
|
||||
others.sort(function(a, b){ return (b.lastSeenTs || 0) - (a.lastSeenTs || 0); });
|
||||
return res.json({
|
||||
result: 'ok',
|
||||
current: current,
|
||||
others: others.slice(0, 50),
|
||||
hidden: Math.max(0, others.length - 50),
|
||||
});
|
||||
}
|
||||
|
||||
const deviceId = req.body.deviceId;
|
||||
if(!deviceId) {
|
||||
return res.json({ result: 'error', message: 'A deviceId is required.' });
|
||||
}
|
||||
|
||||
if(action === 'rename') {
|
||||
await client.setDeviceDetails(deviceId, { display_name: req.body.displayName || '' });
|
||||
return res.json({ result: 'ok' });
|
||||
}
|
||||
|
||||
if(action === 'remove') {
|
||||
const password = req.body.password;
|
||||
try {
|
||||
await client.deleteDevice(deviceId);
|
||||
} catch(e) {
|
||||
// deleting a device is user-interactive-auth protected
|
||||
if(e && e.httpStatus === 401 && e.data && e.data.flows) {
|
||||
if(!password) {
|
||||
return res.json({ result: 'error', message: 'The account password is required to remove a session.' });
|
||||
}
|
||||
await client.deleteDevice(deviceId, {
|
||||
type: 'm.login.password',
|
||||
identifier: { type: 'm.id.user', user: serverNode.userId },
|
||||
password: password,
|
||||
session: e.data.session,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
serverNode.log("Removed session " + deviceId);
|
||||
return res.json({ result: 'ok', message: 'Session removed.' });
|
||||
}
|
||||
|
||||
if(action === 'verify') {
|
||||
const request = await crypto.requestDeviceVerification(serverNode.userId, deviceId);
|
||||
if(typeof serverNode.trackVerificationRequest === 'function') {
|
||||
serverNode.trackVerificationRequest(request);
|
||||
}
|
||||
return res.json({ result: 'ok', verificationId: request.transactionId || null });
|
||||
}
|
||||
|
||||
return res.json({ result: 'error', message: 'Unknown action: ' + action });
|
||||
} catch(error) {
|
||||
res.json({ result: 'error', message: String(error && error.message || error) });
|
||||
}
|
||||
});
|
||||
|
||||
function upgradeDirectoryIfNecessary(node, storageDir) {
|
||||
let oldStorageDir = './matrix-local-storage',
|
||||
oldStorageDir2 = './matrix-client-storage';
|
||||
|
||||
Reference in New Issue
Block a user