my own indieAuth provider!
indiko.dunkirk.sh/docs
indieauth
oauth2-server
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">×</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 editor 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">×</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>