feat(portal): plain no-token "open" links for dev feedback (PORTAL_OPEN_LINKS)
Adds a frictionless shareable link so anyone can open a project's client portal
during dev without minting/copying a magic token. GET /portal/open/{project_id}
(gated by PORTAL_OPEN_LINKS) provisions the client session and lands on /portal;
lives under /portal so it works through a proxy exposing only /portal/*.
The project page's "Copy client link" modal now leads with this Quick share link
(amber, host taken from window.location.origin so it always matches the host you
copied it from — no more LAN-vs-public foot-gun). The token-based generate/list/
revoke stays below for the eventual secure path.
PORTAL_OPEN_LINKS defaults ON for the prototype (whole app is open anyway) and logs
a warning; set =false before real clients. The get_current_client seam is
untouched, so M4 auth still layers in front of the same routes regardless.
Verified: compiles, share script balances, detail.html parses, flag default
on / =false off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2112,6 +2112,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
Anyone with a link can view this project's client portal (read-only). Links are revocable.
|
||||
</p>
|
||||
|
||||
{% if portal_open_links %}
|
||||
<!-- Dev quick link: plain, no-token URL anyone can open (PORTAL_OPEN_LINKS on) -->
|
||||
<div class="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50">
|
||||
<label class="block text-xs font-medium text-amber-700 dark:text-amber-300 mb-1">Quick share link (dev — anyone can open, no login)</label>
|
||||
<div class="flex gap-2">
|
||||
<input id="open-url" readonly
|
||||
class="flex-1 px-3 py-2 text-sm rounded-lg border border-amber-300 dark:border-amber-700 bg-white dark:bg-slate-900 text-gray-800 dark:text-gray-200" />
|
||||
<button onclick="copyOpenUrl(this)" class="px-3 py-2 text-sm rounded-lg bg-seismo-orange text-white hover:bg-orange-600">Copy</button>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1">For feedback during development. Disable <code>PORTAL_OPEN_LINKS</code> before real clients.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="share-new" class="hidden mb-4">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">New link — copy it now</label>
|
||||
<div class="flex gap-2">
|
||||
@@ -2134,10 +2147,20 @@ const SHARE_PROJECT_ID = "{{ project_id }}";
|
||||
function openShareModal() {
|
||||
document.getElementById('share-modal').classList.remove('hidden');
|
||||
document.getElementById('share-new').classList.add('hidden');
|
||||
const ou = document.getElementById('open-url'); // only present when PORTAL_OPEN_LINKS on
|
||||
if (ou) ou.value = `${location.origin}/portal/open/${SHARE_PROJECT_ID}`;
|
||||
loadShareLinks();
|
||||
}
|
||||
function closeShareModal() { document.getElementById('share-modal').classList.add('hidden'); }
|
||||
|
||||
function copyOpenUrl(btn) {
|
||||
const inp = document.getElementById('open-url');
|
||||
inp.select();
|
||||
const done = () => { btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'Copy', 1500); };
|
||||
if (navigator.clipboard) navigator.clipboard.writeText(inp.value).then(done).catch(() => { document.execCommand('copy'); done(); });
|
||||
else { document.execCommand('copy'); done(); }
|
||||
}
|
||||
|
||||
async function loadShareLinks() {
|
||||
const list = document.getElementById('share-list');
|
||||
list.innerHTML = '<div class="text-sm text-gray-400">Loading…</div>';
|
||||
|
||||
Reference in New Issue
Block a user