Gate entire site behind PromptPCB login (Corben tenant auth); catalog + files served only to authed users
This commit is contained in:
parent
706e50cdf5
commit
86e9656c72
9 changed files with 115 additions and 13 deletions
|
|
@ -57,9 +57,8 @@ async function render() {
|
||||||
if (data) { const img = new Image(); img.src = data; img.style.cssText = 'width:100%;height:100%;object-fit:contain'; cv.replaceWith(img); }
|
if (data) { const img = new Image(); img.src = data; img.style.cssText = 'width:100%;height:100%;object-fit:contain'; cv.replaceWith(img); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(async () => {
|
window.ppcbReady(async () => {
|
||||||
PARTS = await (await fetch('/assets/catalog.json')).json();
|
PARTS = await window.getCatalog(); // authed — only reachable once signed in
|
||||||
$('#cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
|
|
||||||
renderChips(); render();
|
renderChips(); render();
|
||||||
let t; $('#q').addEventListener('input', () => { clearTimeout(t); t = setTimeout(render, 160); });
|
let t; $('#q').addEventListener('input', () => { clearTimeout(t); t = setTimeout(render, 160); });
|
||||||
})();
|
});
|
||||||
|
|
|
||||||
86
assets/auth.js
Normal file
86
assets/auth.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// auth.js — gates the whole site behind a PromptPCB account (Corben tenant auth for this site).
|
||||||
|
// Nothing loads until you're signed in; the catalog + file URLs come only from the authed function.
|
||||||
|
(function () {
|
||||||
|
var API = 'https://api.corben.world';
|
||||||
|
var SITE = '6ab5ec79-8b91-453d-aacc-5be76f9271d1'; // this site IS the tenant
|
||||||
|
var FN_CATALOG = '0f3cd09e-81ce-43f4-88e7-cde3d91f122e';
|
||||||
|
var TK_CATALOG = '687d2466cea418815433b60946d6742a';
|
||||||
|
var LS = 'ppcb_token';
|
||||||
|
window.PPCB = { token: null, user: null };
|
||||||
|
|
||||||
|
window.ppcbReady = function (fn) {
|
||||||
|
if (window.PPCB.token) fn(); else addEventListener('ppcb:auth', function () { fn(); }, { once: true });
|
||||||
|
};
|
||||||
|
window.getCatalog = async function () {
|
||||||
|
var r = await fetch(API + '/functions/' + FN_CATALOG + '/invoke?token=' + TK_CATALOG, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.PPCB.token }, body: '{}' });
|
||||||
|
if (r.status === 401) { logout(); throw new Error('session expired'); }
|
||||||
|
var d = await r.json(); return d.parts || [];
|
||||||
|
};
|
||||||
|
window.ppcbAuth = function () { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.PPCB.token || '') }; };
|
||||||
|
|
||||||
|
function authed(user) {
|
||||||
|
window.PPCB.user = user; document.documentElement.classList.add('ppcb-authed');
|
||||||
|
var g = document.getElementById('ppcb-gate'); if (g) g.style.display = 'none';
|
||||||
|
var c = document.querySelector('.credits');
|
||||||
|
if (c) c.innerHTML = '<span title="' + user.email + '" style="cursor:default">◆ ' + (user.display_name || user.email.split('@')[0]) + '</span> · <a href="#" id="ppcb-logout" style="color:var(--dim)">sign out</a>';
|
||||||
|
var lo = document.getElementById('ppcb-logout'); if (lo) lo.onclick = function (e) { e.preventDefault(); logout(); };
|
||||||
|
dispatchEvent(new Event('ppcb:auth'));
|
||||||
|
}
|
||||||
|
function logout() { localStorage.removeItem(LS); location.reload(); }
|
||||||
|
window.ppcbLogout = logout;
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
var t = localStorage.getItem(LS);
|
||||||
|
if (!t) return gate();
|
||||||
|
try {
|
||||||
|
var r = await fetch(API + '/auth/tenant/me', { headers: { Authorization: 'Bearer ' + t } });
|
||||||
|
if (!r.ok) return gate();
|
||||||
|
var u = await r.json(); window.PPCB.token = t; authed(u);
|
||||||
|
} catch (e) { gate(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function gate() {
|
||||||
|
document.documentElement.classList.remove('ppcb-authed');
|
||||||
|
if (document.getElementById('ppcb-gate')) { document.getElementById('ppcb-gate').style.display = 'flex'; return; }
|
||||||
|
var el = document.createElement('div'); el.id = 'ppcb-gate';
|
||||||
|
el.innerHTML =
|
||||||
|
'<div class="gate-card">' +
|
||||||
|
'<div class="gate-brand"><span class="mk"></span> PromptPCB <small>/ parts</small></div>' +
|
||||||
|
'<div class="gate-h">Sign in to the part library</div>' +
|
||||||
|
'<div class="gate-sub">One PromptPCB account. Every part—real specs, 3D, SPICE—once you’re in.</div>' +
|
||||||
|
'<div class="gate-tabs"><button data-m="login" class="on">Sign in</button><button data-m="signup">Create account</button></div>' +
|
||||||
|
'<input id="g-name" placeholder="Display name" style="display:none">' +
|
||||||
|
'<input id="g-email" type="email" placeholder="you@example.com" autocomplete="email">' +
|
||||||
|
'<input id="g-pass" type="password" placeholder="Password (8+ characters)" autocomplete="current-password">' +
|
||||||
|
'<button class="btn gate-go" id="g-go">Sign in</button>' +
|
||||||
|
'<div class="gate-msg" id="g-msg"></div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
var mode = 'login';
|
||||||
|
el.querySelectorAll('.gate-tabs button').forEach(function (b) {
|
||||||
|
b.onclick = function () {
|
||||||
|
mode = b.dataset.m; el.querySelectorAll('.gate-tabs button').forEach(function (x) { x.classList.toggle('on', x === b); });
|
||||||
|
document.getElementById('g-name').style.display = mode === 'signup' ? 'block' : 'none';
|
||||||
|
document.getElementById('g-go').textContent = mode === 'signup' ? 'Create account' : 'Sign in';
|
||||||
|
document.getElementById('g-msg').textContent = '';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
document.getElementById('g-go').onclick = go;
|
||||||
|
el.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(); });
|
||||||
|
async function go() {
|
||||||
|
var email = document.getElementById('g-email').value.trim(), pass = document.getElementById('g-pass').value;
|
||||||
|
var msg = document.getElementById('g-msg'); msg.style.color = 'var(--dim)'; msg.innerHTML = '<span class="spin"></span> …';
|
||||||
|
var path = mode === 'signup' ? '/auth/tenant/register' : '/auth/tenant/login';
|
||||||
|
var body = { tenant_id: SITE, email: email, password: pass };
|
||||||
|
if (mode === 'signup') body.display_name = document.getElementById('g-name').value.trim();
|
||||||
|
try {
|
||||||
|
var r = await fetch(API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||||
|
var d = await r.json();
|
||||||
|
if (!r.ok) { msg.style.color = 'var(--bad,#ff6b6b)'; msg.textContent = d.error || 'failed'; return; }
|
||||||
|
localStorage.setItem(LS, d.token); window.PPCB.token = d.token; authed(d.user);
|
||||||
|
} catch (e) { msg.style.color = '#ff6b6b'; msg.textContent = 'network error'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', check); else check();
|
||||||
|
})();
|
||||||
|
|
@ -49,11 +49,10 @@ function schematicSVG(p) {
|
||||||
return s + '</svg>';
|
return s + '</svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
window.ppcbReady(async () => {
|
||||||
const parts = await (await fetch('/assets/catalog.json')).json();
|
const parts = await window.getCatalog();
|
||||||
const p = parts.find((x) => x.id === id);
|
const p = parts.find((x) => x.id === id);
|
||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
document.getElementById('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
|
|
||||||
if (!p) { root.innerHTML = '<div class="empty">Part not found. <a href="/">Back to library</a></div>'; return; }
|
if (!p) { root.innerHTML = '<div class="empty">Part not found. <a href="/">Back to library</a></div>'; return; }
|
||||||
document.title = `${p.mpn || p.id} — PromptPCB Parts`;
|
document.title = `${p.mpn || p.id} — PromptPCB Parts`;
|
||||||
|
|
||||||
|
|
@ -105,7 +104,6 @@ function schematicSVG(p) {
|
||||||
show('d3');
|
show('d3');
|
||||||
|
|
||||||
document.getElementById('dl-all').onclick = async () => {
|
document.getElementById('dl-all').onclick = async () => {
|
||||||
if (!spendCredit()) return;
|
|
||||||
try { const wrl = await (await fetch(p.model_3d)).text(); dl(`${p.id}.wrl`, wrl); } catch {}
|
try { const wrl = await (await fetch(p.model_3d)).text(); dl(`${p.id}.wrl`, wrl); } catch {}
|
||||||
dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`);
|
dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`);
|
||||||
dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json');
|
dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json');
|
||||||
|
|
@ -115,4 +113,4 @@ function schematicSVG(p) {
|
||||||
if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); }
|
if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); }
|
||||||
document.getElementById('lib').textContent = '✓ In library';
|
document.getElementById('lib').textContent = '✓ In library';
|
||||||
};
|
};
|
||||||
})();
|
});
|
||||||
|
|
|
||||||
|
|
@ -152,3 +152,21 @@ body{animation:pagein .5s ease both}
|
||||||
.vizpanel .hint{position:absolute;bottom:12px;left:12px;font-family:var(--mono);font-size:11px;color:#555}
|
.vizpanel .hint{position:absolute;bottom:12px;left:12px;font-family:var(--mono);font-size:11px;color:#555}
|
||||||
.viznote{font-family:var(--mono);font-size:12px;color:var(--dim);margin-top:12px;line-height:1.5}
|
.viznote{font-family:var(--mono);font-size:12px;color:var(--dim);margin-top:12px;line-height:1.5}
|
||||||
.viznote b{color:var(--acc)}
|
.viznote b{color:var(--acc)}
|
||||||
|
|
||||||
|
/* login gate */
|
||||||
|
html:not(.ppcb-authed) body{overflow:hidden}
|
||||||
|
#ppcb-gate{position:fixed;inset:0;z-index:200;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:24px}
|
||||||
|
.gate-card{width:100%;max-width:400px;display:flex;flex-direction:column}
|
||||||
|
.gate-brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:20px;margin-bottom:30px}
|
||||||
|
.gate-brand .mk{width:13px;height:13px;background:var(--acc);border-radius:2px}
|
||||||
|
.gate-brand small{font-family:var(--mono);font-size:12px;color:var(--dim);font-weight:400}
|
||||||
|
.gate-h{font-size:29px;font-weight:700;letter-spacing:-.02em;margin-bottom:8px;line-height:1.1}
|
||||||
|
.gate-sub{color:var(--dim);font-size:15px;line-height:1.5;margin-bottom:26px}
|
||||||
|
.gate-tabs{display:flex;border:1px solid var(--line2);border-radius:2px;width:fit-content;overflow:hidden;margin-bottom:18px}
|
||||||
|
.gate-tabs button{background:transparent;border:none;border-right:1px solid var(--line2);color:var(--dim);font-family:var(--mono);font-size:13px;padding:8px 18px;cursor:pointer}
|
||||||
|
.gate-tabs button:last-child{border-right:none}
|
||||||
|
.gate-tabs button.on{background:var(--acc);color:var(--acc-ink);font-weight:600}
|
||||||
|
#ppcb-gate input{background:var(--panel);border:1px solid var(--line2);border-radius:2px;color:var(--text);padding:14px 15px;font-size:15px;font-family:var(--display);outline:none;margin-bottom:12px}
|
||||||
|
#ppcb-gate input:focus{border-color:var(--acc)}
|
||||||
|
.gate-go{width:100%;justify-content:center;margin-top:4px}
|
||||||
|
.gate-msg{font-family:var(--mono);font-size:13px;margin-top:14px;min-height:18px}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,9 @@ const FN_SUBMIT = 'b02d634b-cd6d-414d-bf9c-80cf6cc2ccdc';
|
||||||
const TK_SUBMIT = '9c3ed2ece766bcc18ed7d33996bfeb86';
|
const TK_SUBMIT = '9c3ed2ece766bcc18ed7d33996bfeb86';
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
$('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
|
|
||||||
|
|
||||||
async function invoke(fn, token, payload) {
|
async function invoke(fn, token, payload) {
|
||||||
const r = await fetch(`${API}/functions/${fn}/invoke?token=${token}`, {
|
const r = await fetch(`${API}/functions/${fn}/invoke?token=${token}`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
method: 'POST', headers: window.ppcbAuth(), body: JSON.stringify(payload) }); // authed: sends the session token
|
||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +55,6 @@ $('submit').onclick = async () => {
|
||||||
try {
|
try {
|
||||||
const d = await invoke(FN_SUBMIT, TK_SUBMIT, { email, part });
|
const d = await invoke(FN_SUBMIT, TK_SUBMIT, { email, part });
|
||||||
if (!d.ok) throw new Error(d.error || 'submit failed');
|
if (!d.ok) throw new Error(d.error || 'submit failed');
|
||||||
localStorage.setItem('ppcb_credits', d.credits); $('cred-n').textContent = d.credits;
|
|
||||||
document.querySelector('.form').innerHTML = `<h1 class="dh1">Thank you — submitted for review ✓</h1>
|
document.querySelector('.form').innerHTML = `<h1 class="dh1">Thank you — submitted for review ✓</h1>
|
||||||
<div class="banner">Your part <b>${part.mpn}</b> is queued for curation review. <b>+5 download credits</b> added — you now have <b>${d.credits}</b>.
|
<div class="banner">Your part <b>${part.mpn}</b> is queued for curation review. <b>+5 download credits</b> added — you now have <b>${d.credits}</b>.
|
||||||
<br><br>Ref: <code>${d.sub_id}</code></div><a class="btn" href="/">Browse the catalog →</a>`;
|
<br><br>Ref: <code>${d.sub_id}</code></div><a class="btn" href="/">Browse the catalog →</a>`;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<script src="/assets/auth.js"></script>
|
||||||
<script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
|
<script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
|
||||||
|
|
||||||
<header class="top"><div class="wrap">
|
<header class="top"><div class="wrap">
|
||||||
|
|
|
||||||
1
mcp.html
1
mcp.html
|
|
@ -4,6 +4,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<script src="/assets/auth.js"></script>
|
||||||
<header class="top"><div class="wrap">
|
<header class="top"><div class="wrap">
|
||||||
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
||||||
<nav class="main"><a href="/">Library</a><a href="/submit.html">Contribute</a><a href="/mcp.html" class="active">MCP / API</a></nav>
|
<nav class="main"><a href="/">Library</a><a href="/submit.html">Contribute</a><a href="/mcp.html" class="active">MCP / API</a></nav>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<script src="/assets/auth.js"></script>
|
||||||
<header class="top"><div class="wrap">
|
<header class="top"><div class="wrap">
|
||||||
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
||||||
<nav class="main"><a href="/">Library</a><a href="/submit.html">Contribute</a><a href="/mcp.html">MCP / API</a></nav>
|
<nav class="main"><a href="/">Library</a><a href="/submit.html">Contribute</a><a href="/mcp.html">MCP / API</a></nav>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<script src="/assets/auth.js"></script>
|
||||||
<header class="top"><div class="wrap">
|
<header class="top"><div class="wrap">
|
||||||
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
<a class="brand" href="/"><span class="mk"></span> PromptPCB <small>/ parts</small></a>
|
||||||
<nav class="main"><a href="/">Library</a><a href="/submit.html" class="active">Contribute</a><a href="/mcp.html">MCP / API</a></nav>
|
<nav class="main"><a href="/">Library</a><a href="/submit.html" class="active">Contribute</a><a href="/mcp.html">MCP / API</a></nav>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue