@arcblock/did-connect-service 4.0.6 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_generated/asset-bytes.d.ts +3 -0
- package/dist/_generated/asset-bytes.d.ts.map +1 -0
- package/dist/_generated/asset-bytes.js +2 -0
- package/dist/_generated/asset-bytes.js.map +1 -0
- package/dist/_generated/asset-manifest.d.ts +3 -0
- package/dist/_generated/asset-manifest.d.ts.map +1 -0
- package/dist/_generated/asset-manifest.js +12 -0
- package/dist/_generated/asset-manifest.js.map +1 -0
- package/dist/asset-registry.d.ts +38 -0
- package/dist/asset-registry.d.ts.map +1 -0
- package/dist/asset-registry.js +73 -0
- package/dist/asset-registry.js.map +1 -0
- package/dist/assets/admin-core.c0b5af61.js +1393 -0
- package/dist/assets/admin-extra.7ca9c16b.js +2529 -0
- package/dist/assets/admin.c26bb17a.css +2219 -0
- package/dist/assets/design.99dc4ddc.css +97 -0
- package/dist/assets/did-address.7df30f28.js +51 -0
- package/dist/assets/header.94d9e46b.js +136 -0
- package/dist/assets/login.7b12c6dc.css +662 -0
- package/dist/assets/login.d3f05790.js +720 -0
- package/dist/assets/qr.c0d203ca.js +3 -0
- package/dist/blocklet-service.d.ts +7 -32
- package/dist/blocklet-service.d.ts.map +1 -1
- package/dist/blocklet-service.js +43 -37
- package/dist/blocklet-service.js.map +1 -1
- package/dist/env-config.d.ts +86 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +33 -0
- package/dist/env-config.js.map +1 -0
- package/dist/handlers/auth-handler.d.ts +1 -1
- package/dist/handlers/auth-handler.d.ts.map +1 -1
- package/dist/handlers/auth-handler.js +11 -11
- package/dist/handlers/auth-handler.js.map +1 -1
- package/dist/handlers/branding-handler.d.ts.map +1 -1
- package/dist/handlers/branding-handler.js +3 -2
- package/dist/handlers/branding-handler.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/og/generator.d.ts.map +1 -1
- package/dist/og/generator.js +8 -2
- package/dist/og/generator.js.map +1 -1
- package/dist/og/index.d.ts +2 -0
- package/dist/og/index.d.ts.map +1 -1
- package/dist/og/index.js +1 -0
- package/dist/og/index.js.map +1 -1
- package/dist/og/node-config.d.ts +23 -0
- package/dist/og/node-config.d.ts.map +1 -0
- package/dist/og/node-config.js +22 -0
- package/dist/og/node-config.js.map +1 -0
- package/dist/og/types.d.ts +10 -5
- package/dist/og/types.d.ts.map +1 -1
- package/dist/og/types.js.map +1 -1
- package/dist/pages/admin/index.d.ts.map +1 -1
- package/dist/pages/admin/index.js +25 -41
- package/dist/pages/admin/index.js.map +1 -1
- package/dist/pages/admin/tab-access.d.ts +1 -1
- package/dist/pages/admin/tab-access.d.ts.map +1 -1
- package/dist/pages/admin/tab-access.js +5 -2
- package/dist/pages/admin/tab-access.js.map +1 -1
- package/dist/pages/admin/tab-appearance.d.ts +1 -1
- package/dist/pages/admin/tab-appearance.d.ts.map +1 -1
- package/dist/pages/admin/tab-appearance.js +4 -2
- package/dist/pages/admin/tab-appearance.js.map +1 -1
- package/dist/pages/admin/tab-branding.d.ts.map +1 -1
- package/dist/pages/admin/tab-branding.js +4 -2
- package/dist/pages/admin/tab-branding.js.map +1 -1
- package/dist/pages/admin/tab-profile-accounts.d.ts.map +1 -1
- package/dist/pages/admin/tab-profile-accounts.js +4 -2
- package/dist/pages/admin/tab-profile-accounts.js.map +1 -1
- package/dist/pages/admin/tab-settings.d.ts.map +1 -1
- package/dist/pages/admin/tab-settings.js +4 -2
- package/dist/pages/admin/tab-settings.js.map +1 -1
- package/dist/pages/admin-instances-page.d.ts.map +1 -1
- package/dist/pages/admin-instances-page.js +4 -6
- package/dist/pages/admin-instances-page.js.map +1 -1
- package/dist/pages/error-page.d.ts.map +1 -1
- package/dist/pages/error-page.js +3 -2
- package/dist/pages/error-page.js.map +1 -1
- package/dist/pages/gen-access-key-page.d.ts.map +1 -1
- package/dist/pages/gen-access-key-page.js +3 -4
- package/dist/pages/gen-access-key-page.js.map +1 -1
- package/dist/pages/homepage.d.ts.map +1 -1
- package/dist/pages/homepage.js +4 -3
- package/dist/pages/homepage.js.map +1 -1
- package/dist/pages/invite-page.d.ts.map +1 -1
- package/dist/pages/invite-page.js +4 -4
- package/dist/pages/invite-page.js.map +1 -1
- package/dist/pages/login-page.d.ts.map +1 -1
- package/dist/pages/login-page.js +3 -4
- package/dist/pages/login-page.js.map +1 -1
- package/package.json +6 -4
- package/dist/identity/csrf.d.ts +0 -17
- package/dist/identity/csrf.d.ts.map +0 -1
- package/dist/identity/csrf.js +0 -56
- package/dist/identity/csrf.js.map +0 -1
|
@@ -0,0 +1,1393 @@
|
|
|
1
|
+
|
|
2
|
+
var toastContainer = null;
|
|
3
|
+
|
|
4
|
+
function ensureToastContainer() {
|
|
5
|
+
if (toastContainer) return;
|
|
6
|
+
toastContainer = document.createElement("div");
|
|
7
|
+
toastContainer.style.cssText = "position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;";
|
|
8
|
+
document.body.appendChild(toastContainer);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function showToast(message, type) {
|
|
12
|
+
ensureToastContainer();
|
|
13
|
+
var toast = document.createElement("div");
|
|
14
|
+
var bg = type === "error" ? "var(--red-bg)" : type === "success" ? "var(--green-bg)" : "var(--bg-card)";
|
|
15
|
+
var color = type === "error" ? "var(--red)" : type === "success" ? "var(--green)" : "var(--text)";
|
|
16
|
+
var border = type === "error" ? "var(--red)" : type === "success" ? "var(--green)" : "var(--border)";
|
|
17
|
+
toast.style.cssText = "padding:12px 20px;border-radius:8px;font-size:14px;background:" + bg + ";color:" + color + ";border:1px solid " + border + ";box-shadow:0 4px 12px rgba(0,0,0,0.3);opacity:0;transition:opacity 0.2s;";
|
|
18
|
+
toast.textContent = message;
|
|
19
|
+
toastContainer.appendChild(toast);
|
|
20
|
+
requestAnimationFrame(function() { toast.style.opacity = "1"; });
|
|
21
|
+
setTimeout(function() {
|
|
22
|
+
toast.style.opacity = "0";
|
|
23
|
+
setTimeout(function() { toast.remove(); }, 200);
|
|
24
|
+
}, 3000);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
;
|
|
28
|
+
|
|
29
|
+
function relativeTime(iso) {
|
|
30
|
+
var diff = Date.now() - new Date(iso).getTime();
|
|
31
|
+
var abs = Math.abs(diff);
|
|
32
|
+
var future = diff < 0;
|
|
33
|
+
var s = Math.floor(abs / 1000);
|
|
34
|
+
var m = Math.floor(s / 60);
|
|
35
|
+
var h = Math.floor(m / 60);
|
|
36
|
+
var d = Math.floor(h / 24);
|
|
37
|
+
var label;
|
|
38
|
+
if (d > 0) label = d + "d " + (h % 24) + "h";
|
|
39
|
+
else if (h > 0) label = h + "h " + (m % 60) + "m";
|
|
40
|
+
else if (m > 0) label = m + " min";
|
|
41
|
+
else label = "just now";
|
|
42
|
+
if (label === "just now") return label;
|
|
43
|
+
return future ? "in " + label : label + " ago";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function absoluteTime(iso) {
|
|
47
|
+
var d = new Date(iso);
|
|
48
|
+
var pad = function(n) { return n < 10 ? "0" + n : "" + n; };
|
|
49
|
+
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
50
|
+
+ " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function copyToClipboard(text) {
|
|
54
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
55
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
56
|
+
showToast("Copied!", "success");
|
|
57
|
+
}).catch(function() {
|
|
58
|
+
fallbackCopy(text);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
fallbackCopy(text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fallbackCopy(text) {
|
|
66
|
+
var input = document.createElement("input");
|
|
67
|
+
input.value = text;
|
|
68
|
+
document.body.appendChild(input);
|
|
69
|
+
input.select();
|
|
70
|
+
document.execCommand("copy");
|
|
71
|
+
input.remove();
|
|
72
|
+
showToast("Copied!", "success");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Delegate to the canonical implementation from @arcblock/did-connect-core
|
|
76
|
+
// (exposed globally by the <did-address> IIFE bundle as __DidAddressBundle.truncateDid)
|
|
77
|
+
function truncateDid(did) {
|
|
78
|
+
return __DidAddressBundle.truncateDid(did);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function initials(name) {
|
|
82
|
+
if (!name) return "?";
|
|
83
|
+
var parts = name.trim().split(/\s+/);
|
|
84
|
+
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
85
|
+
return name.slice(0, 2).toUpperCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeHtml(str) {
|
|
89
|
+
var div = document.createElement("div");
|
|
90
|
+
div.textContent = str;
|
|
91
|
+
return div.innerHTML;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Generate a shimmer skeleton table: cols = column count, rows = row count
|
|
95
|
+
function skeletonTable(cols, rows) {
|
|
96
|
+
var widths = [70, 45, 60, 55, 65, 50, 60, 30];
|
|
97
|
+
var cells = '';
|
|
98
|
+
for (var i = 0; i < cols; i++) {
|
|
99
|
+
cells += '<td><span class="skel" style="width:' + widths[i % widths.length] + '%;height:13px;"></span></td>';
|
|
100
|
+
}
|
|
101
|
+
var row = '<tr class="skel-row">' + cells + '</tr>';
|
|
102
|
+
var tbody = '';
|
|
103
|
+
for (var j = 0; j < rows; j++) tbody += row;
|
|
104
|
+
return '<table class="table"><tbody>' + tbody + '</tbody></table>';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Generate shimmer skeleton rows for list-style containers. */
|
|
108
|
+
function skeletonRows(rows) {
|
|
109
|
+
var widths = [45, 65, 55];
|
|
110
|
+
var html = '';
|
|
111
|
+
for (var i = 0; i < rows; i++) {
|
|
112
|
+
html += '<div class="settings-row"><span class="skel" style="width:' + widths[i % widths.length] + '%;height:13px;"></span></div>';
|
|
113
|
+
}
|
|
114
|
+
return html;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderAvatar(avatarUrl, fullName, size) {
|
|
118
|
+
size = size || "";
|
|
119
|
+
var cls = "avatar" + (size ? " avatar-" + size : "");
|
|
120
|
+
if (avatarUrl) {
|
|
121
|
+
var sep = avatarUrl.indexOf("?") >= 0 ? "&" : "?";
|
|
122
|
+
var src = avatarUrl + sep + "t=" + Date.now() + (size === "sm" ? "&s=64" : "");
|
|
123
|
+
return '<div class="' + cls + '">'
|
|
124
|
+
+ '<img src="' + escapeHtml(src) + '" '
|
|
125
|
+
+ 'onerror="this.style.display=\'none\';this.nextSibling.style.display=\'flex\'">'
|
|
126
|
+
+ '<span class="avatar-fallback" style="display:none">' + escapeHtml(initials(fullName)) + '</span>'
|
|
127
|
+
+ '</div>';
|
|
128
|
+
}
|
|
129
|
+
return '<div class="' + cls + '"><span class="avatar-fallback">' + escapeHtml(initials(fullName)) + '</span></div>';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
;
|
|
133
|
+
|
|
134
|
+
async function api(method, path, body) {
|
|
135
|
+
try {
|
|
136
|
+
var opts = {
|
|
137
|
+
method: method,
|
|
138
|
+
headers: {},
|
|
139
|
+
};
|
|
140
|
+
if (body !== undefined) {
|
|
141
|
+
opts.headers["Content-Type"] = "application/json";
|
|
142
|
+
opts.body = JSON.stringify(body);
|
|
143
|
+
}
|
|
144
|
+
var res = await fetch(window.__BASE_PATH + path, opts);
|
|
145
|
+
var data = await res.json();
|
|
146
|
+
|
|
147
|
+
if (res.status === 401) {
|
|
148
|
+
location.href = "/.well-known/service/api/did/logout";
|
|
149
|
+
return { ok: false, error: "Session expired", code: "UNAUTHENTICATED" };
|
|
150
|
+
}
|
|
151
|
+
if (res.status === 403 && data.code === "BLOCKED") {
|
|
152
|
+
location.href = "/.well-known/service/api/did/logout";
|
|
153
|
+
return { ok: false, error: "Account blocked", code: "BLOCKED" };
|
|
154
|
+
}
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
showToast(data.error || "Request failed", "error");
|
|
157
|
+
return { ok: false, error: data.error, code: data.code };
|
|
158
|
+
}
|
|
159
|
+
return data;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
showToast("Network error", "error");
|
|
162
|
+
return { ok: false, error: err.message, code: "NETWORK_ERROR" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
;
|
|
167
|
+
|
|
168
|
+
function showDialog(id) {
|
|
169
|
+
var d = document.getElementById(id);
|
|
170
|
+
if (d && d.showModal) d.showModal();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function closeDialog(id) {
|
|
174
|
+
var d = document.getElementById(id || "generic-dialog");
|
|
175
|
+
if (d && d.close) d.close();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function openDialog(title, bodyHtml, confirmLabel, onConfirm) {
|
|
179
|
+
var d = document.getElementById("generic-dialog");
|
|
180
|
+
if (!d) {
|
|
181
|
+
d = document.createElement("dialog");
|
|
182
|
+
d.id = "generic-dialog";
|
|
183
|
+
d.innerHTML = '<div class="dialog-content"><h3 id="gd-title"></h3><div id="gd-body"></div></div><div class="dialog-actions"><button class="btn btn-secondary" id="gd-cancel">Cancel</button><button class="btn btn-primary" id="gd-ok">Confirm</button></div>';
|
|
184
|
+
document.body.appendChild(d);
|
|
185
|
+
}
|
|
186
|
+
document.getElementById("gd-title").textContent = title;
|
|
187
|
+
document.getElementById("gd-body").innerHTML = bodyHtml;
|
|
188
|
+
var okBtn = document.getElementById("gd-ok");
|
|
189
|
+
okBtn.textContent = confirmLabel || "Confirm";
|
|
190
|
+
okBtn.className = (confirmLabel === "Delete") ? "btn btn-danger" : "btn btn-primary";
|
|
191
|
+
okBtn.onclick = function() { onConfirm(); };
|
|
192
|
+
document.getElementById("gd-cancel").onclick = function() { d.close(); };
|
|
193
|
+
d.showModal();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function confirmDialog(opts) {
|
|
197
|
+
return new Promise(function(resolve) {
|
|
198
|
+
var d = document.getElementById("confirm-dialog");
|
|
199
|
+
if (!d) {
|
|
200
|
+
d = document.createElement("dialog");
|
|
201
|
+
d.id = "confirm-dialog";
|
|
202
|
+
d.innerHTML = '<div class="dialog-content"><h3 id="confirm-title"></h3><p id="confirm-message" class="dialog-desc"></p></div><div class="dialog-actions"><button class="btn btn-secondary" id="confirm-cancel">Cancel</button><button class="btn" id="confirm-ok">Confirm</button></div>';
|
|
203
|
+
document.body.appendChild(d);
|
|
204
|
+
}
|
|
205
|
+
document.getElementById("confirm-title").textContent = opts.title || "Confirm";
|
|
206
|
+
document.getElementById("confirm-message").textContent = opts.message || "";
|
|
207
|
+
var okBtn = document.getElementById("confirm-ok");
|
|
208
|
+
okBtn.textContent = opts.confirmLabel || "Confirm";
|
|
209
|
+
okBtn.className = opts.danger ? "btn btn-danger" : "btn";
|
|
210
|
+
document.getElementById("confirm-cancel").textContent = opts.cancelLabel || "Cancel";
|
|
211
|
+
|
|
212
|
+
function cleanup() {
|
|
213
|
+
okBtn.onclick = null;
|
|
214
|
+
document.getElementById("confirm-cancel").onclick = null;
|
|
215
|
+
d.close();
|
|
216
|
+
}
|
|
217
|
+
okBtn.onclick = function() { cleanup(); resolve(true); };
|
|
218
|
+
document.getElementById("confirm-cancel").onclick = function() { cleanup(); resolve(false); };
|
|
219
|
+
d.onclose = function() { resolve(false); };
|
|
220
|
+
d.showModal();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function promptDialog(opts) {
|
|
225
|
+
return new Promise(function(resolve) {
|
|
226
|
+
var d = document.getElementById("prompt-dialog");
|
|
227
|
+
if (!d) {
|
|
228
|
+
d = document.createElement("dialog");
|
|
229
|
+
d.id = "prompt-dialog";
|
|
230
|
+
d.innerHTML = '<div class="dialog-content"><h3 id="prompt-title"></h3><p id="prompt-message" class="dialog-desc"></p><input class="input" id="prompt-input" style="margin-top:12px;" /></div><div class="dialog-actions"><button class="btn btn-secondary" id="prompt-cancel">Cancel</button><button class="btn" id="prompt-ok">Confirm</button></div>';
|
|
231
|
+
document.body.appendChild(d);
|
|
232
|
+
}
|
|
233
|
+
document.getElementById("prompt-title").textContent = opts.title || "";
|
|
234
|
+
document.getElementById("prompt-message").textContent = opts.message || "";
|
|
235
|
+
var input = document.getElementById("prompt-input");
|
|
236
|
+
input.type = opts.inputType || "text";
|
|
237
|
+
input.placeholder = opts.placeholder || "";
|
|
238
|
+
input.value = opts.defaultValue || "";
|
|
239
|
+
var okBtn = document.getElementById("prompt-ok");
|
|
240
|
+
okBtn.textContent = opts.confirmLabel || __t("common.confirm");
|
|
241
|
+
|
|
242
|
+
function cleanup() { d.close(); }
|
|
243
|
+
okBtn.onclick = function() { var v = input.value.trim(); cleanup(); resolve(v || null); };
|
|
244
|
+
document.getElementById("prompt-cancel").onclick = function() { cleanup(); resolve(null); };
|
|
245
|
+
d.onclose = function() { resolve(null); };
|
|
246
|
+
d.showModal();
|
|
247
|
+
input.focus();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
;
|
|
252
|
+
|
|
253
|
+
var currentTab = null;
|
|
254
|
+
var tabHandlers = {};
|
|
255
|
+
|
|
256
|
+
function registerTab(name, handler) {
|
|
257
|
+
tabHandlers[name] = handler;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function switchTab(name) {
|
|
261
|
+
if (currentTab === name) return;
|
|
262
|
+
currentTab = name;
|
|
263
|
+
|
|
264
|
+
// Update tab buttons (desktop)
|
|
265
|
+
document.querySelectorAll("[data-tab]").forEach(function(el) {
|
|
266
|
+
el.classList.toggle("tab-active", el.getAttribute("data-tab") === name);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Update sidebar items (mobile)
|
|
270
|
+
document.querySelectorAll("[data-sidebar-tab]").forEach(function(el) {
|
|
271
|
+
el.classList.toggle("sidebar-item-active", el.getAttribute("data-sidebar-tab") === name);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Update tab panels
|
|
275
|
+
document.querySelectorAll("[data-panel]").forEach(function(el) {
|
|
276
|
+
el.classList.toggle("hidden", el.getAttribute("data-panel") !== name);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Update hash without triggering hashchange
|
|
280
|
+
if (location.hash !== "#" + name) {
|
|
281
|
+
history.replaceState(null, "", "#" + name);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Call tab handler for lazy loading
|
|
285
|
+
if (tabHandlers[name]) {
|
|
286
|
+
tabHandlers[name]();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function initRouter(defaultTab) {
|
|
291
|
+
var hash = location.hash.slice(1);
|
|
292
|
+
var validTabs = Object.keys(tabHandlers);
|
|
293
|
+
var initial = validTabs.indexOf(hash) >= 0 ? hash : defaultTab;
|
|
294
|
+
switchTab(initial);
|
|
295
|
+
|
|
296
|
+
window.addEventListener("hashchange", function() {
|
|
297
|
+
var h = location.hash.slice(1);
|
|
298
|
+
if (tabHandlers[h]) switchTab(h);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
;
|
|
303
|
+
|
|
304
|
+
var profileData = null;
|
|
305
|
+
|
|
306
|
+
async function loadProfile() {
|
|
307
|
+
document.getElementById("profile-card-skeleton").style.display = "";
|
|
308
|
+
document.getElementById("profile-card-content").style.display = "none";
|
|
309
|
+
var data = await api("GET", "/profile");
|
|
310
|
+
document.getElementById("profile-card-skeleton").style.display = "none";
|
|
311
|
+
document.getElementById("profile-card-content").style.display = "";
|
|
312
|
+
if (!data.ok) return;
|
|
313
|
+
profileData = data.user;
|
|
314
|
+
renderProfile(data.user);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderProfile(user) {
|
|
318
|
+
var avatarEl = document.getElementById("profile-avatar");
|
|
319
|
+
if (user.avatar) {
|
|
320
|
+
avatarEl.innerHTML = '<img src="' + escapeHtml(user.avatar) + '" onerror="this.style.display=\'none\';this.nextSibling.style.display=\'flex\'">'
|
|
321
|
+
+ '<span class="avatar-fallback" style="display:none">' + escapeHtml(initials(user.fullName)) + '</span>'
|
|
322
|
+
+ '<div class="avatar-overlay"><span>Edit</span></div>';
|
|
323
|
+
} else {
|
|
324
|
+
avatarEl.innerHTML = '<span class="avatar-fallback">' + escapeHtml(initials(user.fullName)) + '</span>'
|
|
325
|
+
+ '<div class="avatar-overlay"><span>Edit</span></div>';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
document.getElementById("profile-display-name").textContent = user.fullName || __t("profile.unnamed");
|
|
329
|
+
|
|
330
|
+
var role = user.role || "guest";
|
|
331
|
+
var badge = document.getElementById("profile-role-badge");
|
|
332
|
+
badge.textContent = __t("common." + role);
|
|
333
|
+
badge.className = "badge badge-" + role;
|
|
334
|
+
|
|
335
|
+
document.getElementById("profile-did").setAttribute("did", user.did);
|
|
336
|
+
|
|
337
|
+
document.getElementById("profile-name").value = user.fullName || "";
|
|
338
|
+
cancelEditName();
|
|
339
|
+
|
|
340
|
+
document.getElementById("profile-email-display").textContent = user.email || "—";
|
|
341
|
+
|
|
342
|
+
var emailBadge = document.getElementById("profile-email-badge");
|
|
343
|
+
var emailVerifyBtn = document.getElementById("profile-email-verify-btn");
|
|
344
|
+
if (user.email) {
|
|
345
|
+
emailBadge.style.display = "";
|
|
346
|
+
if (user.emailVerified) {
|
|
347
|
+
emailBadge.textContent = __t("profile.emailVerified");
|
|
348
|
+
emailBadge.className = "badge badge-verified";
|
|
349
|
+
emailVerifyBtn.style.display = "none";
|
|
350
|
+
} else {
|
|
351
|
+
emailBadge.textContent = __t("profile.emailUnverified");
|
|
352
|
+
emailBadge.className = "badge badge-unverified";
|
|
353
|
+
emailVerifyBtn.style.display = "";
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
emailBadge.style.display = "none";
|
|
357
|
+
emailVerifyBtn.style.display = "none";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (user.createdAt) {
|
|
361
|
+
document.getElementById("profile-joined-cell").style.display = "";
|
|
362
|
+
document.getElementById("profile-joined-value").textContent = absoluteTime(user.createdAt);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function startEditName() {
|
|
367
|
+
document.getElementById("profile-header-info-view").style.display = "none";
|
|
368
|
+
document.getElementById("profile-name-editor").style.display = "";
|
|
369
|
+
document.getElementById("profile-name").focus();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cancelEditName() {
|
|
373
|
+
document.getElementById("profile-name-editor").style.display = "none";
|
|
374
|
+
document.getElementById("profile-header-info-view").style.display = "";
|
|
375
|
+
if (profileData) document.getElementById("profile-name").value = profileData.fullName || "";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function saveProfile() {
|
|
379
|
+
var body = {
|
|
380
|
+
fullName: document.getElementById("profile-name").value.trim(),
|
|
381
|
+
};
|
|
382
|
+
var data = await api("PUT", "/profile", body);
|
|
383
|
+
if (data.ok) {
|
|
384
|
+
profileData = data.user;
|
|
385
|
+
renderProfile(data.user);
|
|
386
|
+
showToast(__t("profile.updated"), "success");
|
|
387
|
+
var headerName = document.getElementById("header-user-name");
|
|
388
|
+
if (headerName) headerName.textContent = data.user.fullName || truncateDid(data.user.did);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function changeEmail() {
|
|
393
|
+
var currentEmail = profileData ? profileData.email || "" : "";
|
|
394
|
+
var newEmail = await promptDialog({
|
|
395
|
+
title: __t("profile.changeEmailTitle"),
|
|
396
|
+
message: __t("profile.changeEmailMsg"),
|
|
397
|
+
placeholder: __t("profile.emailPlaceholder"),
|
|
398
|
+
defaultValue: currentEmail,
|
|
399
|
+
inputType: "email",
|
|
400
|
+
});
|
|
401
|
+
if (!newEmail || newEmail === currentEmail) return;
|
|
402
|
+
var result = await api("PUT", "/profile/email", { email: newEmail });
|
|
403
|
+
if (result.ok) {
|
|
404
|
+
profileData = result.user;
|
|
405
|
+
renderProfile(result.user);
|
|
406
|
+
showToast(__t("profile.emailUpdated"), "success");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
var _lastVerifySentAt = 0;
|
|
411
|
+
|
|
412
|
+
function verifyEmail() {
|
|
413
|
+
var email = profileData ? profileData.email : "";
|
|
414
|
+
if (!email) return;
|
|
415
|
+
|
|
416
|
+
var verifyBtn = document.getElementById("profile-email-verify-btn");
|
|
417
|
+
verifyBtn.disabled = true;
|
|
418
|
+
|
|
419
|
+
// Build or reuse dialog
|
|
420
|
+
var d = document.getElementById("verify-email-dialog");
|
|
421
|
+
if (!d) {
|
|
422
|
+
d = document.createElement("dialog");
|
|
423
|
+
d.id = "verify-email-dialog";
|
|
424
|
+
d.innerHTML = '<div class="dialog-content">'
|
|
425
|
+
+ '<h3 id="verify-title"></h3>'
|
|
426
|
+
+ '<p id="verify-message" class="dialog-desc"></p>'
|
|
427
|
+
+ '<input class="input" id="verify-input" placeholder="000000" style="margin-top:12px;" />'
|
|
428
|
+
+ '</div>'
|
|
429
|
+
+ '<div class="dialog-actions">'
|
|
430
|
+
+ '<button class="btn-text" id="verify-resend" style="margin-right:auto"></button>'
|
|
431
|
+
+ '<button class="btn btn-secondary" id="verify-cancel">' + __t("common.cancel") + '</button>'
|
|
432
|
+
+ '<button class="btn" id="verify-ok">' + __t("common.confirm") + '</button>'
|
|
433
|
+
+ '</div>';
|
|
434
|
+
document.body.appendChild(d);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var titleEl = document.getElementById("verify-title");
|
|
438
|
+
var msgEl = document.getElementById("verify-message");
|
|
439
|
+
var inputEl = document.getElementById("verify-input");
|
|
440
|
+
var okBtn = document.getElementById("verify-ok");
|
|
441
|
+
var cancelBtn = document.getElementById("verify-cancel");
|
|
442
|
+
var resendBtn = document.getElementById("verify-resend");
|
|
443
|
+
var countdownTimer = null;
|
|
444
|
+
|
|
445
|
+
titleEl.textContent = __t("profile.verifyCode");
|
|
446
|
+
inputEl.value = "";
|
|
447
|
+
|
|
448
|
+
function cleanup() {
|
|
449
|
+
if (countdownTimer) clearInterval(countdownTimer);
|
|
450
|
+
verifyBtn.disabled = false;
|
|
451
|
+
d.close();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function remainingCooldown() {
|
|
455
|
+
if (!_lastVerifySentAt) return 0;
|
|
456
|
+
return Math.max(0, Math.ceil((60000 - (Date.now() - _lastVerifySentAt)) / 1000));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function syncResendBtn() {
|
|
460
|
+
var remain = remainingCooldown();
|
|
461
|
+
resendBtn.style.display = "";
|
|
462
|
+
if (remain > 0) {
|
|
463
|
+
resendBtn.disabled = true;
|
|
464
|
+
resendBtn.textContent = __t("profile.emailResendCountdown", { seconds: remain });
|
|
465
|
+
} else {
|
|
466
|
+
resendBtn.disabled = false;
|
|
467
|
+
resendBtn.textContent = __t("profile.emailResendBtn");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function startCountdown() {
|
|
472
|
+
if (countdownTimer) clearInterval(countdownTimer);
|
|
473
|
+
syncResendBtn();
|
|
474
|
+
countdownTimer = setInterval(function() {
|
|
475
|
+
if (remainingCooldown() <= 0) {
|
|
476
|
+
clearInterval(countdownTimer);
|
|
477
|
+
countdownTimer = null;
|
|
478
|
+
}
|
|
479
|
+
syncResendBtn();
|
|
480
|
+
}, 1000);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function sendCode() {
|
|
484
|
+
msgEl.textContent = __t("profile.emailSending");
|
|
485
|
+
inputEl.disabled = true;
|
|
486
|
+
okBtn.disabled = true;
|
|
487
|
+
resendBtn.style.display = "none";
|
|
488
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
489
|
+
|
|
490
|
+
api("POST", "/profile/email/verify").then(function(sendResult) {
|
|
491
|
+
if (!sendResult.ok) {
|
|
492
|
+
msgEl.textContent = sendResult.error || __t("profile.emailSendFailed");
|
|
493
|
+
// If there's remaining cooldown, show countdown; otherwise show resend
|
|
494
|
+
if (remainingCooldown() > 0) {
|
|
495
|
+
startCountdown();
|
|
496
|
+
} else {
|
|
497
|
+
resendBtn.style.display = "";
|
|
498
|
+
resendBtn.disabled = false;
|
|
499
|
+
resendBtn.textContent = __t("profile.emailResendBtn");
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
_lastVerifySentAt = Date.now();
|
|
504
|
+
msgEl.textContent = __t("profile.verifyCodeMsg", { email: email });
|
|
505
|
+
inputEl.disabled = false;
|
|
506
|
+
okBtn.disabled = false;
|
|
507
|
+
inputEl.focus();
|
|
508
|
+
startCountdown();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
cancelBtn.onclick = function() { cleanup(); };
|
|
513
|
+
d.onclose = function() {
|
|
514
|
+
if (countdownTimer) clearInterval(countdownTimer);
|
|
515
|
+
verifyBtn.disabled = false;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
okBtn.onclick = function() {
|
|
519
|
+
var code = inputEl.value.trim();
|
|
520
|
+
if (!code) return;
|
|
521
|
+
okBtn.disabled = true;
|
|
522
|
+
api("PUT", "/profile/email/verify", { code: code }).then(function(result) {
|
|
523
|
+
if (result.ok) {
|
|
524
|
+
cleanup();
|
|
525
|
+
profileData = result.user;
|
|
526
|
+
renderProfile(result.user);
|
|
527
|
+
showToast(__t("profile.emailVerifiedSuccess"), "success");
|
|
528
|
+
} else {
|
|
529
|
+
okBtn.disabled = false;
|
|
530
|
+
msgEl.textContent = result.error || __t("profile.emailVerifyFailed");
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
resendBtn.onclick = function() { sendCode(); };
|
|
536
|
+
|
|
537
|
+
d.showModal();
|
|
538
|
+
|
|
539
|
+
// If still in cooldown from a previous send, skip sending and show code input directly
|
|
540
|
+
var remain = remainingCooldown();
|
|
541
|
+
if (remain > 0) {
|
|
542
|
+
msgEl.textContent = __t("profile.verifyCodeMsg", { email: email });
|
|
543
|
+
inputEl.disabled = false;
|
|
544
|
+
okBtn.disabled = false;
|
|
545
|
+
inputEl.focus();
|
|
546
|
+
startCountdown();
|
|
547
|
+
} else {
|
|
548
|
+
sendCode();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function triggerAvatarUpload() {
|
|
553
|
+
document.getElementById("avatar-file-input").click();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function resizeImage(file, maxSize) {
|
|
557
|
+
return new Promise(function(resolve, reject) {
|
|
558
|
+
var img = new Image();
|
|
559
|
+
img.onload = function() {
|
|
560
|
+
var canvas = document.createElement("canvas");
|
|
561
|
+
canvas.width = canvas.height = maxSize;
|
|
562
|
+
var ctx = canvas.getContext("2d");
|
|
563
|
+
var s = Math.min(img.width, img.height);
|
|
564
|
+
var sx = (img.width - s) / 2;
|
|
565
|
+
var sy = (img.height - s) / 2;
|
|
566
|
+
ctx.drawImage(img, sx, sy, s, s, 0, 0, maxSize, maxSize);
|
|
567
|
+
resolve(canvas.toDataURL("image/jpeg", 0.85));
|
|
568
|
+
URL.revokeObjectURL(img.src);
|
|
569
|
+
};
|
|
570
|
+
img.onerror = function() {
|
|
571
|
+
URL.revokeObjectURL(img.src);
|
|
572
|
+
reject(new Error("Failed to load image"));
|
|
573
|
+
};
|
|
574
|
+
img.src = URL.createObjectURL(file);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function handleAvatarFile(input) {
|
|
579
|
+
var file = input.files[0];
|
|
580
|
+
if (!file) return;
|
|
581
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
582
|
+
showToast("Image must be under 10MB", "error");
|
|
583
|
+
input.value = "";
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
var base64 = await resizeImage(file, 200);
|
|
588
|
+
var data = await api("PUT", "/profile/avatar", { avatar: base64 });
|
|
589
|
+
if (data.ok) {
|
|
590
|
+
showToast("Avatar updated", "success");
|
|
591
|
+
loadProfile();
|
|
592
|
+
}
|
|
593
|
+
} catch (e) {
|
|
594
|
+
showToast("Failed to process image", "error");
|
|
595
|
+
}
|
|
596
|
+
input.value = "";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function removeAvatar() {
|
|
600
|
+
var ok = await confirmDialog({ title: "Remove Avatar", message: "Remove your avatar? Your profile will show a default avatar.", confirmLabel: "Remove" });
|
|
601
|
+
if (!ok) return;
|
|
602
|
+
var data = await api("DELETE", "/profile/avatar");
|
|
603
|
+
if (data.ok) {
|
|
604
|
+
showToast("Avatar removed", "success");
|
|
605
|
+
loadProfile();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
registerTab("profile", function() {
|
|
610
|
+
loadProfile();
|
|
611
|
+
if (typeof loadConnectedAccounts === "function") loadConnectedAccounts();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
;
|
|
615
|
+
|
|
616
|
+
var __connectedAccounts = [];
|
|
617
|
+
var __oauthProviders = [];
|
|
618
|
+
var __providerIcons = {"passkey":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path fill=\"currentColor\" d=\"M3 20v-2.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.5 0 1 .038t1 .112q-.1 1.45.525 2.738T15.35 18v2zm16 3l-1.5-1.5v-4.65q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25L21 18l-1.5 1.5L21 21zm-8-11q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287\"/></svg>","wallet":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"3\" fill=\"#4598FA\"/><rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.9\"/><rect x=\"0\" y=\"14\" width=\"24\" height=\"10\" rx=\"0\" fill=\"url(#dw_g)\"/><path d=\"M5.5 9.5h4.5c.3 0 .5.2.5.5v2.5c0 .3-.2.5-.5.5H5.5c-.3 0-.5-.2-.5-.5V10c0-.3.2-.5.5-.5z\" fill=\"#4598FA\"/><defs><linearGradient id=\"dw_g\" x1=\"12\" y1=\"14\" x2=\"12\" y2=\"24\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#77B2F6\"/><stop offset=\"1\" stop-color=\"#4598FA\"/></linearGradient></defs></svg>","did-connect":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"3\" fill=\"#4598FA\"/><rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.9\"/><rect x=\"0\" y=\"14\" width=\"24\" height=\"10\" rx=\"0\" fill=\"url(#dw_g)\"/><path d=\"M5.5 9.5h4.5c.3 0 .5.2.5.5v2.5c0 .3-.2.5-.5.5H5.5c-.3 0-.5-.2-.5-.5V10c0-.3.2-.5.5-.5z\" fill=\"#4598FA\"/><defs><linearGradient id=\"dw_g\" x1=\"12\" y1=\"14\" x2=\"12\" y2=\"24\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#77B2F6\"/><stop offset=\"1\" stop-color=\"#4598FA\"/></linearGradient></defs></svg>","email":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path fill=\"currentColor\" d=\"M22 7.535V17a3 3 0 0 1-2.824 2.995L19 20H5a3 3 0 0 1-2.995-2.824L2 17V7.535l9.445 6.297l.116.066a1 1 0 0 0 .878 0l.116-.066z\"/><path fill=\"currentColor\" d=\"M19 4c1.08 0 2.027.57 2.555 1.427L12 11.797l-9.555-6.37a2.999 2.999 0 0 1 2.354-1.42L5 4z\"/></svg>","google":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 262\"><path fill=\"#4285F4\" d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"/><path fill=\"#34A853\" d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"/><path fill=\"#FBBC05\" d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"/><path fill=\"#EB4335\" d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"/></svg>","github":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 250\" fill=\"currentColor\"><path d=\"M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46c6.397 1.185 8.746-2.777 8.746-6.158c0-3.052-.12-13.135-.174-23.83c-35.61 7.742-43.124-15.103-43.124-15.103c-5.823-14.795-14.213-18.73-14.213-18.73c-11.613-7.944.876-7.78.876-7.78c12.853.902 19.621 13.19 19.621 13.19c11.417 19.568 29.945 13.911 37.249 10.64c1.149-8.272 4.466-13.92 8.127-17.116c-28.431-3.236-58.318-14.212-58.318-63.258c0-13.975 5-25.394 13.188-34.358c-1.329-3.224-5.71-16.242 1.24-33.874c0 0 10.749-3.44 35.21 13.121c10.21-2.836 21.16-4.258 32.038-4.307c10.878.049 21.837 1.47 32.066 4.307c24.431-16.56 35.165-13.12 35.165-13.12c6.967 17.63 2.584 30.65 1.255 33.873c8.207 8.964 13.173 20.383 13.173 34.358c0 49.163-29.944 59.988-58.447 63.157c4.591 3.972 8.682 11.762 8.682 23.704c0 17.126-.148 30.91-.148 35.126c0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002C256 57.307 198.691 0 128.001 0\"/></svg>","apple":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 315\" fill=\"currentColor\"><path d=\"M213.803 167.03c.442 47.58 41.74 63.413 42.197 63.615c-.35 1.116-6.599 22.563-21.757 44.716c-13.104 19.153-26.705 38.235-48.13 38.63c-21.05.388-27.82-12.483-51.888-12.483c-24.061 0-31.582 12.088-51.51 12.871c-20.68.783-36.428-20.71-49.64-39.793c-27-39.033-47.633-110.3-19.928-158.406c13.763-23.89 38.36-39.017 65.056-39.405c20.307-.388 39.475 13.675 51.889 13.675c12.406 0 35.699-16.895 60.186-14.414c10.25.427 39.026 4.14 57.503 31.186c-1.49.923-34.335 20.044-33.978 59.808M174.24 50.199c10.98-13.29 18.369-31.79 16.353-50.199c-15.826.636-34.962 10.546-46.314 23.828c-10.173 11.763-19.082 30.589-16.678 48.633c17.64 1.365 35.66-8.964 46.639-22.262\"/></svg>","twitter":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 256\" fill=\"currentColor\"><path d=\"M149.079 108.399L242.33 0h-22.098l-80.97 94.12L74.59 0H0l97.796 142.328L0 256h22.1l85.507-99.395L175.905 256h74.59L149.073 108.399zM118.81 143.58l-9.909-14.172l-78.84-112.773h33.943l63.625 91.011l9.909 14.173l82.705 118.3H186.3l-67.49-96.533z\"/></svg>","auth0":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 256\"><path fill=\"#EB5424\" d=\"M203.97 200.38L167.47 88l-39.5 112.38h75.98zm0-144.76L167.47 168l75.97-26.16l19.76-60.82c6.64-20.4-1.94-42.72-21.18-52.76l-38.06 27.36zM52.03 55.62L88.54 168l39.5-112.38H52.02zM52.03 200.38L88.54 88L12.56 114.16l-5.92 18.2c-6.64 20.4 1.94 42.72 21.18 52.76l24.2-17.38l.01.01v-.01l-.01.01.01-.37z\"/></svg>"};
|
|
619
|
+
var __hiddenProviders = ["facebook","auth0-legacy"];
|
|
620
|
+
|
|
621
|
+
async function loadConnectedAccounts() {
|
|
622
|
+
var card = document.getElementById("connected-accounts-card");
|
|
623
|
+
var list = document.getElementById("connected-accounts-list");
|
|
624
|
+
|
|
625
|
+
// Show card with skeleton chips while loading
|
|
626
|
+
card.style.display = "";
|
|
627
|
+
list.innerHTML = '<div style="padding:12px 20px 16px;display:flex;gap:8px;flex-wrap:wrap;">'
|
|
628
|
+
+ '<span class="skel" style="width:80px;height:32px;border-radius:16px;"></span>'.repeat(4)
|
|
629
|
+
+ '</div>';
|
|
630
|
+
|
|
631
|
+
// Load session data and OAuth configs in parallel
|
|
632
|
+
var results = await Promise.all([
|
|
633
|
+
api("GET", "/../did/session"),
|
|
634
|
+
api("GET", "/../oauth/configs").catch(function() { return { providers: [] }; }),
|
|
635
|
+
]);
|
|
636
|
+
|
|
637
|
+
var session = results[0];
|
|
638
|
+
var oauthData = results[1];
|
|
639
|
+
|
|
640
|
+
var userObj = session.user || session;
|
|
641
|
+
if (!session.authenticated || !userObj.connectedAccounts) {
|
|
642
|
+
card.style.display = "none";
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
__connectedAccounts = userObj.connectedAccounts;
|
|
647
|
+
__oauthProviders = (oauthData.providers || []).map(function(p) { return p.name || p; })
|
|
648
|
+
.filter(function(p) { return __hiddenProviders.indexOf(p) === -1; });
|
|
649
|
+
|
|
650
|
+
card.style.display = "";
|
|
651
|
+
renderConnectedAccounts();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function renderConnectedAccounts() {
|
|
655
|
+
var list = document.getElementById("connected-accounts-list");
|
|
656
|
+
var html = "";
|
|
657
|
+
|
|
658
|
+
// Partition accounts by type
|
|
659
|
+
var walletAccounts = [];
|
|
660
|
+
var passkeyAccounts = [];
|
|
661
|
+
var otherAccounts = [];
|
|
662
|
+
for (var i = 0; i < __connectedAccounts.length; i++) {
|
|
663
|
+
var a = __connectedAccounts[i];
|
|
664
|
+
if (__hiddenProviders.indexOf(a.provider) !== -1) continue;
|
|
665
|
+
if (a.provider === "wallet" || a.provider === "did-connect") {
|
|
666
|
+
walletAccounts.push(a);
|
|
667
|
+
} else if (a.provider === "passkey") {
|
|
668
|
+
passkeyAccounts.push(a);
|
|
669
|
+
} else {
|
|
670
|
+
otherAccounts.push(a);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Sort non-wallet accounts: main first
|
|
674
|
+
otherAccounts.sort(function(x, y) {
|
|
675
|
+
if (x.isMain && !y.isMain) return -1;
|
|
676
|
+
if (!x.isMain && y.isMain) return 1;
|
|
677
|
+
return 0;
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// --- Section 1: DID Wallet (always first) ---
|
|
681
|
+
html += '<div class="ca-section">';
|
|
682
|
+
html += '<div class="ca-section-label">DID Wallet</div>';
|
|
683
|
+
if (walletAccounts.length > 0) {
|
|
684
|
+
html += '<div class="ca-bound-grid">';
|
|
685
|
+
for (var w = 0; w < walletAccounts.length; w++) {
|
|
686
|
+
html += renderBoundCard(walletAccounts[w]);
|
|
687
|
+
}
|
|
688
|
+
html += '</div>';
|
|
689
|
+
// Already has a wallet — no connect button (one wallet per user)
|
|
690
|
+
} else {
|
|
691
|
+
html += '<button class="ca-passkey-add" onclick="connectWallet()">';
|
|
692
|
+
html += '<span class="ca-chip-add-icon">+</span>';
|
|
693
|
+
html += '<span class="ca-chip-icon">' + getProviderIconSvg("did-connect") + '</span>';
|
|
694
|
+
html += __t("profile.connectWallet");
|
|
695
|
+
html += '</button>';
|
|
696
|
+
}
|
|
697
|
+
html += '</div>';
|
|
698
|
+
|
|
699
|
+
// --- Section 2: Other login methods ---
|
|
700
|
+
if (otherAccounts.length > 0) {
|
|
701
|
+
html += '<div class="ca-section">';
|
|
702
|
+
html += '<div class="ca-section-label">' + __t("profile.loginMethods") + '</div>';
|
|
703
|
+
html += '<div class="ca-bound-grid">';
|
|
704
|
+
for (var b = 0; b < otherAccounts.length; b++) {
|
|
705
|
+
html += renderBoundCard(otherAccounts[b]);
|
|
706
|
+
}
|
|
707
|
+
html += '</div></div>';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// --- Section 3: Passkeys (large cards + add chip) ---
|
|
711
|
+
html += '<div class="ca-section">';
|
|
712
|
+
html += '<div class="ca-section-label">' + __t("profile.passkeys") + '</div>';
|
|
713
|
+
if (passkeyAccounts.length > 0) {
|
|
714
|
+
html += '<div class="ca-bound-grid">';
|
|
715
|
+
for (var pk = 0; pk < passkeyAccounts.length; pk++) {
|
|
716
|
+
html += renderBoundCard(passkeyAccounts[pk]);
|
|
717
|
+
}
|
|
718
|
+
html += '</div>';
|
|
719
|
+
}
|
|
720
|
+
html += '<div style="padding:' + (passkeyAccounts.length > 0 ? '8' : '0') + 'px 0 0">';
|
|
721
|
+
html += '<button class="ca-passkey-add" onclick="addPasskey()">';
|
|
722
|
+
html += '<span class="ca-chip-add-icon">+</span>';
|
|
723
|
+
html += '<span class="ca-chip-icon">' + getProviderIconSvg("passkey") + '</span>';
|
|
724
|
+
html += __t("profile.addPasskey");
|
|
725
|
+
html += '</button>';
|
|
726
|
+
html += '</div></div>';
|
|
727
|
+
|
|
728
|
+
// --- Section 4: Available to connect (unbound OAuth, at the bottom) ---
|
|
729
|
+
var boundProviders = {};
|
|
730
|
+
for (var j = 0; j < __connectedAccounts.length; j++) {
|
|
731
|
+
boundProviders[__connectedAccounts[j].provider] = true;
|
|
732
|
+
}
|
|
733
|
+
var unboundOAuth = __oauthProviders.filter(function(p) { return !boundProviders[p]; });
|
|
734
|
+
if (unboundOAuth.length > 0) {
|
|
735
|
+
html += '<div class="ca-section">';
|
|
736
|
+
html += '<div class="ca-section-label">' + __t("profile.availableToConnect") + '</div>';
|
|
737
|
+
html += '<div class="ca-chips">';
|
|
738
|
+
for (var u = 0; u < unboundOAuth.length; u++) {
|
|
739
|
+
var p = unboundOAuth[u];
|
|
740
|
+
html += '<div class="ca-chip ca-chip-unbound" onclick="connectOAuth(\'' + escapeHtml(p) + '\')" title="' + __t("profile.connect") + " " + escapeHtml(getProviderLabel(p)) + '">';
|
|
741
|
+
html += '<span class="ca-chip-add-icon">+</span>';
|
|
742
|
+
html += '<span class="ca-chip-icon">' + getProviderIconSvg(p) + '</span>';
|
|
743
|
+
html += '<span class="ca-chip-name">' + escapeHtml(getProviderLabel(p)) + '</span>';
|
|
744
|
+
html += '</div>';
|
|
745
|
+
}
|
|
746
|
+
html += '</div></div>';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
list.innerHTML = html;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
var __copyIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>';
|
|
753
|
+
|
|
754
|
+
function renderBoundCard(a) {
|
|
755
|
+
var icon = getProviderIconSvg(a.provider);
|
|
756
|
+
var label = getProviderLabel(a.provider);
|
|
757
|
+
var isPasskey = a.provider === "passkey";
|
|
758
|
+
|
|
759
|
+
var html = '<div class="ca-bound-card">';
|
|
760
|
+
|
|
761
|
+
// Top row: icon + name/detail + action buttons (top-right)
|
|
762
|
+
html += '<div class="ca-bound-top">';
|
|
763
|
+
if (isPasskey) {
|
|
764
|
+
html += '<div class="ca-bound-icon ca-bound-icon-lg">' + icon + '</div>';
|
|
765
|
+
} else {
|
|
766
|
+
html += '<div class="ca-bound-icon">' + icon + '</div>';
|
|
767
|
+
}
|
|
768
|
+
html += '<div class="ca-bound-top-body">';
|
|
769
|
+
html += '<div class="ca-bound-header">';
|
|
770
|
+
html += '<span class="ca-bound-name">' + escapeHtml(label) + '</span>';
|
|
771
|
+
if (a.isMain) {
|
|
772
|
+
html += '<span class="ca-chip-badge">' + __t("profile.mainAccount") + '</span>';
|
|
773
|
+
} else if (a.provider === "wallet" || a.provider === "did-connect") {
|
|
774
|
+
html += '<span class="ca-chip-badge">' + __t("profile.walletAccount") + '</span>';
|
|
775
|
+
}
|
|
776
|
+
html += '</div>';
|
|
777
|
+
var info = getAccountUserInfo(a);
|
|
778
|
+
if (info) {
|
|
779
|
+
html += '<div class="ca-bound-detail">' + info + '</div>';
|
|
780
|
+
}
|
|
781
|
+
html += '</div>';
|
|
782
|
+
|
|
783
|
+
// Action buttons at top-right
|
|
784
|
+
if (!a.isMain && a.provider !== "wallet" && a.provider !== "did-connect") {
|
|
785
|
+
html += '<div class="ca-bound-actions">';
|
|
786
|
+
if (isPasskey) {
|
|
787
|
+
html += '<button class="btn-text" onclick="openRenamePasskeyDialog(\'' + escapeHtml(a.did) + '\', \'' + escapeHtml(getPasskeyName(a)) + '\')">' + __t("profile.renamePasskey") + '</button>';
|
|
788
|
+
html += '<button class="btn-text" style="color:var(--red-text)" onclick="removePasskey(\'' + escapeHtml(a.did) + '\')">' + __t("profile.remove") + '</button>';
|
|
789
|
+
} else {
|
|
790
|
+
html += '<button class="btn-text" style="color:var(--red-text)" onclick="unbindOAuth(\'' + escapeHtml(a.provider) + '\')">' + __t("profile.remove") + '</button>';
|
|
791
|
+
}
|
|
792
|
+
html += '</div>';
|
|
793
|
+
}
|
|
794
|
+
html += '</div>';
|
|
795
|
+
|
|
796
|
+
// DID field with copy
|
|
797
|
+
if (a.did) {
|
|
798
|
+
html += '<div class="ca-bound-field">';
|
|
799
|
+
html += '<span class="ca-bound-field-label">DID</span>';
|
|
800
|
+
html += '<did-address did="' + escapeHtml(a.did) + '"></did-address>';
|
|
801
|
+
html += '</div>';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
html += '</div>';
|
|
805
|
+
return html;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function getPasskeyName(a) {
|
|
809
|
+
return (a.userInfo && a.userInfo.name) ? a.userInfo.name : "Passkey";
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function getAccountUserInfo(a) {
|
|
813
|
+
var parts = [];
|
|
814
|
+
if (a.userInfo && a.userInfo.name) parts.push(escapeHtml(a.userInfo.name));
|
|
815
|
+
if (a.userInfo && a.userInfo.email) parts.push(escapeHtml(a.userInfo.email));
|
|
816
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
817
|
+
if (a.provider === "passkey") {
|
|
818
|
+
var name = a.userInfo && a.userInfo.name ? a.userInfo.name : null;
|
|
819
|
+
return name ? escapeHtml(name) : "\u2022\u2022\u2022\u2022" + (a.id ? a.id.slice(-4) : "");
|
|
820
|
+
}
|
|
821
|
+
return "";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function getProviderIconSvg(provider) {
|
|
825
|
+
var svg = __providerIcons[provider];
|
|
826
|
+
if (svg) return svg;
|
|
827
|
+
return '<span style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;background:var(--bg-secondary);font-size:11px;font-weight:600">' + provider.charAt(0).toUpperCase() + '</span>';
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function getProviderLabel(provider) {
|
|
831
|
+
var labels = { passkey: "Passkey", google: "Google", github: "GitHub", apple: "Apple", twitter: "Twitter", wallet: "DID Wallet", "did-connect": "DID Wallet", email: "Email" };
|
|
832
|
+
return labels[provider] || (provider.charAt(0).toUpperCase() + provider.slice(1));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function connectOAuth(provider) {
|
|
836
|
+
var returnUrl = encodeURIComponent(window.location.href);
|
|
837
|
+
var url = window.__BASE_PATH + "/../oauth/" + provider + "/login?returnUrl=" + returnUrl;
|
|
838
|
+
var redirectUri = location.origin + "/.well-known/service/oauth/callback/" + provider;
|
|
839
|
+
var w = 500, h = 600;
|
|
840
|
+
var left = (screen.width - w) / 2, top = (screen.height - h) / 2;
|
|
841
|
+
var popup = window.open(url, "oauth_bind", "width=" + w + ",height=" + h + ",left=" + left + ",top=" + top);
|
|
842
|
+
if (!popup) {
|
|
843
|
+
showToast("Popup blocked — please allow popups and try again", "error");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Listen for postMessage from OAuth callback page
|
|
848
|
+
function onMessage(event) {
|
|
849
|
+
if (!event.data || event.data.type !== "authorization_response") return;
|
|
850
|
+
window.removeEventListener("message", onMessage);
|
|
851
|
+
clearInterval(closeTimer);
|
|
852
|
+
|
|
853
|
+
var resp = event.data.response;
|
|
854
|
+
if (!resp || !resp.code) {
|
|
855
|
+
showToast("OAuth failed — no authorization code received", "error");
|
|
856
|
+
try { popup.close(); } catch(e) {}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Call bind API with the authorization code
|
|
861
|
+
api("POST", "/../oauth/bind", {
|
|
862
|
+
provider: provider,
|
|
863
|
+
code: resp.code,
|
|
864
|
+
redirectUri: redirectUri,
|
|
865
|
+
}).then(function(data) {
|
|
866
|
+
if (data.ok) {
|
|
867
|
+
showToast(__t("profile.accountConnected"), "success");
|
|
868
|
+
loadConnectedAccounts();
|
|
869
|
+
}
|
|
870
|
+
}).catch(function() {}).finally(function() {
|
|
871
|
+
try { popup.close(); } catch(e) {}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
window.addEventListener("message", onMessage);
|
|
875
|
+
|
|
876
|
+
// Fallback: if popup closes without postMessage (COOP / manual close)
|
|
877
|
+
var closeTimer = setInterval(function() {
|
|
878
|
+
if (popup.closed) {
|
|
879
|
+
clearInterval(closeTimer);
|
|
880
|
+
window.removeEventListener("message", onMessage);
|
|
881
|
+
// Reload in case server-side exchange fallback was used
|
|
882
|
+
loadConnectedAccounts();
|
|
883
|
+
}
|
|
884
|
+
}, 500);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function unbindOAuth(provider) {
|
|
888
|
+
var ok = await confirmDialog({
|
|
889
|
+
title: __t("profile.removeConfirmTitle"),
|
|
890
|
+
message: __t("profile.removeConfirmMsg"),
|
|
891
|
+
confirmLabel: __t("profile.remove"),
|
|
892
|
+
});
|
|
893
|
+
if (!ok) return;
|
|
894
|
+
var data = await api("POST", "/../oauth/unbind", { provider: provider });
|
|
895
|
+
if (data.ok) {
|
|
896
|
+
showToast(__t("profile.accountRemoved"), "success");
|
|
897
|
+
loadConnectedAccounts();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function addPasskey() {
|
|
902
|
+
try {
|
|
903
|
+
// 1. Get registration options. The server flattens the WebAuthn options to
|
|
904
|
+
// the top level (challenge/user/rp alongside challengeId), matching the
|
|
905
|
+
// login flow; tolerate a nested { registration } shape too.
|
|
906
|
+
var opts = await api("POST", "/../passkey/connect/register", {});
|
|
907
|
+
var publicKey = opts.registration || opts;
|
|
908
|
+
if (!opts.challengeId || !publicKey || !publicKey.challenge) {
|
|
909
|
+
showToast("Failed to get registration options", "error");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 2. Create credential via WebAuthn
|
|
914
|
+
// Decode challenge from base64url
|
|
915
|
+
publicKey.challenge = base64urlToBuffer(publicKey.challenge);
|
|
916
|
+
publicKey.user.id = base64urlToBuffer(publicKey.user.id);
|
|
917
|
+
if (publicKey.excludeCredentials) {
|
|
918
|
+
for (var i = 0; i < publicKey.excludeCredentials.length; i++) {
|
|
919
|
+
publicKey.excludeCredentials[i].id = base64urlToBuffer(publicKey.excludeCredentials[i].id);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
var credential = await navigator.credentials.create({ publicKey: publicKey });
|
|
924
|
+
if (!credential) return;
|
|
925
|
+
|
|
926
|
+
// 3. Serialize credential for server
|
|
927
|
+
var response = credential.response;
|
|
928
|
+
var credentialData = {
|
|
929
|
+
id: credential.id,
|
|
930
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
931
|
+
type: credential.type,
|
|
932
|
+
response: {
|
|
933
|
+
attestationObject: bufferToBase64url(response.attestationObject),
|
|
934
|
+
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
935
|
+
transports: response.getTransports ? response.getTransports() : [],
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// 4. Verify with server
|
|
940
|
+
var result = await api("POST", "/../passkey/connect/verify", {
|
|
941
|
+
challengeId: opts.challengeId,
|
|
942
|
+
credential: credentialData,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
if (result.ok) {
|
|
946
|
+
showToast(__t("profile.passkeyAdded"), "success");
|
|
947
|
+
loadConnectedAccounts();
|
|
948
|
+
loadProfile(); // refresh passkey count
|
|
949
|
+
}
|
|
950
|
+
} catch (e) {
|
|
951
|
+
if (e.name !== "NotAllowedError") {
|
|
952
|
+
showToast(e.message || "Failed to add passkey", "error");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function removePasskey(passkeyDid) {
|
|
958
|
+
var ok = await confirmDialog({
|
|
959
|
+
title: __t("profile.removeConfirmTitle"),
|
|
960
|
+
message: __t("profile.removePasskeyConfirmMsg"),
|
|
961
|
+
confirmLabel: __t("profile.remove"),
|
|
962
|
+
});
|
|
963
|
+
if (!ok) return;
|
|
964
|
+
var data = await api("POST", "/../passkey/disconnect", { did: passkeyDid });
|
|
965
|
+
if (data.ok) {
|
|
966
|
+
showToast(__t("profile.passkeyRemoved"), "success");
|
|
967
|
+
loadConnectedAccounts();
|
|
968
|
+
loadProfile(); // refresh passkey count
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function openRenamePasskeyDialog(passkeyDid, currentName) {
|
|
973
|
+
var result = await promptDialog({
|
|
974
|
+
title: __t("profile.renamePasskeyTitle"),
|
|
975
|
+
message: __t("profile.passkeyNameLabel"),
|
|
976
|
+
placeholder: __t("profile.passkeyNamePlaceholder"),
|
|
977
|
+
defaultValue: currentName || "",
|
|
978
|
+
confirmLabel: __t("common.save"),
|
|
979
|
+
});
|
|
980
|
+
if (result === null || result === undefined) return;
|
|
981
|
+
var name = result.trim();
|
|
982
|
+
if (!name) return;
|
|
983
|
+
var data = await api("POST", "/../passkey/rename", { did: passkeyDid, name: name });
|
|
984
|
+
if (data.ok) {
|
|
985
|
+
showToast(__t("profile.passkeyRenamed"), "success");
|
|
986
|
+
loadConnectedAccounts();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ─── DID Wallet open helpers (mirrors LoginPage.openInWallet) ─────────
|
|
991
|
+
var __WEB_WALLET_URL = "https://web.abtwallet.io/";
|
|
992
|
+
|
|
993
|
+
function __isMobile() {
|
|
994
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function __getWalletExtension() {
|
|
998
|
+
return window.ABT_DEV || window.ABT || null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Open the DID auth URL in the wallet — platform-aware:
|
|
1003
|
+
* Mobile → navigate to abt:// deep link
|
|
1004
|
+
* Extension → call extension.open({ action:'login', url, appInfo, memberAppInfo })
|
|
1005
|
+
* Desktop → open web wallet popup
|
|
1006
|
+
*
|
|
1007
|
+
* action must be "login" — the extension only recognises "login", not "bind".
|
|
1008
|
+
* The bind vs login distinction lives in the URL path (/bind/auth vs /login/auth).
|
|
1009
|
+
*/
|
|
1010
|
+
function __openInWallet(url, appInfo, memberAppInfo) {
|
|
1011
|
+
var extension = __getWalletExtension();
|
|
1012
|
+
if (__isMobile()) {
|
|
1013
|
+
window.location.href = url.replace(/^https?:\/\//, "abt://");
|
|
1014
|
+
} else if (extension && typeof extension.open === "function") {
|
|
1015
|
+
extension.open({
|
|
1016
|
+
action: "login",
|
|
1017
|
+
locale: document.documentElement.lang || "en",
|
|
1018
|
+
url: encodeURIComponent(url),
|
|
1019
|
+
appInfo: appInfo || {},
|
|
1020
|
+
memberAppInfo: memberAppInfo || {},
|
|
1021
|
+
});
|
|
1022
|
+
} else {
|
|
1023
|
+
var walletUrl = __WEB_WALLET_URL + "?action=requestAuth&url=" + encodeURIComponent(url);
|
|
1024
|
+
var w = 414, h = 736;
|
|
1025
|
+
var left = window.screenX + window.innerWidth - w, top = window.screenY;
|
|
1026
|
+
window.open(walletUrl, "did-wallet:bind", "left=" + left + ",top=" + top + ",width=" + w + ",height=" + h + ",resizable,scrollbars=yes,status=1,popup");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ─── DID Wallet Connect (bind) ──────────────────────────────────────
|
|
1031
|
+
async function connectWallet() {
|
|
1032
|
+
// 1. Create bind session
|
|
1033
|
+
var tokenData;
|
|
1034
|
+
try {
|
|
1035
|
+
tokenData = await api("GET", "/../did/bind/token?provider=wallet");
|
|
1036
|
+
} catch(e) {
|
|
1037
|
+
showToast("Failed to create bind session", "error");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (!tokenData || !tokenData.token) {
|
|
1041
|
+
showToast("Failed to create bind session", "error");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
var token = tokenData.token;
|
|
1046
|
+
var connectUrl = tokenData.url || "";
|
|
1047
|
+
var appInfo = tokenData.appInfo || {};
|
|
1048
|
+
var memberAppInfo = tokenData.memberAppInfo || {};
|
|
1049
|
+
|
|
1050
|
+
// 2. Build/reset dialog — mirrors login-page QR view structure
|
|
1051
|
+
var d = document.getElementById("wallet-bind-dialog");
|
|
1052
|
+
if (!d) {
|
|
1053
|
+
d = document.createElement("dialog");
|
|
1054
|
+
d.id = "wallet-bind-dialog";
|
|
1055
|
+
document.body.appendChild(d);
|
|
1056
|
+
}
|
|
1057
|
+
// Re-render dialog content each time (fresh token / fresh state)
|
|
1058
|
+
d.innerHTML = '<div class="dialog-content" style="text-align:center;">'
|
|
1059
|
+
+ '<h3 style="margin-bottom:4px;">' + __t("profile.connectWallet") + '</h3>'
|
|
1060
|
+
+ '<div id="wallet-bind-qr" class="wallet-qr-view">'
|
|
1061
|
+
+ '<div class="wallet-qr-placeholder"><span class="wallet-qr-spinner"></span></div>'
|
|
1062
|
+
+ '</div>'
|
|
1063
|
+
+ '<button id="wallet-bind-open" class="btn btn-secondary wallet-deep-link-btn" disabled>'
|
|
1064
|
+
+ getProviderIconSvg("did-connect") + ' ' + __t("profile.openInWallet")
|
|
1065
|
+
+ '</button>'
|
|
1066
|
+
+ '<p id="wallet-bind-status" class="wallet-bind-status">' + __t("profile.scanWithWallet") + '</p>'
|
|
1067
|
+
+ '</div>'
|
|
1068
|
+
+ '<div class="dialog-actions">'
|
|
1069
|
+
+ '<button class="btn btn-secondary" id="wallet-bind-cancel">' + __t("common.cancel") + '</button>'
|
|
1070
|
+
+ '</div>';
|
|
1071
|
+
|
|
1072
|
+
var statusEl = document.getElementById("wallet-bind-status");
|
|
1073
|
+
var qrEl = document.getElementById("wallet-bind-qr");
|
|
1074
|
+
var openBtn = document.getElementById("wallet-bind-open");
|
|
1075
|
+
var cancelBtn = document.getElementById("wallet-bind-cancel");
|
|
1076
|
+
|
|
1077
|
+
// Render QR code (replaces placeholder spinner)
|
|
1078
|
+
if (connectUrl && typeof __QRBundle !== "undefined" && __QRBundle.renderQR) {
|
|
1079
|
+
__QRBundle.renderQR(qrEl, connectUrl, 212);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Enable open button once URL is ready
|
|
1083
|
+
openBtn.disabled = false;
|
|
1084
|
+
openBtn.onclick = function() { __openInWallet(connectUrl, appInfo, memberAppInfo); };
|
|
1085
|
+
|
|
1086
|
+
var pollTimer = null;
|
|
1087
|
+
var pollCount = 0;
|
|
1088
|
+
var maxPolls = 120; // 2 minutes at 1.5s interval
|
|
1089
|
+
|
|
1090
|
+
function cleanup() {
|
|
1091
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1092
|
+
pollTimer = null;
|
|
1093
|
+
d.close();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
cancelBtn.onclick = function() { cleanup(); };
|
|
1097
|
+
d.onclose = function() { if (pollTimer) clearInterval(pollTimer); };
|
|
1098
|
+
|
|
1099
|
+
d.showModal();
|
|
1100
|
+
|
|
1101
|
+
// Helper: collapse QR area and show terminal status
|
|
1102
|
+
function showTerminalStatus(msg, isError) {
|
|
1103
|
+
qrEl.style.display = "none";
|
|
1104
|
+
openBtn.style.display = "none";
|
|
1105
|
+
statusEl.className = "wallet-bind-status" + (isError ? " wallet-bind-error" : "");
|
|
1106
|
+
statusEl.textContent = msg;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 3. Poll for status
|
|
1110
|
+
pollTimer = setInterval(async function() {
|
|
1111
|
+
pollCount++;
|
|
1112
|
+
if (pollCount > maxPolls) {
|
|
1113
|
+
clearInterval(pollTimer);
|
|
1114
|
+
pollTimer = null;
|
|
1115
|
+
showTerminalStatus(__t("profile.connectTimeout"), true);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
var status = await api("GET", "/../did/bind/status?_t_=" + encodeURIComponent(token));
|
|
1120
|
+
if (status.status === "scanned") {
|
|
1121
|
+
statusEl.textContent = __t("profile.confirmInWallet");
|
|
1122
|
+
} else if (status.status === "succeed") {
|
|
1123
|
+
clearInterval(pollTimer);
|
|
1124
|
+
pollTimer = null;
|
|
1125
|
+
// Collapse QR; show "Binding..." while we call bind-complete
|
|
1126
|
+
qrEl.style.display = "none";
|
|
1127
|
+
openBtn.style.display = "none";
|
|
1128
|
+
statusEl.textContent = __t("profile.binding");
|
|
1129
|
+
// 4. Call bind-complete
|
|
1130
|
+
var result = await api("POST", "/../did/connect/bind-complete", { token: token });
|
|
1131
|
+
if (result.ok) {
|
|
1132
|
+
cleanup();
|
|
1133
|
+
showToast(__t("profile.walletConnected"), "success");
|
|
1134
|
+
loadConnectedAccounts();
|
|
1135
|
+
} else {
|
|
1136
|
+
showTerminalStatus(result.error || "Bind failed", true);
|
|
1137
|
+
}
|
|
1138
|
+
} else if (status.status === "error") {
|
|
1139
|
+
clearInterval(pollTimer);
|
|
1140
|
+
pollTimer = null;
|
|
1141
|
+
showTerminalStatus(status.error || "Error", true);
|
|
1142
|
+
}
|
|
1143
|
+
} catch(e) { /* ignore poll errors */ }
|
|
1144
|
+
}, 1500);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// base64url helpers for WebAuthn
|
|
1148
|
+
function base64urlToBuffer(str) {
|
|
1149
|
+
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1150
|
+
while (str.length % 4) str += "=";
|
|
1151
|
+
var bin = atob(str);
|
|
1152
|
+
var buf = new Uint8Array(bin.length);
|
|
1153
|
+
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1154
|
+
return buf.buffer;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function bufferToBase64url(buf) {
|
|
1158
|
+
var bytes = new Uint8Array(buf);
|
|
1159
|
+
var str = "";
|
|
1160
|
+
for (var i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
|
|
1161
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
;
|
|
1165
|
+
|
|
1166
|
+
var akPage = 1;
|
|
1167
|
+
var akPageSize = 20;
|
|
1168
|
+
var akSearchTimer = null;
|
|
1169
|
+
|
|
1170
|
+
function debounceAkSearch() {
|
|
1171
|
+
clearTimeout(akSearchTimer);
|
|
1172
|
+
akSearchTimer = setTimeout(function() { loadAccessKeys(1); }, 300);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function loadAccessKeys(page) {
|
|
1176
|
+
akPage = page || 1;
|
|
1177
|
+
var search = document.getElementById("ak-search").value.trim();
|
|
1178
|
+
var qs = "?page=" + akPage + "&pageSize=" + akPageSize;
|
|
1179
|
+
if (search) qs += "&search=" + encodeURIComponent(search);
|
|
1180
|
+
document.getElementById("ak-table-wrap").innerHTML = skeletonTable(7, 5);
|
|
1181
|
+
document.getElementById("ak-pagination").innerHTML = "";
|
|
1182
|
+
var data = await apiRaw("GET", "/.well-known/service/api/access-keys" + qs);
|
|
1183
|
+
if (!data || data.error) {
|
|
1184
|
+
document.getElementById("ak-table-wrap").innerHTML = '<div class="empty-state"><div class="empty-state-title">' + __t("accessKeys.loadFailed") + '</div></div>';
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
renderAccessKeys(data.keys, { total: data.total, page: data.page, pageSize: data.pageSize });
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function renderAccessKeys(keys, paging) {
|
|
1191
|
+
var wrap = document.getElementById("ak-table-wrap");
|
|
1192
|
+
if (!keys || keys.length === 0) {
|
|
1193
|
+
wrap.innerHTML = '<div class="empty-state"><div class="empty-state-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg></div><div class="empty-state-title">' + __t("accessKeys.noKeys") + '</div><div class="empty-state-desc">' + __t("accessKeys.noKeysHint") + '</div></div>';
|
|
1194
|
+
document.getElementById("ak-pagination").innerHTML = "";
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
var html = '<table class="table"><thead><tr><th>' + __t("accessKeys.col.keyId") + '</th><th>' + __t("accessKeys.col.role") + '</th><th class="col-hide-sm">' + __t("accessKeys.col.remark") + '</th><th class="col-hide-md">' + __t("accessKeys.col.createdBy") + '</th><th>' + __t("accessKeys.col.expires") + '</th><th class="col-hide-md">' + __t("accessKeys.col.lastUsed") + '</th><th>' + __t("common.actions") + '</th></tr></thead><tbody>';
|
|
1199
|
+
keys.forEach(function(k) {
|
|
1200
|
+
var keyIdHtml = '<did-address did="' + escapeHtml(k.accessKeyId) + '" compact></did-address>';
|
|
1201
|
+
var roleBadge = '<span class="badge badge-' + k.role + '">' + __t("common." + k.role) + '</span>';
|
|
1202
|
+
var remark = k.remark ? escapeHtml(k.remark.length > 40 ? k.remark.slice(0, 40) + "..." : k.remark) : '<span style="color:var(--text-secondary)">—</span>';
|
|
1203
|
+
var creator = k.createdByName
|
|
1204
|
+
? escapeHtml(k.createdByName)
|
|
1205
|
+
: '<did-address did="' + escapeHtml(k.createdBy) + '" compact></did-address>';
|
|
1206
|
+
var expiry = k.expireAt ? (new Date(k.expireAt) < new Date() ? '<span style="color:var(--error)">' + __t("common.expired") + '</span>' : relativeTime(k.expireAt)) : __t("common.never");
|
|
1207
|
+
var lastUsed = k.lastUsedAt ? relativeTime(k.lastUsedAt) : __t("common.never");
|
|
1208
|
+
var deleteLabel = k.remark ? escapeHtml(k.remark) : truncateDid(k.accessKeyId);
|
|
1209
|
+
|
|
1210
|
+
html += '<tr>';
|
|
1211
|
+
html += '<td><code style="font-size:12px">' + keyIdHtml + '</code></td>';
|
|
1212
|
+
html += '<td>' + roleBadge + '</td>';
|
|
1213
|
+
html += '<td class="col-hide-sm">' + remark + '</td>';
|
|
1214
|
+
html += '<td class="col-hide-md">' + creator + '</td>';
|
|
1215
|
+
html += '<td>' + expiry + '</td>';
|
|
1216
|
+
html += '<td class="col-hide-md">' + lastUsed + '</td>';
|
|
1217
|
+
html += '<td class="action-cell"><button class="action-trigger" onclick="event.stopPropagation();toggleActionMenu(this)">⋯</button><div class="action-menu"><button class="action-menu-item" onclick="event.stopPropagation();openEditAkDialog(\''+k.accessKeyId+'\')">' + __t("common.edit") + '</button><div class="action-menu-sep"></div><button class="action-menu-item danger" onclick="event.stopPropagation();confirmDeleteAk(\''+k.accessKeyId+'\',\''+escapeHtml(deleteLabel)+'\')">' + __t("common.delete") + '</button></div></td>';
|
|
1218
|
+
html += '</tr>';
|
|
1219
|
+
});
|
|
1220
|
+
html += '</tbody></table>';
|
|
1221
|
+
wrap.innerHTML = html;
|
|
1222
|
+
|
|
1223
|
+
renderPagination("ak-pagination", paging, loadAccessKeys);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function openCreateAkDialog() {
|
|
1227
|
+
var callerRole = __caller.role;
|
|
1228
|
+
var roleOptions = '<option value="guest">Guest</option>';
|
|
1229
|
+
if (callerRole === "member" || callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="member"' + (callerRole === "member" ? ' selected' : '') + '>Member</option>';
|
|
1230
|
+
if (callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="admin"' + (callerRole === "admin" ? ' selected' : '') + '>Admin</option>';
|
|
1231
|
+
if (callerRole === "owner") roleOptions += '<option value="owner">Owner</option>';
|
|
1232
|
+
|
|
1233
|
+
var expiryOptions = '<option value="">' + __t("accessKeys.expirations.never") + '</option><option value="7d">' + __t("accessKeys.expirations.days7") + '</option><option value="30d">' + __t("accessKeys.expirations.days30") + '</option><option value="90d">' + __t("accessKeys.expirations.days90") + '</option><option value="custom">' + __t("accessKeys.expirations.custom") + '</option>';
|
|
1234
|
+
|
|
1235
|
+
var html = '<div class="form-group"><label class="form-label">' + __t("accessKeys.role") + '</label><select class="select" id="ak-create-role">' + roleOptions + '</select></div>';
|
|
1236
|
+
html += '<div class="form-group"><label class="form-label">' + __t("accessKeys.remark") + '</label><input type="text" class="input" id="ak-create-remark" placeholder="' + __t("accessKeys.remarkPlaceholder") + '" maxlength="200" /></div>';
|
|
1237
|
+
html += '<div class="form-group"><label class="form-label">' + __t("accessKeys.expiration") + '</label><select class="select" id="ak-create-expiry" onchange="toggleAkCustomExpiry()">' + expiryOptions + '</select></div>';
|
|
1238
|
+
html += '<div class="form-group hidden" id="ak-custom-expiry-wrap"><label class="form-label">' + __t("accessKeys.customDate") + '</label><input type="datetime-local" class="input" id="ak-create-custom-expiry" /></div>';
|
|
1239
|
+
|
|
1240
|
+
openDialog(__t("accessKeys.createTitle"), html, __t("common.save"), async function() {
|
|
1241
|
+
var role = document.getElementById("ak-create-role").value;
|
|
1242
|
+
var remark = document.getElementById("ak-create-remark").value;
|
|
1243
|
+
var expirySelect = document.getElementById("ak-create-expiry").value;
|
|
1244
|
+
var expireAt = null;
|
|
1245
|
+
|
|
1246
|
+
if (expirySelect === "custom") {
|
|
1247
|
+
var customDate = document.getElementById("ak-create-custom-expiry").value;
|
|
1248
|
+
if (!customDate) { showToast(__t("accessKeys.selectExpDate"), "error"); return; }
|
|
1249
|
+
expireAt = new Date(customDate).toISOString();
|
|
1250
|
+
} else if (expirySelect) {
|
|
1251
|
+
var days = parseInt(expirySelect);
|
|
1252
|
+
expireAt = new Date(Date.now() + days * 86400000).toISOString();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
var result = await apiRaw("POST", "/.well-known/service/api/access-keys", { role: role, remark: remark, expireAt: expireAt });
|
|
1256
|
+
if (!result || result.error) {
|
|
1257
|
+
showToast(result ? result.error : __t("accessKeys.createFailed"), "error");
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
closeDialog();
|
|
1262
|
+
showSecretDialog(result.accessKeySecret);
|
|
1263
|
+
loadAccessKeys(1);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function toggleAkCustomExpiry() {
|
|
1268
|
+
var sel = document.getElementById("ak-create-expiry").value;
|
|
1269
|
+
var wrap = document.getElementById("ak-custom-expiry-wrap");
|
|
1270
|
+
if (sel === "custom") wrap.classList.remove("hidden");
|
|
1271
|
+
else wrap.classList.add("hidden");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function showSecretDialog(secret) {
|
|
1275
|
+
var html = '<div style="margin-bottom:12px;color:var(--error);font-weight:500">' + __t("accessKeys.secretWarning") + '</div>';
|
|
1276
|
+
var copyIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1277
|
+
var copyBtnStyle = 'position:absolute;top:4px;right:4px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-base);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-secondary);transition:color 0.15s,border-color 0.15s';
|
|
1278
|
+
html += '<div style="position:relative"><code id="ak-secret-display" style="display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:13px;word-break:break-all;user-select:all;overflow-wrap:anywhere">' + escapeHtml(secret) + '</code>';
|
|
1279
|
+
html += '<button id="ak-copy-btn" style="' + copyBtnStyle + '" onclick="copyAkSecret()" title="' + __t("common.copy") + '">' + copyIconSvg + '</button></div>';
|
|
1280
|
+
var curlCmd = 'curl -H "Authorization: Bearer ' + secret + '" ' + location.origin + '/.well-known/service/api/did/session';
|
|
1281
|
+
html += '<div style="margin-top:12px;font-size:13px;color:var(--text-secondary)"><strong>' + __t("accessKeys.usage") + '</strong></div>';
|
|
1282
|
+
html += '<div style="position:relative;margin-top:6px"><code id="ak-curl-display" style="display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:12px;word-break:break-all;overflow-wrap:anywhere;user-select:all">' + escapeHtml(curlCmd) + '</code>';
|
|
1283
|
+
html += '<button id="ak-curl-copy-btn" style="' + copyBtnStyle + '" onclick="copyAkCurl()" title="' + __t("common.copy") + '">' + copyIconSvg + '</button></div>';
|
|
1284
|
+
|
|
1285
|
+
openDialog(__t("accessKeys.secretTitle"), html, __t("accessKeys.done"), function() {
|
|
1286
|
+
document.getElementById("ak-secret-display").textContent = "";
|
|
1287
|
+
closeDialog();
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function copyAkCurl() {
|
|
1292
|
+
var text = document.getElementById("ak-curl-display").textContent;
|
|
1293
|
+
var btn = document.getElementById("ak-curl-copy-btn");
|
|
1294
|
+
var origHtml = btn.innerHTML;
|
|
1295
|
+
function onCopied() {
|
|
1296
|
+
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>';
|
|
1297
|
+
btn.style.color = "var(--green)";
|
|
1298
|
+
setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = ""; }, 2000);
|
|
1299
|
+
}
|
|
1300
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1301
|
+
navigator.clipboard.writeText(text).then(onCopied).catch(function() {
|
|
1302
|
+
fallbackCopy(text);
|
|
1303
|
+
onCopied();
|
|
1304
|
+
});
|
|
1305
|
+
} else {
|
|
1306
|
+
fallbackCopy(text);
|
|
1307
|
+
onCopied();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function copyAkSecret() {
|
|
1312
|
+
var text = document.getElementById("ak-secret-display").textContent;
|
|
1313
|
+
var btn = document.getElementById("ak-copy-btn");
|
|
1314
|
+
var origHtml = btn.innerHTML;
|
|
1315
|
+
function onCopied() {
|
|
1316
|
+
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>';
|
|
1317
|
+
btn.style.color = "var(--green)";
|
|
1318
|
+
setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = ""; }, 2000);
|
|
1319
|
+
}
|
|
1320
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1321
|
+
navigator.clipboard.writeText(text).then(onCopied).catch(function() {
|
|
1322
|
+
fallbackCopy(text);
|
|
1323
|
+
onCopied();
|
|
1324
|
+
});
|
|
1325
|
+
} else {
|
|
1326
|
+
fallbackCopy(text);
|
|
1327
|
+
onCopied();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function openEditAkDialog(accessKeyId) {
|
|
1332
|
+
var data = await apiRaw("GET", "/.well-known/service/api/access-keys/" + encodeURIComponent(accessKeyId));
|
|
1333
|
+
if (!data || data.error) { showToast(__t("accessKeys.loadKeyFailed"), "error"); return; }
|
|
1334
|
+
|
|
1335
|
+
var html = '<div class="form-group"><label class="form-label">' + __t("accessKeys.col.keyId") + '</label><code style="font-size:12px">' + escapeHtml(data.accessKeyId) + '</code></div>';
|
|
1336
|
+
html += '<div class="form-group"><label class="form-label">' + __t("accessKeys.role") + '</label><span class="badge badge-' + data.role + '">' + __t("common." + data.role) + '</span></div>';
|
|
1337
|
+
html += '<div class="form-group"><label class="form-label">' + __t("accessKeys.remark") + '</label><input type="text" class="input" id="ak-edit-remark" value="' + escapeHtml(data.remark || "") + '" maxlength="200" /></div>';
|
|
1338
|
+
html += '<div class="form-group"><label class="form-label">' + __t("accessKeys.expiration") + '</label><input type="datetime-local" class="input" id="ak-edit-expiry" value="' + (data.expireAt ? data.expireAt.slice(0,16) : "") + '" /> <label style="margin-top:4px;display:block;font-size:13px"><input type="checkbox" id="ak-edit-no-expiry"' + (data.expireAt ? "" : " checked") + ' onchange="toggleAkEditExpiry()" /> ' + __t("accessKeys.neverExpires") + '</label></div>';
|
|
1339
|
+
|
|
1340
|
+
openDialog(__t("accessKeys.editTitle"), html, __t("common.save"), async function() {
|
|
1341
|
+
var remark = document.getElementById("ak-edit-remark").value;
|
|
1342
|
+
var noExpiry = document.getElementById("ak-edit-no-expiry").checked;
|
|
1343
|
+
var expireAt = noExpiry ? null : (document.getElementById("ak-edit-expiry").value ? new Date(document.getElementById("ak-edit-expiry").value).toISOString() : undefined);
|
|
1344
|
+
|
|
1345
|
+
var body = { remark: remark };
|
|
1346
|
+
if (expireAt !== undefined) body.expireAt = expireAt;
|
|
1347
|
+
|
|
1348
|
+
var result = await apiRaw("PUT", "/.well-known/service/api/access-keys/" + encodeURIComponent(accessKeyId), body);
|
|
1349
|
+
if (!result || result.error) {
|
|
1350
|
+
showToast(result ? result.error : __t("accessKeys.updateFailed"), "error");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
closeDialog();
|
|
1354
|
+
showToast(__t("accessKeys.keyUpdated"), "success");
|
|
1355
|
+
loadAccessKeys(akPage);
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function toggleAkEditExpiry() {
|
|
1360
|
+
var noExpiry = document.getElementById("ak-edit-no-expiry").checked;
|
|
1361
|
+
document.getElementById("ak-edit-expiry").disabled = noExpiry;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function confirmDeleteAk(accessKeyId, label) {
|
|
1365
|
+
openDialog(__t("accessKeys.deleteTitle"), '<p>' + __t("accessKeys.deleteMsg", { label: escapeHtml(label) }) + '</p><p style="color:var(--error)">' + __t("accessKeys.deleteWarning") + '</p>', __t("common.delete"), async function() {
|
|
1366
|
+
var result = await apiRaw("DELETE", "/.well-known/service/api/access-keys/" + encodeURIComponent(accessKeyId));
|
|
1367
|
+
if (result && result.error) {
|
|
1368
|
+
showToast(result.error, "error");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
closeDialog();
|
|
1372
|
+
showToast(__t("accessKeys.keyDeleted"), "success");
|
|
1373
|
+
loadAccessKeys(akPage);
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Direct API call (bypasses team handler's /team prefix)
|
|
1378
|
+
async function apiRaw(method, path, body) {
|
|
1379
|
+
try {
|
|
1380
|
+
var opts = { method: method, headers: { "Accept": "application/json" } };
|
|
1381
|
+
if (body !== undefined) {
|
|
1382
|
+
opts.headers["Content-Type"] = "application/json";
|
|
1383
|
+
opts.body = JSON.stringify(body);
|
|
1384
|
+
}
|
|
1385
|
+
var res = await fetch(path, opts);
|
|
1386
|
+
if (res.status === 204) return {};
|
|
1387
|
+
return await res.json();
|
|
1388
|
+
} catch(e) {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
registerTab("access-keys", function() { loadAccessKeys(1); });
|