Close create-invite modal after success; drop redundant result panel.
Release rolling / release (push) Successful in 8m4s
Release rolling / release (push) Successful in 8m4s
Invite strings already appear under Active invites with Copy, so the post-create panel in the modal was unnecessary and felt stuck open.
This commit is contained in:
@@ -1723,80 +1723,36 @@ window.applyRoleUI = applyRoleUI;
|
|||||||
|
|
||||||
// Wire access view actions once
|
// Wire access view actions once
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Invite form (role / TTL / max uses + full pd1 string + Copy invite)
|
// Invite form (role / TTL / max uses) — result lives in Active invites list, not a second panel
|
||||||
const inviteForm = document.getElementById('invite-peer-form');
|
const inviteForm = document.getElementById('invite-peer-form');
|
||||||
|
|
||||||
function setInviteResultUi({ token, kind, publicKeyHex, role, jti, persistent }) {
|
function hideInvitePeerModal() {
|
||||||
const resultBox = document.getElementById('invite-token-result');
|
const modalEl = document.getElementById('invitePeerModal');
|
||||||
const tokenInput = document.getElementById('invite-token-value');
|
if (!modalEl || typeof bootstrap === 'undefined') return;
|
||||||
const lenEl = document.getElementById('invite-token-len');
|
const inst = bootstrap.Modal.getInstance(modalEl);
|
||||||
const pkgKeys = document.getElementById('invite-pkg-keys');
|
if (inst) inst.hide();
|
||||||
const pubEl = document.getElementById('invite-pkg-pubkey');
|
else new bootstrap.Modal(modalEl).hide();
|
||||||
const grantEl = document.getElementById('invite-grant-meta');
|
|
||||||
if (!token || !tokenInput) return;
|
|
||||||
// Full string only — never slice/substring (pd1 invites break if truncated)
|
|
||||||
const full = String(token);
|
|
||||||
tokenInput.value = full;
|
|
||||||
tokenInput.defaultValue = full;
|
|
||||||
if (lenEl) lenEl.textContent = `${full.length} characters (complete)`;
|
|
||||||
if (grantEl) {
|
|
||||||
const bits = [
|
|
||||||
role ? `role=${role}` : null,
|
|
||||||
jti ? `jti=${String(jti).slice(0, 12)}…` : null,
|
|
||||||
persistent ? 'persistent (never expires, unlimited reconnects)' : null,
|
|
||||||
kind ? `kind=${kind}` : null,
|
|
||||||
].filter(Boolean);
|
|
||||||
grantEl.textContent = bits.length
|
|
||||||
? `Capability grant linked to this invite: ${bits.join(' · ')}`
|
|
||||||
: '';
|
|
||||||
grantEl.classList.toggle('hidden', !bits.length);
|
|
||||||
}
|
|
||||||
resultBox?.classList.remove('hidden');
|
|
||||||
if (publicKeyHex && pubEl && /^[0-9a-fA-F]{64}$/.test(String(publicKeyHex))) {
|
|
||||||
pubEl.textContent = String(publicKeyHex).toLowerCase();
|
|
||||||
pkgKeys?.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
if (pubEl) pubEl.textContent = '';
|
|
||||||
pkgKeys?.classList.add('hidden');
|
|
||||||
}
|
|
||||||
// Focus + select so user can verify nothing is clipped
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
tokenInput.focus();
|
|
||||||
tokenInput.select();
|
|
||||||
tokenInput.setSelectionRange?.(0, full.length);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyInviteFull(sourceEl) {
|
async function copyInviteText(token, { quiet = false } = {}) {
|
||||||
const token =
|
const full = String(token || '');
|
||||||
(sourceEl && 'value' in sourceEl ? sourceEl.value : null) ||
|
|
||||||
document.getElementById('invite-token-value')?.value ||
|
|
||||||
'';
|
|
||||||
const full = String(token);
|
|
||||||
if (!full) {
|
if (!full) {
|
||||||
showAlert('warning', 'No invite to copy yet — create one first');
|
if (!quiet) showAlert('warning', 'No invite to copy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(full);
|
await navigator.clipboard.writeText(full);
|
||||||
|
if (!quiet) {
|
||||||
showAlert('success', `Invite copied (${full.length} characters, full string)`);
|
showAlert('success', `Invite copied (${full.length} characters, full string)`);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
if (sourceEl?.select) {
|
if (!quiet) {
|
||||||
sourceEl.focus();
|
showAlert(
|
||||||
sourceEl.select();
|
'warning',
|
||||||
sourceEl.setSelectionRange?.(0, full.length);
|
'Could not auto-copy — use Copy invite on the card under Active invites'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const ok = document.execCommand?.('copy');
|
|
||||||
if (ok) {
|
|
||||||
showAlert('success', `Invite copied (${full.length} characters, full string)`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
showAlert('warning', 'Could not copy — select the full invite and copy manually (Cmd/Ctrl+C)');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1820,30 +1776,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const res = await manager.request(Methods.invitePeer, { role, ttlHours, maxUses });
|
const res = await manager.request(Methods.invitePeer, { role, ttlHours, maxUses });
|
||||||
const token = res?.data?.token || res?.data?.invite || res?.data?.capability;
|
const token = res?.data?.token || res?.data?.invite || res?.data?.capability;
|
||||||
const kind = res?.data?.kind || 'invite';
|
const kind = res?.data?.kind || 'invite';
|
||||||
const publicKeyHex = res?.data?.publicKeyHex || null;
|
|
||||||
const grantRole = res?.data?.role || role;
|
const grantRole = res?.data?.role || role;
|
||||||
const jti = res?.data?.jti || null;
|
|
||||||
const persistent = res?.data?.persistent === true || (ttlHours === 0 && maxUses === 0);
|
|
||||||
if (token) {
|
if (token) {
|
||||||
setInviteResultUi({
|
// Close create form — full string + Copy live under Active invites
|
||||||
token,
|
hideInvitePeerModal();
|
||||||
kind,
|
const copied = await copyInviteText(token, { quiet: true });
|
||||||
publicKeyHex,
|
const base =
|
||||||
role: grantRole,
|
|
||||||
jti,
|
|
||||||
persistent,
|
|
||||||
});
|
|
||||||
const label =
|
|
||||||
kind === 'pd1'
|
kind === 'pd1'
|
||||||
? `Invite created for role “${grantRole}” — share the full pd1. string only`
|
? `Invite created for role “${grantRole}” — share the full pd1. string`
|
||||||
: `Invite created for role “${grantRole}” — copy the full grant`;
|
: `Invite created for role “${grantRole}”`;
|
||||||
showAlert('success', label);
|
showAlert(
|
||||||
// Auto-copy full string for convenience
|
'success',
|
||||||
await copyInviteFull(document.getElementById('invite-token-value'));
|
copied
|
||||||
|
? `${base} (copied · ${String(token).length} characters)`
|
||||||
|
: `${base}. Use Copy invite on the Active invites card.`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('danger', 'Server returned an empty invite — try again');
|
showAlert('danger', 'Server returned an empty invite — try again');
|
||||||
}
|
}
|
||||||
loadAccessView();
|
await loadAccessView();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('danger', err.message || 'Failed to create invite');
|
showAlert('danger', err.message || 'Failed to create invite');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1851,39 +1802,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById('invite-token-copy')?.addEventListener('click', async () => {
|
|
||||||
await copyInviteFull(document.getElementById('invite-token-value'));
|
|
||||||
});
|
|
||||||
document.getElementById('invite-token-select-all')?.addEventListener('click', () => {
|
|
||||||
const el = document.getElementById('invite-token-value');
|
|
||||||
if (!el) return;
|
|
||||||
el.focus();
|
|
||||||
el.select();
|
|
||||||
el.setSelectionRange?.(0, el.value.length);
|
|
||||||
});
|
|
||||||
document.getElementById('invite-pkg-pubkey-copy')?.addEventListener('click', async () => {
|
|
||||||
const pk = document.getElementById('invite-pkg-pubkey')?.textContent?.trim() || '';
|
|
||||||
if (!pk) return;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(pk);
|
|
||||||
showAlert('success', `Public key copied (${pk.length} characters)`);
|
|
||||||
} catch {
|
|
||||||
showAlert('warning', 'Could not copy public key');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('invitePeerModal')?.addEventListener('show.bs.modal', () => {
|
|
||||||
document.getElementById('invite-token-result')?.classList.add('hidden');
|
|
||||||
document.getElementById('invite-pkg-keys')?.classList.add('hidden');
|
|
||||||
const tokenInput = document.getElementById('invite-token-value');
|
|
||||||
if (tokenInput) {
|
|
||||||
tokenInput.value = '';
|
|
||||||
tokenInput.defaultValue = '';
|
|
||||||
}
|
|
||||||
const lenEl = document.getElementById('invite-token-len');
|
|
||||||
if (lenEl) lenEl.textContent = '';
|
|
||||||
const pubEl = document.getElementById('invite-pkg-pubkey');
|
|
||||||
if (pubEl) pubEl.textContent = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// First-connect checklist
|
// First-connect checklist
|
||||||
document.getElementById('first-connect-dismiss')?.addEventListener('click', dismissFirstConnectChecklist);
|
document.getElementById('first-connect-dismiss')?.addEventListener('click', dismissFirstConnectChecklist);
|
||||||
|
|||||||
+4
-36
@@ -4329,45 +4329,13 @@ services:
|
|||||||
<div class="form-text text-muted">0 = unlimited (default)</div>
|
<div class="form-text text-muted">0 = unlimited (default)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-3">Grants persist forever by default. Invited clients reconnect automatically with their saved capability + stable identity. Set TTL/uses only if you want the grant to expire.</p>
|
<p class="small text-muted mb-0">
|
||||||
<div id="invite-token-result" class="invite-token-result hidden">
|
Grants persist forever by default. After create, the full <code>pd1.</code> invite appears under
|
||||||
<label class="form-label" for="invite-token-value">Operator invite <span class="text-muted fw-normal">(pd1.… full string — never truncated)</span></label>
|
<strong>Active invites</strong> (with Copy). Invited clients reconnect with their saved capability + stable identity.
|
||||||
<textarea
|
|
||||||
id="invite-token-value"
|
|
||||||
class="form-control bg-dark text-white font-monospace invite-secret-full"
|
|
||||||
rows="5"
|
|
||||||
readonly
|
|
||||||
wrap="soft"
|
|
||||||
spellcheck="false"
|
|
||||||
autocomplete="off"
|
|
||||||
aria-describedby="invite-token-hint invite-token-len"
|
|
||||||
></textarea>
|
|
||||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
|
|
||||||
<button type="button" class="btn btn-primary" id="invite-token-copy" title="Copy full invite to clipboard">
|
|
||||||
<i class="fas fa-copy me-1"></i>Copy invite
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="invite-token-select-all" title="Select full invite text">
|
|
||||||
Select all
|
|
||||||
</button>
|
|
||||||
<span class="small text-muted" id="invite-token-len"></span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-info mt-2 mb-1" id="invite-grant-meta"></p>
|
|
||||||
<p class="small text-muted mt-1 mb-0" id="invite-token-hint">
|
|
||||||
Share <strong>only this pd1. string</strong> with the operator. It embeds the public key + capability for the role you chose. Paste the entire string in Add peer (do not use an old saved peer entry).
|
|
||||||
</p>
|
</p>
|
||||||
<div id="invite-pkg-keys" class="invite-pkg-keys hidden mt-3">
|
|
||||||
<label class="form-label small text-muted mb-1">Server public key (also inside the package)</label>
|
|
||||||
<div class="d-flex gap-2 align-items-start">
|
|
||||||
<code id="invite-pkg-pubkey" class="invite-secret-full flex-grow-1 user-select-all mb-0"></code>
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm flex-shrink-0" id="invite-pkg-pubkey-copy" title="Copy public key">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-secondary">
|
<div class="modal-footer border-secondary">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" id="invite-peer-submit">
|
<button type="submit" class="btn btn-primary" id="invite-peer-submit">
|
||||||
<i class="fas fa-ticket me-1"></i>Create invite
|
<i class="fas fa-ticket me-1"></i>Create invite
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user