my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: use double click on indieauth clients

+51 -28
+51 -28
src/client/admin-clients.ts
··· 255 255 Granted ${grantedDate} • Last used ${lastUsedDate} • Scopes: ${user.scopes.join(', ')} 256 256 </div> 257 257 </div> 258 - <button class="revoke-btn" onclick="event.stopPropagation(); revokeUserPermission('${clientId}', '${user.username}')">revoke</button> 258 + <button class="revoke-btn" onclick="event.stopPropagation(); revokeUserPermission('${clientId}', '${user.username}', event)">revoke</button> 259 259 </div> 260 260 `; 261 261 }).join('')} ··· 563 563 } 564 564 }; 565 565 566 - (window as any).revokeUserPermission = async function(clientId: string, username: string) { 567 - if (!confirm(`Are you sure you want to revoke access for ${username}? They will need to authorize this app again.`)) { 568 - return; 569 - } 566 + (window as any).revokeUserPermission = async function(clientId: string, username: string, event?: Event) { 567 + const btn = event?.target as HTMLButtonElement | undefined; 568 + 569 + // Double-click confirmation pattern 570 + if (btn?.dataset.confirmState === 'pending') { 571 + // Second click - execute revoke 572 + delete btn.dataset.confirmState; 573 + btn.disabled = true; 574 + btn.textContent = 'revoking...'; 575 + 576 + try { 577 + const response = await fetch(`/api/admin/apps/${encodeURIComponent(clientId)}/users/${encodeURIComponent(username)}`, { 578 + method: 'DELETE', 579 + headers: { 580 + 'Authorization': `Bearer ${token}`, 581 + }, 582 + }); 583 + 584 + if (!response.ok) { 585 + throw new Error('Failed to revoke permission'); 586 + } 570 587 571 - try { 572 - const response = await fetch(`/api/admin/apps/${encodeURIComponent(clientId)}/users/${encodeURIComponent(username)}`, { 573 - method: 'DELETE', 574 - headers: { 575 - 'Authorization': `Bearer ${token}`, 576 - }, 577 - }); 588 + // Reload the client details 589 + const detailsDiv = document.getElementById(`details-${encodeURIComponent(clientId)}`); 590 + if (detailsDiv) { 591 + detailsDiv.dataset.loaded = 'false'; 592 + } 578 593 579 - if (!response.ok) { 580 - throw new Error('Failed to revoke permission'); 581 - } 594 + const card = document.querySelector(`[data-client-id="${clientId}"]`) as HTMLElement; 595 + if (card) { 596 + card.classList.remove('expanded'); 597 + } 582 598 583 - // Reload the client details 584 - const detailsDiv = document.getElementById(`details-${encodeURIComponent(clientId)}`); 585 - if (detailsDiv) { 586 - detailsDiv.dataset.loaded = 'false'; 599 + await loadClients(); 600 + } catch (error) { 601 + console.error('Failed to revoke permission:', error); 602 + showToast('Failed to revoke permission. Please try again.', 'error'); 603 + btn.disabled = false; 604 + btn.textContent = 'revoke'; 587 605 } 588 - 589 - const card = document.querySelector(`[data-client-id="${clientId}"]`) as HTMLElement; 590 - if (card) { 591 - card.classList.remove('expanded'); 606 + } else { 607 + // First click - set pending state 608 + if (btn) { 609 + const originalText = btn.textContent; 610 + btn.dataset.confirmState = 'pending'; 611 + btn.textContent = 'you sure?'; 612 + 613 + // Reset after 3 seconds if not confirmed 614 + setTimeout(() => { 615 + if (btn.dataset.confirmState === 'pending') { 616 + delete btn.dataset.confirmState; 617 + btn.textContent = originalText; 618 + } 619 + }, 3000); 592 620 } 593 - 594 - await loadClients(); 595 - } catch (error) { 596 - console.error('Failed to revoke permission:', error); 597 - showToast('Failed to revoke permission. Please try again.', 'error'); 598 621 } 599 622 }; 600 623