|
28 | 28 | } |
29 | 29 |
|
30 | 30 | const normalizeUrl = urls.normalizeUrl; |
| 31 | + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
31 | 32 |
|
32 | 33 | async function load() { |
33 | 34 | const s = await storage.getSettings(); |
34 | 35 | $('baseUrl').value = s.baseUrl; |
35 | | - $('token').value = ''; |
36 | | - $('token').placeholder = s.token ? '(set - leave blank to keep)' : '(not set)'; |
37 | 36 | $('wait').value = s.waitSeconds; |
38 | 37 | $('scanCap').value = s.scanCap; |
39 | 38 | $('minSize').value = s.minImageSize; |
|
48 | 47 | minImageSize: clampInt($('minSize').value, 0, 4000, 64), |
49 | 48 | previews: $('previews').checked |
50 | 49 | }; |
51 | | - const token = $('token').value; |
52 | | - if (token) patch.token = token; // keep existing unless a new one is typed |
53 | 50 | return patch; |
54 | 51 | } |
55 | 52 |
|
56 | 53 | async function cfgFromForm() { |
57 | 54 | const s = await storage.getSettings(); |
58 | 55 | return { |
59 | 56 | baseUrl: normalizeUrl($('baseUrl').value) || s.baseUrl, |
60 | | - token: $('token').value || s.token |
| 57 | + token: s.token |
61 | 58 | }; |
62 | 59 | } |
63 | 60 |
|
|
77 | 74 |
|
78 | 75 | const patch = readForm(); |
79 | 76 | patch.baseUrl = url; |
| 77 | + // A token is bound to the instance that issued it; a changed URL drops the |
| 78 | + // stored token so the old key is never sent to a different monloader. |
| 79 | + const urlChanged = !!(lastSavedUrl && lastSavedUrl !== url); |
| 80 | + if (urlChanged) { |
| 81 | + const cur = await storage.getSettings(); |
| 82 | + if (cur.token) patch.token = ''; |
| 83 | + } |
80 | 84 | const saved = await storage.setSettings(patch); |
81 | 85 | lastSavedUrl = saved.baseUrl; |
82 | | - $('token').value = ''; |
83 | | - $('token').placeholder = saved.token ? '(set - leave blank to keep)' : '(not set)'; |
84 | | - if (granted) setFlash('saved', 'flash-ok'); |
| 86 | + if (granted) setFlash(urlChanged && !saved.token ? 'saved; URL changed, cleared the stored token' : 'saved', 'flash-ok'); |
85 | 87 | // Always refresh the dot: when the grant was denied, test() short-circuits |
86 | 88 | // to the "allow host access" state instead of leaving a stale indicator. |
87 | 89 | test(); |
88 | 90 | } |
89 | 91 |
|
| 92 | + // connect pairs with monloader without copying keys: it requests a pairing, |
| 93 | + // the operator approves in monloader, and the issued token is stored here. |
| 94 | + async function connect() { |
| 95 | + const url = normalizeUrl($('baseUrl').value); |
| 96 | + $('baseUrl').value = url; |
| 97 | + if (!url) { setFlash('set a monloader URL first', 'flash-err'); return; } |
| 98 | + let granted = false; |
| 99 | + try { |
| 100 | + const oldUrl = lastSavedUrl && lastSavedUrl !== url ? lastSavedUrl : null; |
| 101 | + granted = await perms.syncOrigin(url, oldUrl); |
| 102 | + } catch (e) { granted = false; } |
| 103 | + if (!granted) { setFlash('host access for ' + url + ' was not granted', 'flash-warn'); return; } |
| 104 | + |
| 105 | + setFlash('requesting pairing...', 'flash-warn'); |
| 106 | + const cfg = { baseUrl: url }; |
| 107 | + const r = await api.pair(cfg); |
| 108 | + if (!r.ok || !(r.data && r.data.request_id)) { |
| 109 | + setFlash((r.data && r.data.error) || (r.networkError ? 'monloader unreachable' : 'pairing request failed'), 'flash-err'); |
| 110 | + return; |
| 111 | + } |
| 112 | + setFlash('approve the request in monloader (settings → authentication), waiting...', 'flash-warn'); |
| 113 | + const id = r.data.request_id; |
| 114 | + for (let i = 0; i < 60; i++) { |
| 115 | + await sleep(1500); |
| 116 | + const st = await api.pairStatus(cfg, id); |
| 117 | + if (!st.ok || !st.data) continue; |
| 118 | + if (st.data.status === 'approved' && st.data.token) { |
| 119 | + const saved = await storage.setSettings({ baseUrl: url, token: st.data.token }); |
| 120 | + lastSavedUrl = saved.baseUrl; |
| 121 | + setFlash('paired with monloader', 'flash-ok'); |
| 122 | + test(); |
| 123 | + return; |
| 124 | + } |
| 125 | + if (st.data.status === 'denied') { setFlash('pairing was denied in monloader', 'flash-err'); return; } |
| 126 | + if (st.data.status === 'expired') { setFlash('pairing request expired; try again', 'flash-err'); return; } |
| 127 | + } |
| 128 | + setFlash('timed out waiting for approval; try again', 'flash-err'); |
| 129 | + } |
| 130 | + |
90 | 131 | async function test() { |
91 | 132 | const cfg = await cfgFromForm(); |
92 | 133 | if (!cfg.baseUrl) { setConn('down', 'no URL'); return; } |
|
111 | 152 |
|
112 | 153 | $('save').addEventListener('click', save); |
113 | 154 | $('test').addEventListener('click', test); |
| 155 | + $('connect').addEventListener('click', connect); |
114 | 156 |
|
115 | 157 | // Scripts are at the end of <body>, so the DOM above exists. Init now. |
116 | 158 | load().then(test); |
|
0 commit comments