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>invites • indiko</title>
8 <meta name="description" content="Indiko admin panel - manage invites" />
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="Admin • Indiko" />
14 <meta property="og:description" content="Indiko admin panel - manage users and invites" />
15
16 <!-- Twitter -->
17 <meta name="twitter:card" content="summary" />
18 <meta name="twitter:title" content="Admin • Indiko" />
19 <meta name="twitter:description" content="Indiko admin panel - manage users and invites" />
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 <link rel="stylesheet" href="../styles.css">
24 <style>
25 /* Admin Invites-specific styles */
26 header {
27 align-self: flex-start;
28 margin-left: auto;
29 margin-right: auto;
30 display: flex;
31 justify-content: space-between;
32 align-items: flex-start;
33 }
34
35 main {
36 padding: 2rem 1.25rem;
37 }
38
39 .invites-section {
40 width: 100%;
41 }
42
43 .invite-actions {
44 display: flex;
45 gap: 1rem;
46 margin-bottom: 1.5rem;
47 }
48
49 .invite-btn {
50 padding: 0.75rem 1.5rem;
51 background: var(--berry-crush);
52 color: var(--lavender);
53 border: none;
54 cursor: pointer;
55 font-family: inherit;
56 font-size: 1rem;
57 font-weight: 500;
58 transition: background 0.2s;
59 }
60
61 .invite-btn:hover {
62 background: var(--rosewood);
63 }
64
65 .invite-btn:disabled {
66 opacity: 0.5;
67 cursor: not-allowed;
68 }
69
70 .invite-list {
71 display: flex;
72 flex-direction: column;
73 gap: 0.75rem;
74 }
75
76 .invite-item {
77 background: rgba(188, 141, 160, 0.05);
78 border: 1px solid var(--old-rose);
79 padding: 1rem;
80 display: grid;
81 grid-template-columns: 1fr auto;
82 gap: 1rem;
83 align-items: start;
84 }
85
86 .invite-code {
87 font-family: monospace;
88 font-size: 0.875rem;
89 color: var(--lavender);
90 word-break: break-all;
91 }
92
93 .invite-meta {
94 font-size: 0.75rem;
95 color: var(--old-rose);
96 margin-top: 0.25rem;
97 }
98
99 .invite-actions-btns {
100 display: flex;
101 flex-direction: column;
102 gap: 0.5rem;
103 min-width: 8rem;
104 }
105
106 .invite-url {
107 background: rgba(12, 23, 19, 0.8);
108 border: 1px solid var(--rosewood);
109 padding: 0.75rem;
110 margin-top: 0.5rem;
111 font-family: monospace;
112 font-size: 0.875rem;
113 word-break: break-all;
114 color: var(--berry-crush);
115 border-left: 3px solid var(--berry-crush);
116 }
117
118 .invite-inactive {
119 opacity: 0.6;
120 }
121
122 .invite-note {
123 font-size: 0.875rem;
124 color: var(--lavender);
125 margin-top: 0.5rem;
126 font-style: italic;
127 }
128
129 .invite-message {
130 font-size: 0.875rem;
131 color: var(--old-rose);
132 margin-top: 0.5rem;
133 font-style: italic;
134 }
135
136 .invite-roles {
137 font-size: 0.875rem;
138 color: var(--berry-crush);
139 margin-top: 0.5rem;
140 }
141
142 .invite-used-by {
143 font-size: 0.75rem;
144 color: var(--old-rose);
145 margin-top: 0.5rem;
146 }
147
148 .form-group label {
149 color: var(--lavender);
150 }
151
152 .form-group input,
153 .form-group textarea {
154 background: rgba(0, 0, 0, 0.3);
155 border: 1px solid var(--old-rose);
156 }
157
158 .form-group input:focus,
159 .form-group textarea:focus {
160 outline: none;
161 border-color: var(--berry-crush);
162 }
163
164 .app-role-item {
165 display: flex;
166 align-items: center;
167 gap: 1rem;
168 padding: 0.75rem;
169 background: rgba(188, 141, 160, 0.05);
170 border: 1px solid var(--old-rose);
171 margin-bottom: 0.5rem;
172 }
173
174 .app-role-item label {
175 display: flex;
176 align-items: center;
177 gap: 0.5rem;
178 flex: 1;
179 margin: 0;
180 cursor: pointer;
181 }
182
183 .app-role-item input[type="checkbox"] {
184 appearance: none;
185 width: 1.5rem;
186 height: 1.5rem;
187 border: 2px solid var(--old-rose);
188 background: rgba(12, 23, 19, 0.6);
189 cursor: pointer;
190 flex-shrink: 0;
191 position: relative;
192 transition: all 0.2s;
193 }
194
195 .app-role-item input[type="checkbox"]:checked {
196 background: var(--berry-crush);
197 border-color: var(--berry-crush);
198 }
199
200 .app-role-item input[type="checkbox"]:checked::after {
201 content: "✓";
202 position: absolute;
203 top: 50%;
204 left: 50%;
205 transform: translate(-50%, -50%);
206 color: var(--lavender);
207 font-size: 1rem;
208 font-weight: 700;
209 }
210
211 .app-role-item input[type="checkbox"]:disabled {
212 cursor: not-allowed;
213 opacity: 0.5;
214 }
215
216 .app-role-item input.role-input-custom {
217 flex: 1;
218 padding: 0.5rem;
219 font-size: 0.875rem;
220 }
221
222 .app-role-item select.role-select {
223 flex: 1;
224 background: rgba(0, 0, 0, 0.3);
225 border: 1px solid var(--old-rose);
226 color: var(--lavender);
227 padding: 0.5rem 2.5rem 0.5rem 0.5rem;
228 font-family: inherit;
229 font-size: 0.875rem;
230 cursor: pointer;
231 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23d9d0de' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
232 background-repeat: no-repeat;
233 background-position: right 0.75rem center;
234 appearance: none;
235 }
236
237 .app-role-item select.role-select:disabled {
238 opacity: 0.5;
239 cursor: not-allowed;
240 }
241
242 .app-role-item select.role-select:focus {
243 outline: none;
244 border-color: var(--berry-crush);
245 }
246
247 .modal-btn {
248 padding: 0.75rem 1.5rem;
249 font-family: inherit;
250 font-size: 1rem;
251 border: none;
252 cursor: pointer;
253 transition: background 0.2s;
254 }
255
256 .modal-btn-primary {
257 background: var(--berry-crush);
258 color: var(--lavender);
259 }
260
261 .modal-btn-primary:hover {
262 background: var(--rosewood);
263 }
264
265 .modal-btn-primary:disabled {
266 opacity: 0.5;
267 cursor: not-allowed;
268 }
269
270 .modal-btn-secondary {
271 background: rgba(188, 141, 160, 0.2);
272 color: var(--lavender);
273 border: 1px solid var(--old-rose);
274 }
275
276 .modal-btn-secondary:hover {
277 background: rgba(188, 141, 160, 0.3);
278 }
279 </style>
280</head>
281
282<body>
283 <header>
284 <div>
285 <img src="../../public/logo.svg" alt="indiko" style="height: 2rem;" />
286 </div>
287 <div class="header-nav">
288 <a href="/admin">users</a>
289 <a href="/admin/invites" class="active">invites</a>
290 <a href="/admin/clients">apps</a>
291 </div>
292 </header>
293
294 <main>
295 <div class="invites-section">
296 <h2>invites</h2>
297 <div class="invite-actions">
298 <button class="invite-btn" id="createInviteBtn">create invite link</button>
299 </div>
300 <div id="invitesList" class="invite-list">
301 <div class="loading">loading invites...</div>
302 </div>
303 </div>
304 </main>
305
306 <footer id="footer">
307 loading...
308 <div class="back-link"><a href="/">← back to dashboard</a></div>
309 </footer>
310
311 <!-- Create Invite Modal -->
312 <div id="createInviteModal" class="modal">
313 <div class="modal-content">
314 <div class="modal-header">
315 <h3>create invite</h3>
316 <button class="modal-close" onclick="closeCreateInviteModal()">×</button>
317 </div>
318
319 <div class="form-group">
320 <label for="maxUses">maximum uses</label>
321 <input type="number" id="maxUses" min="1" max="999" value="1">
322 <div class="form-help">How many people can use this invite (1-999)</div>
323 </div>
324
325 <div class="form-group">
326 <label for="expiresAt">expiration date</label>
327 <input type="datetime-local" id="expiresAt">
328 <div class="form-help">Optional: leave empty for no expiry</div>
329 </div>
330
331 <div class="form-group">
332 <label for="inviteNote">internal note</label>
333 <textarea id="inviteNote" placeholder="Optional internal note (not visible to invitees)"></textarea>
334 <div class="form-help">Private admin note to help you remember what this invite is for</div>
335 </div>
336
337 <div class="form-group">
338 <label for="inviteMessage">message to invitees</label>
339 <textarea id="inviteMessage" placeholder="Optional welcome message shown to invitees"></textarea>
340 <div class="form-help">Public message that will be shown to users when they use this invite</div>
341 </div>
342
343 <div class="form-group">
344 <label>app role assignments (optional)</label>
345 <div class="form-help">Users who register with this invite will automatically be assigned these roles</div>
346 <div id="appRolesContainer" style="margin-top: 1rem;">
347 <div class="loading">Loading apps...</div>
348 </div>
349 </div>
350
351 <div class="modal-actions">
352 <button class="modal-btn modal-btn-secondary" onclick="closeCreateInviteModal()">cancel</button>
353 <button class="modal-btn modal-btn-primary" id="submitInviteBtn" onclick="submitCreateInvite()">create invite</button>
354 </div>
355 </div>
356 </div>
357
358 <!-- Edit Invite Modal -->
359 <div id="editInviteModal" class="modal">
360 <div class="modal-content">
361 <div class="modal-header">
362 <h3>edit invite</h3>
363 <button class="modal-close" onclick="closeEditInviteModal()">×</button>
364 </div>
365
366 <div class="form-group">
367 <label for="editMaxUses">maximum uses remaining</label>
368 <input type="number" id="editMaxUses" min="0" max="999">
369 <div class="form-help">Total number of times this invite can be used</div>
370 </div>
371
372 <div class="form-group">
373 <label for="editExpiresAt">expiration date</label>
374 <input type="datetime-local" id="editExpiresAt">
375 <div class="form-help">Leave empty for no expiry</div>
376 </div>
377
378 <div class="form-group">
379 <label for="editInviteNote">internal note</label>
380 <textarea id="editInviteNote" placeholder="Optional internal note (not visible to invitees)"></textarea>
381 <div class="form-help">Private admin note to help you remember what this invite is for</div>
382 </div>
383
384 <div class="form-group">
385 <label for="editInviteMessage">message to invitees</label>
386 <textarea id="editInviteMessage" placeholder="Optional welcome message shown to invitees"></textarea>
387 <div class="form-help">Public message that will be shown to users when they use this invite</div>
388 </div>
389
390 <div class="modal-actions">
391 <button class="modal-btn modal-btn-secondary" onclick="closeEditInviteModal()">cancel</button>
392 <button class="modal-btn modal-btn-primary" id="submitEditInviteBtn" onclick="submitEditInvite()">save changes</button>
393 </div>
394 </div>
395 </div>
396
397 <script type="module" src="../client/admin-invites.ts"></script>
398</body>
399
400</html>