Un jeton par appareil, révocable seul (2.2.0)

Tous les appareils partageaient le jeton du serveur : perdre un téléphone
obligeait à changer ce jeton, donc à réappairer tout le monde. Chaque
appairage émet désormais le sien.

Révoquer devient une opération locale, vérifiée en appairant deux appareils
puis en coupant le premier : 401 pour lui, 200 pour l'autre, jeton maître
intact. L'interface du serveur les liste avec leur modèle, leur date
d'appairage et leur dernière synchronisation — sans jamais afficher les jetons
eux-mêmes, ce qui reviendrait à les distribuer.

La table s'ajoute sans toucher aux données existantes et le jeton maître reste
accepté : rien ne casse pour un appareil appairé avant cette version.
This commit is contained in:
tuxgyver committed 2026-09-13 21:23:19 +02:00
1 parent 51b60cda70
commit 28cb545685
7 files changed
+130 -8

No files matched your search

+26 -1
View File
@@ -65,6 +65,7 @@
<button onclick="show('watch')" aria-selected="false" id="t-watch">📡 Guets</button>
<button onclick="show('queue')" aria-selected="false" id="t-queue">⏳ File</button>
<button onclick="show('library')" aria-selected="false" id="t-library">📚 Bibliothèque</button>
<button onclick="show('devices')" aria-selected="false" id="t-devices">📱 Appareils</button>
</div>
<section id="p-search">
@@ -88,6 +89,10 @@
<section id="p-queue" hidden><div id="queue"></div></section>
<section id="p-library" hidden><div id="library"></div></section>
<section id="p-devices" hidden>
<div class="row"><button class="go" onclick="openPairing()">Appairer un appareil</button></div>
<div id="devices"></div>
</section>
</main>
<script>
@@ -120,13 +125,14 @@ async function api(path, options = {}) {
}
function show(name) {
for (const tab of ['search', 'watch', 'queue', 'library']) {
for (const tab of ['search', 'watch', 'queue', 'library', 'devices']) {
$('p-' + tab).hidden = tab !== name;
$('t-' + tab).setAttribute('aria-selected', String(tab === name));
}
if (name === 'queue') loadQueue();
if (name === 'library') loadLibrary();
if (name === 'watch') loadWatches();
if (name === 'devices') loadDevices();
}
async function refreshAll() {
@@ -257,6 +263,25 @@ async function sweep() {
loadWatches();
}
async function loadDevices() {
try {
const { devices } = await api('/devices');
$('devices').innerHTML = devices.length ? devices.map((d) => `
<div class="card"><div class="grow"><b>${esc(d.name)}</b><small>appairé le ${
new Date(d.created_at).toLocaleString('fr-FR')}${
d.last_seen ? ' · vu le ' + new Date(d.last_seen).toLocaleString('fr-FR')
: ' · jamais synchronisé'}</small></div>
<button class="ghost" onclick="revokeDevice('${d.id}','${esc(d.name)}')">Révoquer</button></div>`).join('')
: '<div class="empty">Aucun appareil appairé</div>';
} catch (e) { $('devices').innerHTML = `<div class="empty">${esc(String(e))}</div>`; }
}
async function revokeDevice(id, name) {
if (!confirm(`Révoquer « ${name} » ? Il ne pourra plus se synchroniser tant qu'il n'est pas réappairé.`)) return;
await api('/devices/' + id, { method: 'DELETE' });
loadDevices();
}
async function openPairing() {
try {
const { seconds } = await api('/pair/open', { method: 'POST' });