my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 213 lines 6.0 kB view raw
1const token = localStorage.getItem("indiko_session"); 2const footer = document.getElementById("footer") as HTMLElement; 3const usersList = document.getElementById("usersList") as HTMLElement; 4let currentUserId: number; 5 6// Check auth and display user 7async function checkAuth() { 8 if (!token) { 9 window.location.href = "/login"; 10 return; 11 } 12 13 try { 14 const response = await fetch("/api/hello", { 15 headers: { 16 Authorization: `Bearer ${token}`, 17 }, 18 }); 19 20 if (response.status === 401 || response.status === 403) { 21 localStorage.removeItem("indiko_session"); 22 window.location.href = "/login"; 23 return; 24 } 25 26 const data = await response.json(); 27 currentUserId = data.id; 28 29 footer.innerHTML = `admin • signed in as <strong><a href="/u/${data.username}">${data.username}</a></strong> • <a href="/login" id="logoutLink">sign out</a> 30 <div class="back-link"><a href="/">← back to dashboard</a></div>`; 31 32 // Handle logout 33 document 34 .getElementById("logoutLink") 35 ?.addEventListener("click", async (e) => { 36 e.preventDefault(); 37 try { 38 await fetch("/auth/logout", { 39 method: "POST", 40 headers: { 41 Authorization: `Bearer ${token}`, 42 }, 43 }); 44 } catch { 45 // Ignore logout errors 46 } 47 localStorage.removeItem("indiko_session"); 48 window.location.href = "/login"; 49 }); 50 51 // Check if admin 52 if (!data.isAdmin) { 53 window.location.href = "/"; 54 return; 55 } 56 57 // Load users if admin 58 loadUsers(); 59 } catch (error) { 60 console.error("Auth check failed:", error); 61 footer.textContent = "error loading user info"; 62 usersList.innerHTML = '<div class="error">Failed to load users</div>'; 63 } 64} 65 66async function loadUsers() { 67 try { 68 const response = await fetch("/api/users", { 69 headers: { 70 Authorization: `Bearer ${token}`, 71 }, 72 }); 73 74 if (!response.ok) { 75 throw new Error("Failed to load users"); 76 } 77 78 const data = await response.json(); 79 80 if (data.users.length === 0) { 81 usersList.innerHTML = '<div class="loading">No users found</div>'; 82 return; 83 } 84 85 usersList.innerHTML = data.users 86 .map( 87 (user: { 88 id: number; 89 username: string; 90 name: string; 91 email: string | null; 92 photo: string | null; 93 status: string; 94 role: string; 95 isAdmin: boolean; 96 createdAt: number; 97 credentialCount: number; 98 }) => { 99 const createdDate = new Date( 100 user.createdAt * 1000, 101 ).toLocaleDateString(); 102 const initials = user.username.substring(0, 2).toUpperCase(); 103 const avatarContent = user.photo 104 ? `<img src="${user.photo}" alt="${user.username}" />` 105 : initials; 106 const isSelf = user.id === currentUserId; 107 108 return ` 109 <div class="user-card ${user.status === "suspended" ? "user-suspended" : ""}" data-user-id="${user.id}"> 110 <div class="user-avatar">${avatarContent}</div> 111 <div class="user-info"> 112 <div class="user-name">${user.username}${isSelf ? " (you)" : ""}</div> 113 <div class="user-meta"> 114 <span class="user-meta-item">${user.credentialCount} passkey${user.credentialCount !== 1 ? "s" : ""}</span> 115 <span class="user-meta-item">joined ${createdDate}</span> 116 ${user.email ? `<span class="user-meta-item">${user.email}</span>` : ""} 117 </div> 118 </div> 119 <div class="user-badges"> 120 <span class="user-badge badge-status ${user.status}">${user.status}</span> 121 <span class="user-badge badge-role">${user.role}</span> 122 </div> 123 <div class="user-actions"> 124 ${ 125 !isSelf 126 ? user.status === "suspended" 127 ? `<button class="btn-edit" data-action="enable" data-user-id="${user.id}">enable</button>` 128 : `<button class="btn-disable" data-action="disable" data-user-id="${user.id}">disable</button>` 129 : "" 130 } 131 ${!isSelf ? `<button class="btn-delete" data-action="delete" data-user-id="${user.id}">delete</button>` : ""} 132 </div> 133 </div> 134 `; 135 }, 136 ) 137 .join(""); 138 139 // Add event listeners for action buttons 140 document.querySelectorAll("button[data-action]").forEach((btn) => { 141 btn.addEventListener("click", handleUserAction); 142 }); 143 } catch (error) { 144 console.error("Failed to load users:", error); 145 usersList.innerHTML = '<div class="error">Failed to load users</div>'; 146 } 147} 148 149async function handleUserAction(e: Event) { 150 const btn = e.target as HTMLButtonElement; 151 const action = btn.dataset.action; 152 const userId = btn.dataset.userId; 153 154 if (!userId || !action) return; 155 156 // Check if already in confirmation state 157 if (btn.dataset.confirmState === "pending") { 158 // Second click - perform action 159 btn.dataset.confirmState = ""; 160 btn.disabled = true; 161 162 try { 163 let endpoint = ""; 164 let method = "POST"; 165 166 if (action === "delete") { 167 endpoint = `/api/admin/users/${userId}/delete`; 168 method = "DELETE"; 169 } else if (action === "disable") { 170 endpoint = `/api/admin/users/${userId}/disable`; 171 } else if (action === "enable") { 172 endpoint = `/api/admin/users/${userId}/enable`; 173 } 174 175 const response = await fetch(endpoint, { 176 method, 177 headers: { 178 Authorization: `Bearer ${token}`, 179 }, 180 }); 181 182 if (!response.ok) { 183 const error = await response.json(); 184 throw new Error(error.error || "Failed to perform action"); 185 } 186 187 // Reload users list 188 loadUsers(); 189 } catch (error) { 190 console.error(`Failed to ${action} user:`, error); 191 alert( 192 `Failed to ${action} user: ${error instanceof Error ? error.message : "Unknown error"}`, 193 ); 194 btn.disabled = false; 195 } 196 } else { 197 // First click - set confirmation state 198 const originalText = btn.textContent; 199 btn.dataset.confirmState = "pending"; 200 btn.dataset.originalText = originalText || ""; 201 btn.textContent = "you sure?"; 202 203 // Reset after 3 seconds if not clicked again 204 setTimeout(() => { 205 if (btn.dataset.confirmState === "pending") { 206 btn.dataset.confirmState = ""; 207 btn.textContent = btn.dataset.originalText || originalText; 208 } 209 }, 3000); 210 } 211} 212 213checkAuth();