Compare commits
No commits in common. "a1982fd760c5c3215caac3cd30336bc0c089f284" and "3c8d60e61471b51f6bb4d17a251162cf04423b1e" have entirely different histories.
a1982fd760
...
3c8d60e614
2 changed files with 6 additions and 66 deletions
|
|
@ -74,11 +74,7 @@ Body `{ code }`. Sets `status=denied` and scrubs the pending secrets. → `{ ok
|
||||||
- Revoke: `DELETE /api/collections/device_auth/records/{id}`
|
- Revoke: `DELETE /api/collections/device_auth/records/{id}`
|
||||||
|
|
||||||
### `GET /device?code=CODE` (HTML)
|
### `GET /device?code=CODE` (HTML)
|
||||||
The self-contained approve page (`device_page.pb.js`). No external assets. The
|
The self-contained approve page (`device_page.pb.js`). No external assets.
|
||||||
`?code=` (QR / direct link) is optional: opened bare, the page shows a manual
|
|
||||||
code-entry field so the user can type the `XXXX-XXXX` code shown on the TV. The
|
|
||||||
page normalizes (uppercase, strips the dash) before the `/info` + `/approve`
|
|
||||||
lookups, since the code is stored dash-less.
|
|
||||||
|
|
||||||
### Cleanup cron (`*/5 * * * *`)
|
### Cleanup cron (`*/5 * * * *`)
|
||||||
Scrubs `authToken`/`keyCiphertext`/`pollSecret`/`devicePubKey` from approved rows
|
Scrubs `authToken`/`keyCiphertext`/`pollSecret`/`devicePubKey` from approved rows
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,6 @@ routerAdd("GET", "/device", (e) => {
|
||||||
@keyframes s { to { transform:rotate(360deg); } }
|
@keyframes s { to { transform:rotate(360deg); } }
|
||||||
.hidden { display:none; }
|
.hidden { display:none; }
|
||||||
.note { margin-top:16px; font-size:12px; color:var(--muted); line-height:1.5; }
|
.note { margin-top:16px; font-size:12px; color:var(--muted); line-height:1.5; }
|
||||||
.code-input { text-transform:uppercase; letter-spacing:.22em; text-align:center;
|
|
||||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:20px;
|
|
||||||
font-weight:600; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -80,14 +77,6 @@ routerAdd("GET", "/device", (e) => {
|
||||||
|
|
||||||
<div id="devbox" class="dev hidden"></div>
|
<div id="devbox" class="dev hidden"></div>
|
||||||
|
|
||||||
<div id="codeform" class="hidden">
|
|
||||||
<label for="codeInput">Kód ze zařízení</label>
|
|
||||||
<input id="codeInput" class="code-input" type="text" inputmode="text"
|
|
||||||
autocomplete="off" autocapitalize="characters" spellcheck="false"
|
|
||||||
maxlength="9" placeholder="XXXX-XXXX" />
|
|
||||||
<button id="codeGo" class="primary">Pokračovat</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="form">
|
<div id="form">
|
||||||
<label for="email">E-mail</label>
|
<label for="email">E-mail</label>
|
||||||
<input id="email" type="email" autocomplete="username" inputmode="email" />
|
<input id="email" type="email" autocomplete="username" inputmode="email" />
|
||||||
|
|
@ -150,10 +139,6 @@ function b64d(s){var bin=atob(s),u8=new Uint8Array(bin.length);for(var i=0;i<bin
|
||||||
function concat(){var n=0,i;for(i=0;i<arguments.length;i++)n+=arguments[i].length;
|
function concat(){var n=0,i;for(i=0;i<arguments.length;i++)n+=arguments[i].length;
|
||||||
var out=new Uint8Array(n),o=0;for(i=0;i<arguments.length;i++){out.set(arguments[i],o);o+=arguments[i].length;}return out;}
|
var out=new Uint8Array(n),o=0;for(i=0;i<arguments.length;i++){out.set(arguments[i],o);o+=arguments[i].length;}return out;}
|
||||||
function qs(name){return new URLSearchParams(location.search).get(name)||"";}
|
function qs(name){return new URLSearchParams(location.search).get(name)||"";}
|
||||||
// The human code is 8 chars shown as XXXX-XXXX but stored dash-less; normalize
|
|
||||||
// (uppercase, drop the dash/spaces, cap at 8) before any /info + /approve lookup.
|
|
||||||
function normCode(s){return String(s||"").toUpperCase().replace(/[^0-9A-Z]/g,"").slice(0,8);}
|
|
||||||
function fmtCode(s){var c=normCode(s);return c.length>4?c.slice(0,4)+"-"+c.slice(4):c;}
|
|
||||||
|
|
||||||
async function deriveVaultKeyB64(password, saltB64){
|
async function deriveVaultKeyB64(password, saltB64){
|
||||||
var salt=b64d(saltB64);
|
var salt=b64d(saltB64);
|
||||||
|
|
@ -178,7 +163,7 @@ async function sealTo(tvPubB64, plaintext){
|
||||||
function newSaltB64(){ return b64e(crypto.getRandomValues(new Uint8Array(16))); }
|
function newSaltB64(){ return b64e(crypto.getRandomValues(new Uint8Array(16))); }
|
||||||
|
|
||||||
// ── page state ───────────────────────────────────────────────────────────────
|
// ── page state ───────────────────────────────────────────────────────────────
|
||||||
var CODE = normCode(qs("code"));
|
var CODE = qs("code").toUpperCase().trim();
|
||||||
var info = null;
|
var info = null;
|
||||||
var msgEl = document.getElementById("msg");
|
var msgEl = document.getElementById("msg");
|
||||||
var approveBtn = document.getElementById("approve");
|
var approveBtn = document.getElementById("approve");
|
||||||
|
|
@ -199,21 +184,12 @@ async function api(method, path, body, token){
|
||||||
return { ok:r.ok, status:r.status, data:data };
|
return { ok:r.ok, status:r.status, data:data };
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCodeEntry(prefill){
|
|
||||||
document.getElementById("form").classList.add("hidden");
|
|
||||||
document.getElementById("devbox").classList.add("hidden");
|
|
||||||
document.getElementById("codeform").classList.remove("hidden");
|
|
||||||
var input = document.getElementById("codeInput");
|
|
||||||
if (prefill) input.value = fmtCode(prefill);
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInfo(){
|
async function loadInfo(){
|
||||||
if (!CODE){ showCodeEntry(""); setMsg("Zadejte kód zobrazený na televizi.", ""); return; }
|
if (!CODE){ setMsg("Chybí kód zařízení v odkazu.", "err"); document.getElementById("form").classList.add("hidden"); return; }
|
||||||
var r = await api("GET", "/api/device-auth/info?code=" + encodeURIComponent(CODE));
|
var r = await api("GET", "/api/device-auth/info?code=" + encodeURIComponent(CODE));
|
||||||
if (!r.ok){
|
if (!r.ok){
|
||||||
showCodeEntry(CODE);
|
setMsg("Požadavek nebyl nalezen nebo vypršel. Vytvořte na televizi nový.", "err");
|
||||||
setMsg("Kód nebyl nalezen nebo vypršel. Zkontrolujte ho, nebo vytvořte na televizi nový.", "err");
|
document.getElementById("form").classList.add("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info = r.data;
|
info = r.data;
|
||||||
|
|
@ -222,14 +198,8 @@ async function loadInfo(){
|
||||||
box.innerHTML = "Přihlásit zařízení: <b>" + (info.deviceName ? escapeHtml(info.deviceName) : "nové zařízení") + "</b>";
|
box.innerHTML = "Přihlásit zařízení: <b>" + (info.deviceName ? escapeHtml(info.deviceName) : "nové zařízení") + "</b>";
|
||||||
if (info.status && info.status !== "pending"){
|
if (info.status && info.status !== "pending"){
|
||||||
setMsg("Tento požadavek už byl vyřízen.", "err");
|
setMsg("Tento požadavek už byl vyřízen.", "err");
|
||||||
document.getElementById("codeform").classList.add("hidden");
|
|
||||||
document.getElementById("form").classList.add("hidden");
|
document.getElementById("form").classList.add("hidden");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// code accepted → reveal the sign-in form
|
|
||||||
document.getElementById("codeform").classList.add("hidden");
|
|
||||||
document.getElementById("form").classList.remove("hidden");
|
|
||||||
setMsg("");
|
|
||||||
}
|
}
|
||||||
function escapeHtml(s){ return String(s).replace(/[&<>"']/g,function(c){return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c];}); }
|
function escapeHtml(s){ return String(s).replace(/[&<>"']/g,function(c){return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c];}); }
|
||||||
|
|
||||||
|
|
@ -303,33 +273,7 @@ async function doDecline(){
|
||||||
|
|
||||||
approveBtn.addEventListener("click", doApprove);
|
approveBtn.addEventListener("click", doApprove);
|
||||||
declineBtn.addEventListener("click", doDecline);
|
declineBtn.addEventListener("click", doDecline);
|
||||||
|
|
||||||
// Manual code entry: when the page is opened bare (no ?code= — e.g. the TV tells
|
|
||||||
// the user to open …/device and type the XXXX-XXXX code). The QR/direct-link path
|
|
||||||
// still fills CODE from the URL and skips straight to sign-in.
|
|
||||||
var codeInput = document.getElementById("codeInput");
|
|
||||||
var codeGo = document.getElementById("codeGo");
|
|
||||||
|
|
||||||
function submitCode(){
|
|
||||||
CODE = normCode(codeInput.value);
|
|
||||||
if (CODE.length !== 8){ setMsg("Zadejte celý kód z televize (např. 25K4-CFQP).", "err"); return; }
|
|
||||||
setMsg("");
|
|
||||||
loadInfo();
|
loadInfo();
|
||||||
}
|
|
||||||
|
|
||||||
codeInput.addEventListener("input", function(){
|
|
||||||
var before = codeInput.value.length;
|
|
||||||
codeInput.value = fmtCode(codeInput.value);
|
|
||||||
// when the auto-dash shifts length, park the caret at the end (8-char field)
|
|
||||||
if (codeInput.value.length !== before){
|
|
||||||
codeInput.setSelectionRange(codeInput.value.length, codeInput.value.length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
codeInput.addEventListener("keydown", function(ev){ if (ev.key === "Enter"){ ev.preventDefault(); submitCode(); } });
|
|
||||||
codeGo.addEventListener("click", submitCode);
|
|
||||||
|
|
||||||
// Boot: URL code → sign-in; otherwise show the manual code-entry field.
|
|
||||||
if (CODE){ loadInfo(); } else { showCodeEntry(""); }
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue