my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 138 lines 3.5 kB view raw
1const token = localStorage.getItem("indiko_session"); 2const appsList = document.getElementById("appsList") as HTMLElement; 3 4if (!token) { 5 window.location.href = "/login"; 6} 7 8interface App { 9 clientId: string; 10 name: string; 11 scopes: string[]; 12 grantedAt: number; 13 lastUsed: number; 14} 15 16async function loadApps() { 17 try { 18 const response = await fetch("/api/apps", { 19 headers: { 20 Authorization: `Bearer ${token}`, 21 }, 22 }); 23 24 if (response.status === 401 || response.status === 403) { 25 localStorage.removeItem("indiko_session"); 26 window.location.href = "/login"; 27 return; 28 } 29 30 if (!response.ok) { 31 throw new Error("Failed to load apps"); 32 } 33 34 const data = await response.json(); 35 displayApps(data.apps); 36 } catch (error) { 37 console.error("Failed to load apps:", error); 38 appsList.innerHTML = 39 '<div class="error">Failed to load authorized apps</div>'; 40 } 41} 42 43function displayApps(apps: App[]) { 44 if (apps.length === 0) { 45 appsList.innerHTML = 46 '<div class="empty">No authorized apps yet. Apps will appear here after you grant them access.</div>'; 47 return; 48 } 49 50 appsList.innerHTML = apps 51 .map((app) => { 52 const lastUsedDate = new Date(app.lastUsed * 1000).toLocaleDateString(); 53 const grantedDate = new Date(app.grantedAt * 1000).toLocaleDateString(); 54 55 return ` 56 <div class="app-card" data-client-id="${app.clientId}"> 57 <div class="app-header"> 58 <div> 59 <div class="app-name">${app.name}</div> 60 <div class="app-meta">Granted ${grantedDate} • Last used ${lastUsedDate}</div> 61 </div> 62 <button class="revoke-btn" onclick="revokeApp('${app.clientId}', event)">revoke</button> 63 </div> 64 <div class="scopes"> 65 <div class="scope-title">permissions</div> 66 <div class="scope-list"> 67 ${app.scopes.map((scope) => `<span class="scope-badge">${scope}</span>`).join("")} 68 </div> 69 </div> 70 </div> 71 `; 72 }) 73 .join(""); 74} 75 76(window as any).revokeApp = async (clientId: string, event?: Event) => { 77 const btn = event?.target as HTMLButtonElement | undefined; 78 79 // Double-click confirmation pattern 80 if (btn?.dataset.confirmState === "pending") { 81 // Second click - execute revoke 82 delete btn.dataset.confirmState; 83 btn.disabled = true; 84 btn.textContent = "revoking..."; 85 86 const card = document.querySelector(`[data-client-id="${clientId}"]`); 87 88 try { 89 const response = await fetch( 90 `/api/apps/${encodeURIComponent(clientId)}`, 91 { 92 method: "DELETE", 93 headers: { 94 Authorization: `Bearer ${token}`, 95 }, 96 }, 97 ); 98 99 if (!response.ok) { 100 throw new Error("Failed to revoke app"); 101 } 102 103 // Remove from UI 104 card?.remove(); 105 106 // Check if list is now empty 107 const remaining = document.querySelectorAll(".app-card"); 108 if (remaining.length === 0) { 109 appsList.innerHTML = 110 '<div class="empty">No authorized apps yet. Apps will appear here after you grant them access.</div>'; 111 } 112 } catch (error) { 113 console.error("Failed to revoke app:", error); 114 alert("Failed to revoke app access. Please try again."); 115 if (btn) { 116 btn.disabled = false; 117 btn.textContent = "revoke"; 118 } 119 } 120 } else { 121 // First click - set pending state 122 if (btn) { 123 const originalText = btn.textContent; 124 btn.dataset.confirmState = "pending"; 125 btn.textContent = "you sure?"; 126 127 // Reset after 3 seconds if not confirmed 128 setTimeout(() => { 129 if (btn.dataset.confirmState === "pending") { 130 delete btn.dataset.confirmState; 131 btn.textContent = originalText; 132 } 133 }, 3000); 134 } 135 } 136}; 137 138loadApps();