my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 400 lines 11 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>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()">&times;</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()">&times;</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>