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