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

feat: display client ID and secret in modal with separate copy buttons

- Remove Client ID input field from create form
- Show both credentials in modal after creation
- Add separate copy buttons for client ID and secret
- Remove clientId field references in edit mode

dunkirk.sh 08616fcf 25b6a876

verified
+40 -18
+25 -9
src/client/admin-clients.ts
··· 310 310 311 311 modalTitle.textContent = 'Edit OAuth Client'; 312 312 (document.getElementById('editClientId') as HTMLInputElement).value = clientId; 313 - (document.getElementById('clientId') as HTMLInputElement).value = client.clientId; 314 - (document.getElementById('clientId') as HTMLInputElement).disabled = true; 315 313 (document.getElementById('clientName') as HTMLInputElement).value = client.name || ''; 316 314 (document.getElementById('logoUrl') as HTMLInputElement).value = client.logoUrl || ''; 317 315 (document.getElementById('description') as HTMLTextAreaElement).value = client.description || ''; ··· 383 381 modalTitle.textContent = 'Create OAuth Client'; 384 382 clientForm.reset(); 385 383 (document.getElementById('editClientId') as HTMLInputElement).value = ''; 386 - (document.getElementById('clientId') as HTMLInputElement).disabled = false; 387 384 redirectUrisList.innerHTML = ` 388 385 <div class="redirect-uri-item"> 389 386 <input type="url" class="form-input redirect-uri-input" placeholder="https://example.com/auth/callback" required /> ··· 424 421 e.preventDefault(); 425 422 426 423 const editClientId = (document.getElementById('editClientId') as HTMLInputElement).value; 427 - const clientId = (document.getElementById('clientId') as HTMLInputElement).value; 428 424 const name = (document.getElementById('clientName') as HTMLInputElement).value; 429 425 const logoUrl = (document.getElementById('logoUrl') as HTMLInputElement).value; 430 426 const description = (document.getElementById('description') as HTMLTextAreaElement).value; ··· 465 461 'Content-Type': 'application/json', 466 462 }, 467 463 body: JSON.stringify({ 468 - clientId: isEdit ? undefined : clientId, 469 464 name, 470 465 logoUrl, 471 466 description, ··· 482 477 483 478 clientModal.classList.remove('active'); 484 479 485 - // If creating a new client, show the secret in modal 480 + // If creating a new client, show the credentials in modal 486 481 if (!isEdit) { 487 482 const result = await response.json(); 488 - if (result.client && result.client.clientSecret) { 483 + if (result.client && result.client.clientId && result.client.clientSecret) { 489 484 const secretModal = document.getElementById('secretModal') as HTMLElement; 485 + const generatedClientId = document.getElementById('generatedClientId') as HTMLElement; 490 486 const generatedSecret = document.getElementById('generatedSecret') as HTMLElement; 491 487 492 - if (generatedSecret && secretModal) { 488 + if (generatedClientId && generatedSecret && secretModal) { 489 + generatedClientId.textContent = result.client.clientId; 493 490 generatedSecret.textContent = result.client.clientSecret; 494 491 secretModal.classList.add('active'); 495 492 } ··· 576 573 // Secret modal handlers 577 574 const secretModal = document.getElementById('secretModal') as HTMLElement; 578 575 const secretModalClose = document.getElementById('secretModalClose') as HTMLButtonElement; 576 + const copyClientIdBtn = document.getElementById('copyClientIdBtn') as HTMLButtonElement; 579 577 const copySecretBtn = document.getElementById('copySecretBtn') as HTMLButtonElement; 580 578 581 579 secretModalClose?.addEventListener('click', () => { 582 580 secretModal?.classList.remove('active'); 583 581 }); 584 582 583 + copyClientIdBtn?.addEventListener('click', async () => { 584 + const generatedClientId = document.getElementById('generatedClientId') as HTMLElement; 585 + if (generatedClientId) { 586 + try { 587 + await navigator.clipboard.writeText(generatedClientId.textContent || ''); 588 + const originalText = copyClientIdBtn.textContent; 589 + copyClientIdBtn.textContent = 'copied! ✓'; 590 + setTimeout(() => { 591 + copyClientIdBtn.textContent = originalText; 592 + }, 2000); 593 + } catch (error) { 594 + console.error('Failed to copy:', error); 595 + showToast('Failed to copy to clipboard', 'error'); 596 + } 597 + } 598 + }); 599 + 585 600 copySecretBtn?.addEventListener('click', async () => { 586 601 const generatedSecret = document.getElementById('generatedSecret') as HTMLElement; 587 602 if (generatedSecret) { 588 603 try { 589 604 await navigator.clipboard.writeText(generatedSecret.textContent || ''); 605 + const originalText = copySecretBtn.textContent; 590 606 copySecretBtn.textContent = 'copied! ✓'; 591 607 setTimeout(() => { 592 - copySecretBtn.textContent = 'copy to clipboard'; 608 + copySecretBtn.textContent = originalText; 593 609 }, 2000); 594 610 } catch (error) { 595 611 console.error('Failed to copy:', error);
+15 -9
src/html/admin-clients.html
··· 636 636 <form id="clientForm"> 637 637 <input type="hidden" id="editClientId" /> 638 638 <div class="form-group"> 639 - <label class="form-label" for="clientId">Client ID (URL)</label> 640 - <input type="url" class="form-input" id="clientId" required placeholder="https://example.com" /> 641 - </div> 642 - <div class="form-group"> 643 639 <label class="form-label" for="clientName">Name</label> 644 640 <input type="text" class="form-input" id="clientName" placeholder="My Application" /> 645 641 </div> ··· 682 678 <div id="secretModal" class="modal"> 683 679 <div class="modal-content"> 684 680 <div class="modal-header"> 685 - <h3 class="modal-title">Client Secret Generated</h3> 681 + <h3 class="modal-title">Client Credentials Generated</h3> 686 682 <button class="modal-close" id="secretModalClose">&times;</button> 687 683 </div> 688 684 <div style="margin-bottom: 1.5rem;"> 689 685 <p style="color: var(--rosewood); font-weight: 600; margin-bottom: 1rem;"> 690 - ⚠️ Save this secret now. You won't be able to see it again! 686 + ⚠️ Save these credentials now. You won't be able to see the secret again! 691 687 </p> 692 - <div style="background: rgba(0, 0, 0, 0.3); padding: 1rem; border: 1px solid var(--old-rose); margin-bottom: 1rem;"> 693 - <code id="generatedSecret" style="color: var(--lavender); font-size: 0.875rem; word-break: break-all; display: block;"></code> 688 + <div style="margin-bottom: 1rem;"> 689 + <label style="display: block; color: var(--old-rose); font-size: 0.875rem; margin-bottom: 0.5rem;">Client ID</label> 690 + <div style="background: rgba(0, 0, 0, 0.3); padding: 1rem; border: 1px solid var(--old-rose); margin-bottom: 0.5rem;"> 691 + <code id="generatedClientId" style="color: var(--lavender); font-size: 0.875rem; word-break: break-all; display: block;"></code> 692 + </div> 693 + <button class="btn" id="copyClientIdBtn" style="width: 100%; background: rgba(188, 141, 160, 0.2);">copy client id</button> 694 694 </div> 695 - <button class="btn" id="copySecretBtn" style="width: 100%;">copy to clipboard</button> 695 + <div style="margin-bottom: 1rem;"> 696 + <label style="display: block; color: var(--old-rose); font-size: 0.875rem; margin-bottom: 0.5rem;">Client Secret</label> 697 + <div style="background: rgba(0, 0, 0, 0.3); padding: 1rem; border: 1px solid var(--old-rose); margin-bottom: 0.5rem;"> 698 + <code id="generatedSecret" style="color: var(--lavender); font-size: 0.875rem; word-break: break-all; display: block;"></code> 699 + </div> 700 + <button class="btn" id="copySecretBtn" style="width: 100%; background: rgba(188, 141, 160, 0.2);">copy secret</button> 701 + </div> 696 702 </div> 697 703 </div> 698 704 </div>