|
143 | 143 | </div> |
144 | 144 | </div> |
145 | 145 |
|
| 146 | + <div class="settings-section" data-tab="platforms"> |
| 147 | + <div class="settings-section-header"> Multiple Accounts</div> |
| 148 | + <div class="settings-section-description"> |
| 149 | + Add more than one credential per platform to pull programs, domains and scope from |
| 150 | + <strong>all</strong> of your accounts at once. Every enabled account is queried and the |
| 151 | + results are merged (deduplicated). The single credential in the section below is used |
| 152 | + automatically as an extra <code>env</code> account. |
| 153 | + </div> |
| 154 | + <div class="settings-section-body"> |
| 155 | + <div id="settings-accounts-manager" style="padding:8px 24px 16px;"> |
| 156 | + <div style="color:var(--text-muted);font-size:13px;">Loading accounts…</div> |
| 157 | + </div> |
| 158 | + </div> |
| 159 | + </div> |
| 160 | +
|
146 | 161 | <div class="settings-section" data-tab="platforms"> |
147 | 162 | <div class="settings-section-header"> Bug Bounty Platform API Keys</div> |
148 | 163 | <div class="settings-section-description"> |
|
268 | 283 |
|
269 | 284 | // Restore last-active tab (default: Platforms & Keys — the most-used surface). |
270 | 285 | settingsTab(window.state._settingsTab || 'platforms'); |
| 286 | + // Populate the multi-account manager (async — fills the placeholder in-place). |
| 287 | + loadSettingsAccounts(); |
| 288 | + } |
| 289 | + |
| 290 | + // ── Multi-account manager (Platforms & Keys tab) ────────────────────────── |
| 291 | + // Lets the user store several credentials per platform. Every enabled account |
| 292 | + // is queried when fetching programs/scope (see accounts.For on the server) and |
| 293 | + // the results are merged — so one dashboard pulls from all your accounts. |
| 294 | + const ACCT_PLATFORMS = [ |
| 295 | + { id: 'h1', name: 'HackerOne', fields: ['username', 'token'] }, |
| 296 | + { id: 'bc', name: 'Bugcrowd', fields: ['token'] }, |
| 297 | + { id: 'it', name: 'Intigriti', fields: ['token'] }, |
| 298 | + { id: 'ywh', name: 'YesWeHack', fields: ['token', 'email', 'password'] }, |
| 299 | + ]; |
| 300 | + |
| 301 | + async function loadSettingsAccounts() { |
| 302 | + const host = document.getElementById('settings-accounts-manager'); |
| 303 | + if (!host) return; |
| 304 | + let accts = []; |
| 305 | + try { |
| 306 | + const data = await window.apiFetch('/api/accounts'); // "" platform = all |
| 307 | + accts = data.accounts || []; |
| 308 | + } catch (e) { |
| 309 | + host.innerHTML = `<div style="color:var(--accent-amber);font-size:13px;">Failed to load accounts: ${escValue(e.message || String(e))}</div>`; |
| 310 | + return; |
| 311 | + } |
| 312 | + renderSettingsAccounts(host, accts); |
| 313 | + } |
| 314 | + |
| 315 | + function renderSettingsAccounts(host, accts) { |
| 316 | + const byPlatform = {}; |
| 317 | + for (const a of accts) (byPlatform[a.platform] = byPlatform[a.platform] || []).push(a); |
| 318 | + |
| 319 | + host.innerHTML = ACCT_PLATFORMS.map((p) => { |
| 320 | + const list = byPlatform[p.id] || []; |
| 321 | + const rows = list.length |
| 322 | + ? list.map((a) => ` |
| 323 | + <div class="acct-row"> |
| 324 | + <div class="acct-meta"> |
| 325 | + <div class="acct-label">${escValue(a.label)}${a.enabled ? '' : ' <span class="acct-disabled">(disabled)</span>'}</div> |
| 326 | + <div class="acct-sub">${a.username ? escValue(a.username) + ' · ' : ''}${a.token_set ? 'token ' + escValue(a.token_mask || '••••') : 'no token'}</div> |
| 327 | + </div> |
| 328 | + <button class="acct-toggle ${a.enabled ? 'on' : 'off'}" onclick="window.SettingsPage.toggleAccount(${a.id}, ${a.enabled ? 'false' : 'true'})">${a.enabled ? 'On' : 'Off'}</button> |
| 329 | + <button class="acct-del" title="Delete" onclick="window.SettingsPage.deleteAccount(${a.id})">✕</button> |
| 330 | + </div>`).join('') |
| 331 | + : `<div class="acct-empty">No extra accounts yet — add one below.</div>`; |
| 332 | + const addFields = p.fields.map((f) => { |
| 333 | + const isSecret = f === 'password' || f === 'token'; |
| 334 | + const ph = f.charAt(0).toUpperCase() + f.slice(1); |
| 335 | + return `<input id="acct-${p.id}-${f}" type="${isSecret ? 'password' : 'text'}" placeholder="${ph}" class="form-control premium-input acct-input">`; |
| 336 | + }).join(''); |
| 337 | + return ` |
| 338 | + <div class="acct-platform"> |
| 339 | + <div class="acct-platform-head">${escValue(p.name)}${list.length ? ` <span class="acct-count">${list.length}</span>` : ''}</div> |
| 340 | + <div class="acct-list">${rows}</div> |
| 341 | + <div class="acct-add"> |
| 342 | + <input id="acct-${p.id}-label" type="text" placeholder="Label (e.g. main, alt)" class="form-control premium-input acct-input"> |
| 343 | + ${addFields} |
| 344 | + <button class="btn btn-primary acct-add-btn" onclick="window.SettingsPage.addAccount('${p.id}')">+ Add</button> |
| 345 | + </div> |
| 346 | + </div>`; |
| 347 | + }).join(''); |
| 348 | + } |
| 349 | + |
| 350 | + async function addAccount(platformId) { |
| 351 | + const p = ACCT_PLATFORMS.find((x) => x.id === platformId); |
| 352 | + if (!p) return; |
| 353 | + const label = (document.getElementById(`acct-${platformId}-label`)?.value || '').trim(); |
| 354 | + if (!label) { window.showToast('warning', 'Label required', 'Give the account a label (e.g. main, alt).'); return; } |
| 355 | + const body = { platform: platformId, label, enabled: true }; |
| 356 | + for (const f of p.fields) body[f] = (document.getElementById(`acct-${platformId}-${f}`)?.value || '').trim(); |
| 357 | + try { |
| 358 | + await window.apiPost('/api/accounts', body); |
| 359 | + window.showToast('success', 'Account added', `${label} saved — programs will refresh in the background.`); |
| 360 | + await loadSettingsAccounts(); |
| 361 | + } catch (e) { |
| 362 | + window.showToast('error', 'Add failed', e.message); |
| 363 | + } |
| 364 | + } |
| 365 | + |
| 366 | + async function toggleAccount(id, enabled) { |
| 367 | + try { |
| 368 | + await window.apiPost(`/api/accounts/${id}/toggle`, { enabled }); |
| 369 | + await loadSettingsAccounts(); |
| 370 | + } catch (e) { |
| 371 | + window.showToast('error', 'Toggle failed', e.message); |
| 372 | + } |
| 373 | + } |
| 374 | + |
| 375 | + async function deleteAccount(id) { |
| 376 | + if (!window.confirm('Delete this account?')) return; |
| 377 | + try { |
| 378 | + await window.apiDelete(`/api/accounts/${id}`); |
| 379 | + window.showToast('success', 'Deleted', 'Account removed.'); |
| 380 | + await loadSettingsAccounts(); |
| 381 | + } catch (e) { |
| 382 | + window.showToast('error', 'Delete failed', e.message); |
| 383 | + } |
271 | 384 | } |
272 | 385 |
|
273 | 386 | // Show only the sections belonging to the chosen tab; highlight the active pill. |
|
523 | 636 | loadConfig, |
524 | 637 | renderSettings, |
525 | 638 | settingsTab, |
| 639 | + loadSettingsAccounts, |
| 640 | + addAccount, |
| 641 | + toggleAccount, |
| 642 | + deleteAccount, |
526 | 643 | saveOpenRouterKey, |
527 | 644 | saveOpenCodeKey, |
528 | 645 | saveOpenCodeModel, |
|
0 commit comments