my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 709 lines 16 kB view raw
1<!doctype html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>oauth clients • admin • indiko</title> 8 <meta name="description" content="Manage OAuth clients and application registrations" /> 9 <link rel="icon" href="../../public/favicon.svg" type="image/svg+xml" /> 10 11 <!-- Open Graph / Facebook --> 12 <meta property="og:type" content="website" /> 13 <meta property="og:title" content="OAuth Clients • Indiko Admin" /> 14 <meta property="og:description" content="Manage OAuth clients and application registrations" /> 15 16 <!-- Twitter --> 17 <meta name="twitter:card" content="summary" /> 18 <meta name="twitter:title" content="OAuth Clients • Indiko Admin" /> 19 <meta name="twitter:description" content="Manage OAuth clients and application registrations" /> 20 <link rel="preconnect" href="https://fonts.googleapis.com"> 21 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 22 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet"> 23 <style> 24 :root { 25 --mahogany: #26242b; 26 --lavender: #d9d0de; 27 --old-rose: #bc8da0; 28 --rosewood: #a04668; 29 --berry-crush: #ab4967; 30 } 31 32 * { 33 margin: 0; 34 padding: 0; 35 box-sizing: border-box; 36 } 37 38 body { 39 font-family: "Space Grotesk", sans-serif; 40 background: var(--mahogany); 41 color: var(--lavender); 42 min-height: 100vh; 43 display: flex; 44 flex-direction: column; 45 align-items: center; 46 padding: 2.5rem 1.25rem; 47 } 48 49 header { 50 width: 100%; 51 max-width: 56.25rem; 52 align-self: flex-start; 53 margin-left: auto; 54 margin-right: auto; 55 margin-bottom: 2rem; 56 display: flex; 57 justify-content: space-between; 58 align-items: flex-start; 59 } 60 61 .header-nav { 62 display: flex; 63 gap: 1rem; 64 margin-top: 0.5rem; 65 } 66 67 .header-nav a { 68 color: var(--old-rose); 69 text-decoration: none; 70 font-size: 0.875rem; 71 font-weight: 500; 72 padding: 0.5rem 1rem; 73 border: 1px solid var(--old-rose); 74 transition: all 0.2s; 75 } 76 77 .header-nav a:hover { 78 background: rgba(188, 141, 160, 0.1); 79 color: var(--berry-crush); 80 border-color: var(--berry-crush); 81 } 82 83 .header-nav a.active { 84 background: var(--berry-crush); 85 color: var(--lavender); 86 border-color: var(--berry-crush); 87 } 88 89 h1 { 90 font-size: 2rem; 91 font-weight: 700; 92 background: linear-gradient(135deg, var(--old-rose), var(--berry-crush), var(--rosewood)); 93 -webkit-background-clip: text; 94 -webkit-text-fill-color: transparent; 95 background-clip: text; 96 letter-spacing: -0.125rem; 97 } 98 99 main { 100 flex: 1; 101 width: 100%; 102 max-width: 56.25rem; 103 padding: 2rem 1.25rem; 104 } 105 106 h2 { 107 font-size: 1.5rem; 108 font-weight: 600; 109 color: var(--lavender); 110 margin-bottom: 1.5rem; 111 letter-spacing: -0.05rem; 112 } 113 114 footer { 115 width: 100%; 116 max-width: 56.25rem; 117 padding: 1rem; 118 text-align: center; 119 color: var(--old-rose); 120 font-size: 0.875rem; 121 font-weight: 300; 122 letter-spacing: 0.05rem; 123 } 124 125 footer a { 126 color: var(--berry-crush); 127 text-decoration: none; 128 transition: color 0.2s; 129 } 130 131 footer a:hover { 132 color: var(--rosewood); 133 text-decoration: underline; 134 } 135 136 .back-link { 137 margin-top: 0.5rem; 138 font-size: 0.875rem; 139 color: var(--old-rose); 140 } 141 142 .actions { 143 display: flex; 144 justify-content: space-between; 145 align-items: center; 146 margin-bottom: 1.5rem; 147 } 148 149 .btn { 150 padding: 0.75rem 1.5rem; 151 background: var(--berry-crush); 152 color: var(--lavender); 153 border: none; 154 cursor: pointer; 155 font-family: inherit; 156 font-size: 1rem; 157 font-weight: 500; 158 transition: background 0.2s; 159 text-decoration: none; 160 display: inline-block; 161 } 162 163 .btn:hover { 164 background: var(--rosewood); 165 } 166 167 .btn:disabled { 168 opacity: 0.5; 169 cursor: not-allowed; 170 } 171 172 .clients-list { 173 display: flex; 174 flex-direction: column; 175 gap: 1rem; 176 } 177 178 .client-card { 179 background: rgba(188, 141, 160, 0.05); 180 border: 1px solid var(--old-rose); 181 padding: 1.5rem; 182 cursor: pointer; 183 transition: background 0.2s; 184 } 185 186 .client-card:hover { 187 background: rgba(188, 141, 160, 0.1); 188 } 189 190 .client-card.expanded { 191 background: rgba(188, 141, 160, 0.1); 192 } 193 194 .client-header { 195 display: flex; 196 gap: 1rem; 197 align-items: flex-start; 198 } 199 200 .client-logo { 201 width: 4rem; 202 height: 4rem; 203 border-radius: 0.5rem; 204 background: rgba(188, 141, 160, 0.2); 205 display: flex; 206 align-items: center; 207 justify-content: center; 208 flex-shrink: 0; 209 overflow: hidden; 210 } 211 212 .client-logo img { 213 width: 100%; 214 height: 100%; 215 object-fit: cover; 216 } 217 218 .client-logo-placeholder { 219 font-size: 1.5rem; 220 color: var(--old-rose); 221 } 222 223 .client-info { 224 flex: 1; 225 } 226 227 .client-name { 228 font-size: 1.125rem; 229 font-weight: 600; 230 color: var(--lavender); 231 margin-bottom: 0.25rem; 232 } 233 234 .client-id { 235 font-size: 0.75rem; 236 color: var(--old-rose); 237 font-family: monospace; 238 margin-bottom: 0.5rem; 239 } 240 241 .client-description { 242 font-size: 0.875rem; 243 color: var(--old-rose); 244 margin-bottom: 0.5rem; 245 } 246 247 .client-badges { 248 display: flex; 249 gap: 0.5rem; 250 flex-wrap: wrap; 251 margin-top: 0.5rem; 252 } 253 254 .badge { 255 padding: 0.25rem 0.75rem; 256 font-size: 0.75rem; 257 font-weight: 700; 258 text-transform: uppercase; 259 letter-spacing: 0.05rem; 260 } 261 262 .badge-preregistered { 263 background: var(--berry-crush); 264 color: var(--lavender); 265 } 266 267 .badge-auto { 268 background: rgba(188, 141, 160, 0.2); 269 color: var(--lavender); 270 border: 1px solid var(--old-rose); 271 } 272 273 .client-details { 274 margin-top: 1.5rem; 275 padding-top: 1.5rem; 276 border-top: 1px solid var(--old-rose); 277 display: none; 278 } 279 280 .client-card.expanded .client-details { 281 display: block; 282 } 283 284 .detail-section { 285 margin-bottom: 1.5rem; 286 } 287 288 .detail-title { 289 font-size: 0.75rem; 290 color: var(--old-rose); 291 text-transform: uppercase; 292 letter-spacing: 0.05rem; 293 margin-bottom: 0.5rem; 294 } 295 296 .redirect-uris { 297 display: flex; 298 flex-direction: column; 299 gap: 0.25rem; 300 } 301 302 .redirect-uri { 303 font-family: monospace; 304 font-size: 0.75rem; 305 color: var(--lavender); 306 background: rgba(0, 0, 0, 0.2); 307 padding: 0.5rem; 308 } 309 310 .users-list { 311 display: flex; 312 flex-direction: column; 313 gap: 0.75rem; 314 } 315 316 .user-item { 317 background: rgba(0, 0, 0, 0.2); 318 padding: 1rem; 319 display: flex; 320 justify-content: space-between; 321 align-items: center; 322 } 323 324 .user-info { 325 flex: 1; 326 } 327 328 .user-name { 329 font-weight: 600; 330 color: var(--lavender); 331 margin-bottom: 0.25rem; 332 } 333 334 .user-role-input { 335 display: flex; 336 gap: 0.5rem; 337 align-items: center; 338 margin-top: 0.5rem; 339 } 340 341 .user-role-input input { 342 padding: 0.5rem; 343 background: rgba(0, 0, 0, 0.3); 344 border: 1px solid var(--old-rose); 345 color: var(--lavender); 346 font-family: inherit; 347 font-size: 0.875rem; 348 } 349 350 .user-role-input button { 351 padding: 0.5rem 1rem; 352 background: var(--berry-crush); 353 color: var(--lavender); 354 border: none; 355 cursor: pointer; 356 font-family: inherit; 357 font-size: 0.875rem; 358 transition: background 0.2s; 359 } 360 361 .user-role-input button:hover { 362 background: var(--rosewood); 363 } 364 365 .user-meta { 366 font-size: 0.75rem; 367 color: var(--old-rose); 368 } 369 370 .expand-indicator { 371 color: var(--old-rose); 372 font-size: 0.75rem; 373 text-transform: uppercase; 374 letter-spacing: 0.05rem; 375 cursor: pointer; 376 } 377 378 .client-header { 379 cursor: pointer; 380 } 381 382 .client-actions { 383 display: flex; 384 gap: 0.5rem; 385 margin-top: 1rem; 386 } 387 388 .btn-edit, .btn-delete, .revoke-btn { 389 padding: 0.5rem 1rem; 390 font-family: inherit; 391 font-size: 0.875rem; 392 font-weight: 600; 393 cursor: pointer; 394 transition: all 0.2s; 395 border: none; 396 } 397 398 .btn-edit { 399 background: rgba(188, 141, 160, 0.2); 400 color: var(--lavender); 401 } 402 403 .btn-edit:hover { 404 background: rgba(188, 141, 160, 0.3); 405 } 406 407 .btn-delete, .revoke-btn { 408 background: rgba(160, 70, 104, 0.2); 409 color: var(--lavender); 410 border: 2px solid var(--rosewood); 411 } 412 413 .btn-delete:hover, .revoke-btn:hover { 414 background: rgba(160, 70, 104, 0.3); 415 } 416 417 .loading, .error, .empty { 418 text-align: center; 419 padding: 2rem; 420 color: var(--old-rose); 421 } 422 423 .error { 424 color: var(--rosewood); 425 } 426 427 .modal { 428 display: none; 429 position: fixed; 430 top: 0; 431 left: 0; 432 right: 0; 433 bottom: 0; 434 background: rgba(0, 0, 0, 0.8); 435 z-index: 1000; 436 align-items: center; 437 justify-content: center; 438 } 439 440 .modal.active { 441 display: flex; 442 } 443 444 .modal-content { 445 background: var(--mahogany); 446 border: 2px solid var(--old-rose); 447 padding: 2rem; 448 max-width: 40rem; 449 width: 90%; 450 max-height: 90vh; 451 overflow-y: auto; 452 } 453 454 .modal-header { 455 display: flex; 456 justify-content: space-between; 457 align-items: center; 458 margin-bottom: 1.5rem; 459 } 460 461 .modal-title { 462 font-size: 1.5rem; 463 font-weight: 600; 464 color: var(--lavender); 465 } 466 467 .modal-close { 468 background: none; 469 border: none; 470 color: var(--old-rose); 471 font-size: 1.5rem; 472 cursor: pointer; 473 padding: 0; 474 width: 2rem; 475 height: 2rem; 476 } 477 478 .modal-close:hover { 479 color: var(--lavender); 480 } 481 482 .form-group { 483 margin-bottom: 1rem; 484 } 485 486 .form-label { 487 display: block; 488 font-size: 0.875rem; 489 color: var(--old-rose); 490 margin-bottom: 0.5rem; 491 text-transform: uppercase; 492 letter-spacing: 0.05rem; 493 } 494 495 .form-input { 496 width: 100%; 497 padding: 0.75rem; 498 background: rgba(0, 0, 0, 0.3); 499 border: 1px solid var(--old-rose); 500 color: var(--lavender); 501 font-family: inherit; 502 font-size: 1rem; 503 } 504 505 .form-input:focus { 506 outline: none; 507 border-color: var(--berry-crush); 508 } 509 510 .form-textarea { 511 min-height: 5rem; 512 resize: vertical; 513 } 514 515 .redirect-uris-list { 516 display: flex; 517 flex-direction: column; 518 gap: 0.5rem; 519 margin-top: 0.5rem; 520 } 521 522 .redirect-uri-item { 523 display: flex; 524 gap: 0.5rem; 525 } 526 527 .redirect-uri-item input { 528 flex: 1; 529 } 530 531 .btn-remove { 532 padding: 0.5rem 1rem; 533 background: rgba(160, 70, 104, 0.2); 534 color: var(--lavender); 535 border: none; 536 cursor: pointer; 537 font-family: inherit; 538 } 539 540 .btn-remove:hover { 541 background: rgba(160, 70, 104, 0.3); 542 } 543 544 .btn-add { 545 margin-top: 0.5rem; 546 padding: 0.5rem 1rem; 547 background: rgba(188, 141, 160, 0.2); 548 color: var(--lavender); 549 border: none; 550 cursor: pointer; 551 font-family: inherit; 552 } 553 554 .btn-add:hover { 555 background: rgba(188, 141, 160, 0.3); 556 } 557 558 .form-actions { 559 display: flex; 560 gap: 1rem; 561 margin-top: 1.5rem; 562 } 563 564 .form-actions .btn { 565 flex: 1; 566 } 567 568 .toast { 569 position: fixed; 570 bottom: 2rem; 571 right: 2rem; 572 background: var(--mahogany); 573 border: 2px solid var(--berry-crush); 574 padding: 1rem 1.5rem; 575 color: var(--lavender); 576 font-size: 0.875rem; 577 font-weight: 500; 578 z-index: 2000; 579 opacity: 0; 580 transform: translateY(1rem); 581 transition: opacity 0.3s, transform 0.3s; 582 max-width: 25rem; 583 box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.5); 584 } 585 586 .toast.show { 587 opacity: 1; 588 transform: translateY(0); 589 } 590 591 .toast.error { 592 border-color: var(--rosewood); 593 } 594 595 .toast.success { 596 border-color: var(--berry-crush); 597 } 598 </style> 599</head> 600 601<body> 602 <header> 603 <div> 604 <img src="../../public/logo.svg" alt="indiko" style="height: 2rem;" /> 605 </div> 606 <div class="header-nav"> 607 <a href="/admin">users</a> 608 <a href="/admin/invites">invites</a> 609 <a href="/admin/clients" class="active">apps</a> 610 </div> 611 </header> 612 613 <main> 614 <div class="actions"> 615 <h2>oauth clients</h2> 616 <button class="btn" id="createClientBtn">create client</button> 617 </div> 618 <div id="clientsList" class="clients-list"> 619 <div class="loading">loading clients...</div> 620 </div> 621 </main> 622 623 <div id="toast" class="toast"></div> 624 625 <footer id="footer"> 626 loading... 627 <div class="back-link"><a href="/">← back to dashboard</a></div> 628 </footer> 629 630 <div id="clientModal" class="modal"> 631 <div class="modal-content"> 632 <div class="modal-header"> 633 <h3 class="modal-title" id="modalTitle">Create OAuth Client</h3> 634 <button class="modal-close" id="modalClose">&times;</button> 635 </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> 642 <div class="form-group"> 643 <label class="form-label" for="logoUrl">Logo URL</label> 644 <input type="url" class="form-input" id="logoUrl" placeholder="https://example.com/logo.png" /> 645 </div> 646 <div class="form-group"> 647 <label class="form-label" for="description">Description</label> 648 <textarea class="form-input form-textarea" id="description" placeholder="A brief description of your application"></textarea> 649 </div> 650 <div class="form-group"> 651 <label class="form-label">Redirect URIs</label> 652 <div id="redirectUrisList" class="redirect-uris-list"> 653 <div class="redirect-uri-item"> 654 <input type="url" class="form-input redirect-uri-input" placeholder="https://example.com/auth/callback" required /> 655 <button type="button" class="btn-remove" onclick="removeRedirectUri(this)">remove</button> 656 </div> 657 </div> 658 <button type="button" class="btn-add" id="addRedirectUriBtn">add redirect uri</button> 659 </div> 660 <div class="form-group"> 661 <label class="form-label">Available Roles (one per line)</label> 662 <textarea class="form-input form-textarea" id="availableRoles" placeholder="admin&#10;editor&#10;viewer" style="min-height: 6rem;"></textarea> 663 <p style="font-size: 0.75rem; color: var(--old-rose); margin-top: 0.5rem;">Define which roles can be assigned to users for this app. Leave empty to allow free-text roles.</p> 664 </div> 665 <div class="form-group"> 666 <label class="form-label" for="defaultRole">Default Role</label> 667 <input type="text" class="form-input" id="defaultRole" placeholder="Leave empty for no default" /> 668 <p style="font-size: 0.75rem; color: var(--old-rose); margin-top: 0.5rem;">Automatically assigned when users first authorize this app.</p> 669 </div> 670 <div class="form-actions"> 671 <button type="button" class="btn" style="background: rgba(188, 141, 160, 0.2);" id="cancelBtn">cancel</button> 672 <button type="submit" class="btn">save</button> 673 </div> 674 </form> 675 </div> 676 </div> 677 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> 705 706 <script type="module" src="../client/admin-clients.ts"></script> 707</body> 708 709</html>