Microservice to bring 2FA to self hosted PDSes

wip of template partials

+291 -1593
+3 -226
html_templates/admin/account_detail.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>{{account.handle}} - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 9 + {{> admin/partials/base_css.hbs}} 154 10 155 11 .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 12 margin-bottom: 4px; 159 13 } 160 14 ··· 164 18 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 165 19 margin-bottom: 24px; 166 20 word-break: break-all; 167 - } 168 - 169 - .flash-success { 170 - background: rgba(22, 163, 74, 0.1); 171 - color: var(--success-color); 172 - border: 1px solid rgba(22, 163, 74, 0.2); 173 - border-radius: 8px; 174 - padding: 10px 14px; 175 - font-size: 0.875rem; 176 - margin-bottom: 20px; 177 - } 178 - 179 - .flash-error { 180 - background: rgba(220, 38, 38, 0.1); 181 - color: var(--danger-color); 182 - border: 1px solid rgba(220, 38, 38, 0.2); 183 - border-radius: 8px; 184 - padding: 10px 14px; 185 - font-size: 0.875rem; 186 - margin-bottom: 20px; 187 21 } 188 22 189 23 .detail-section { ··· 260 94 } 261 95 262 96 .btn { 263 - display: inline-flex; 264 - align-items: center; 265 - justify-content: center; 266 97 padding: 8px 16px; 267 98 font-size: 0.8125rem; 268 - font-weight: 500; 269 99 border: 1px solid var(--border-color); 270 - border-radius: 8px; 271 - cursor: pointer; 272 - transition: opacity 0.15s; 273 - text-decoration: none; 274 100 background: var(--bg-primary-color); 275 101 color: var(--primary-color); 276 102 } 277 103 278 - .btn:hover { 279 - opacity: 0.85; 280 - } 281 - 282 104 .btn-primary { 283 - background: var(--brand-color); 284 - color: #fff; 285 105 border-color: var(--brand-color); 286 106 } 287 107 288 108 .btn-danger { 289 - background: var(--danger-color); 290 - color: #fff; 291 109 border-color: var(--danger-color); 292 110 } 293 111 294 112 .btn-warning { 295 - background: var(--warning-color); 296 - color: #000; 297 113 border-color: var(--warning-color); 298 114 } 299 115 ··· 355 171 padding: 4px 0; 356 172 color: var(--secondary-color); 357 173 } 358 - 359 - @media (max-width: 768px) { 360 - .sidebar { 361 - display: none; 362 - } 363 - 364 - .main { 365 - margin-left: 0; 366 - } 367 - } 368 174 </style> 369 175 </head> 370 176 <body> 371 177 <div class="layout"> 372 - <aside class="sidebar"> 373 - <div class="sidebar-title">{{pds_hostname}}</div> 374 - <div class="sidebar-subtitle">Admin Portal</div> 375 - <nav> 376 - <a href="/admin/dashboard">Dashboard</a> 377 - {{#if can_view_accounts}} 378 - <a href="/admin/accounts" class="active">Accounts</a> 379 - {{/if}} 380 - {{#if can_manage_invites}} 381 - <a href="/admin/invite-codes">Invite Codes</a> 382 - {{/if}} 383 - {{#if can_create_account}} 384 - <a href="/admin/create-account">Create Account</a> 385 - {{/if}} 386 - {{#if can_request_crawl}} 387 - <a href="/admin/request-crawl">Request Crawl</a> 388 - {{/if}} 389 - </nav> 390 - <div class="sidebar-footer"> 391 - <div class="session-info">Signed in as {{handle}}</div> 392 - <form method="POST" action="/admin/logout"> 393 - <button type="submit">Sign out</button> 394 - </form> 395 - </div> 396 - </aside> 178 + {{> admin/partials/sidebar.hbs}} 397 179 398 180 <main class="main"> 399 - {{#if flash_success}} 400 - <div class="flash-success">{{flash_success}}</div> 401 - {{/if}} 402 - {{#if flash_error}} 403 - <div class="flash-error">{{flash_error}}</div> 404 - {{/if}} 181 + {{> admin/partials/flash.hbs}} 405 182 406 183 <a href="/admin/accounts" class="back-link">&larr; Back to Accounts</a> 407 184
+3 -235
html_templates/admin/accounts.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Accounts - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 154 - 155 - .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 - margin-bottom: 24px; 159 - } 160 - 161 - .flash-success { 162 - background: rgba(22, 163, 74, 0.1); 163 - color: var(--success-color); 164 - border: 1px solid rgba(22, 163, 74, 0.2); 165 - border-radius: 8px; 166 - padding: 10px 14px; 167 - font-size: 0.875rem; 168 - margin-bottom: 20px; 169 - } 170 - 171 - .flash-error { 172 - background: rgba(220, 38, 38, 0.1); 173 - color: var(--danger-color); 174 - border: 1px solid rgba(220, 38, 38, 0.2); 175 - border-radius: 8px; 176 - padding: 10px 14px; 177 - font-size: 0.875rem; 178 - margin-bottom: 20px; 179 - } 9 + {{> admin/partials/base_css.hbs}} 180 10 181 11 .search-form { 182 12 display: flex; ··· 199 29 border-color: var(--brand-color); 200 30 } 201 31 202 - .btn { 203 - display: inline-flex; 204 - align-items: center; 205 - justify-content: center; 206 - padding: 10px 20px; 207 - font-size: 0.875rem; 208 - font-weight: 500; 209 - border: none; 210 - border-radius: 8px; 211 - cursor: pointer; 212 - transition: opacity 0.15s; 213 - text-decoration: none; 214 - } 215 - 216 - .btn:hover { 217 - opacity: 0.85; 218 - } 219 - 220 - .btn-primary { 221 - background: var(--brand-color); 222 - color: #fff; 223 - } 224 - 225 32 .table-container { 226 33 background: var(--bg-primary-color); 227 34 border: 1px solid var(--border-color); ··· 283 90 font-size: 0.75rem; 284 91 color: var(--secondary-color); 285 92 } 286 - 287 - @media (max-width: 768px) { 288 - .sidebar { 289 - display: none; 290 - } 291 - 292 - .main { 293 - margin-left: 0; 294 - } 295 - } 296 93 </style> 297 94 </head> 298 95 <body> 299 96 <div class="layout"> 300 - <aside class="sidebar"> 301 - <div class="sidebar-title">{{pds_hostname}}</div> 302 - <div class="sidebar-subtitle">Admin Portal</div> 303 - <nav> 304 - <a href="/admin/dashboard">Dashboard</a> 305 - {{#if can_view_accounts}} 306 - <a href="/admin/accounts" class="active">Accounts</a> 307 - {{/if}} 308 - {{#if can_manage_invites}} 309 - <a href="/admin/invite-codes">Invite Codes</a> 310 - {{/if}} 311 - {{#if can_create_account}} 312 - <a href="/admin/create-account">Create Account</a> 313 - {{/if}} 314 - {{#if can_request_crawl}} 315 - <a href="/admin/request-crawl">Request Crawl</a> 316 - {{/if}} 317 - </nav> 318 - <div class="sidebar-footer"> 319 - <div class="session-info">Signed in as {{handle}}</div> 320 - <form method="POST" action="/admin/logout"> 321 - <button type="submit">Sign out</button> 322 - </form> 323 - </div> 324 - </aside> 97 + {{> admin/partials/sidebar.hbs}} 325 98 326 99 <main class="main"> 327 - {{#if flash_success}} 328 - <div class="flash-success">{{flash_success}}</div> 329 - {{/if}} 330 - {{#if flash_error}} 331 - <div class="flash-error">{{flash_error}}</div> 332 - {{/if}} 100 + {{> admin/partials/flash.hbs}} 333 101 334 102 <h1 class="page-title">Accounts</h1> 335 103
+3 -235
html_templates/admin/create_account.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Create Account - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 154 - 155 - .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 - margin-bottom: 24px; 159 - } 160 - 161 - .flash-success { 162 - background: rgba(22, 163, 74, 0.1); 163 - color: var(--success-color); 164 - border: 1px solid rgba(22, 163, 74, 0.2); 165 - border-radius: 8px; 166 - padding: 10px 14px; 167 - font-size: 0.875rem; 168 - margin-bottom: 20px; 169 - } 170 - 171 - .flash-error { 172 - background: rgba(220, 38, 38, 0.1); 173 - color: var(--danger-color); 174 - border: 1px solid rgba(220, 38, 38, 0.2); 175 - border-radius: 8px; 176 - padding: 10px 14px; 177 - font-size: 0.875rem; 178 - margin-bottom: 20px; 179 - } 9 + {{> admin/partials/base_css.hbs}} 180 10 181 11 .form-card { 182 12 background: var(--bg-primary-color); ··· 220 50 margin-top: 4px; 221 51 } 222 52 223 - .btn { 224 - display: inline-flex; 225 - align-items: center; 226 - justify-content: center; 227 - padding: 10px 20px; 228 - font-size: 0.875rem; 229 - font-weight: 500; 230 - border: none; 231 - border-radius: 8px; 232 - cursor: pointer; 233 - transition: opacity 0.15s; 234 - text-decoration: none; 235 - } 236 - 237 - .btn:hover { 238 - opacity: 0.85; 239 - } 240 - 241 - .btn-primary { 242 - background: var(--brand-color); 243 - color: #fff; 244 - } 245 - 246 53 .success-card { 247 54 background: var(--bg-primary-color); 248 55 border: 1px solid var(--border-color); ··· 309 116 color: var(--primary-color); 310 117 border-color: var(--primary-color); 311 118 } 312 - 313 - @media (max-width: 768px) { 314 - .sidebar { 315 - display: none; 316 - } 317 - 318 - .main { 319 - margin-left: 0; 320 - } 321 - } 322 119 </style> 323 120 </head> 324 121 <body> 325 122 <div class="layout"> 326 - <aside class="sidebar"> 327 - <div class="sidebar-title">{{pds_hostname}}</div> 328 - <div class="sidebar-subtitle">Admin Portal</div> 329 - <nav> 330 - <a href="/admin/dashboard">Dashboard</a> 331 - {{#if can_view_accounts}} 332 - <a href="/admin/accounts">Accounts</a> 333 - {{/if}} 334 - {{#if can_manage_invites}} 335 - <a href="/admin/invite-codes">Invite Codes</a> 336 - {{/if}} 337 - {{#if can_create_account}} 338 - <a href="/admin/create-account" class="active">Create Account</a> 339 - {{/if}} 340 - {{#if can_request_crawl}} 341 - <a href="/admin/request-crawl">Request Crawl</a> 342 - {{/if}} 343 - </nav> 344 - <div class="sidebar-footer"> 345 - <div class="session-info">Signed in as {{handle}}</div> 346 - <form method="POST" action="/admin/logout"> 347 - <button type="submit">Sign out</button> 348 - </form> 349 - </div> 350 - </aside> 123 + {{> admin/partials/sidebar.hbs}} 351 124 352 125 <main class="main"> 353 - {{#if flash_success}} 354 - <div class="flash-success">{{flash_success}}</div> 355 - {{/if}} 356 - {{#if flash_error}} 357 - <div class="flash-error">{{flash_error}}</div> 358 - {{/if}} 126 + {{> admin/partials/flash.hbs}} 359 127 360 128 <h1 class="page-title">Create Account</h1> 361 129
+3 -212
html_templates/admin/dashboard.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Dashboard - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 154 - 155 - .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 - margin-bottom: 24px; 159 - } 160 - 161 - .flash-success { 162 - background: rgba(22, 163, 74, 0.1); 163 - color: var(--success-color); 164 - border: 1px solid rgba(22, 163, 74, 0.2); 165 - border-radius: 8px; 166 - padding: 10px 14px; 167 - font-size: 0.875rem; 168 - margin-bottom: 20px; 169 - } 170 - 171 - .flash-error { 172 - background: rgba(220, 38, 38, 0.1); 173 - color: var(--danger-color); 174 - border: 1px solid rgba(220, 38, 38, 0.2); 175 - border-radius: 8px; 176 - padding: 10px 14px; 177 - font-size: 0.875rem; 178 - margin-bottom: 20px; 179 - } 9 + {{> admin/partials/base_css.hbs}} 180 10 181 11 .cards { 182 12 display: grid; ··· 259 89 .detail-row .value a:hover { 260 90 text-decoration: underline; 261 91 } 262 - 263 - @media (max-width: 768px) { 264 - .sidebar { 265 - display: none; 266 - } 267 - 268 - .main { 269 - margin-left: 0; 270 - } 271 - } 272 92 </style> 273 93 </head> 274 94 <body> 275 95 <div class="layout"> 276 - <aside class="sidebar"> 277 - <div class="sidebar-title">{{pds_hostname}}</div> 278 - <div class="sidebar-subtitle">Admin Portal</div> 279 - <nav> 280 - <a href="/admin/dashboard" class="active">Dashboard</a> 281 - {{#if can_view_accounts}} 282 - <a href="/admin/accounts">Accounts</a> 283 - {{/if}} 284 - {{#if can_manage_invites}} 285 - <a href="/admin/invite-codes">Invite Codes</a> 286 - {{/if}} 287 - {{#if can_create_account}} 288 - <a href="/admin/create-account">Create Account</a> 289 - {{/if}} 290 - {{#if can_request_crawl}} 291 - <a href="/admin/request-crawl">Request Crawl</a> 292 - {{/if}} 293 - </nav> 294 - <div class="sidebar-footer"> 295 - <div class="session-info">Signed in as {{handle}}</div> 296 - <form method="POST" action="/admin/logout"> 297 - <button type="submit">Sign out</button> 298 - </form> 299 - </div> 300 - </aside> 96 + {{> admin/partials/sidebar.hbs}} 301 97 302 98 <main class="main"> 303 - {{#if flash_success}} 304 - <div class="flash-success">{{flash_success}}</div> 305 - {{/if}} 306 - {{#if flash_error}} 307 - <div class="flash-error">{{flash_error}}</div> 308 - {{/if}} 99 + {{> admin/partials/flash.hbs}} 309 100 310 101 <h1 class="page-title">Dashboard</h1> 311 102
+4 -144
html_templates/admin/error.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Error - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - } 20 - 21 - @media (prefers-color-scheme: dark) { 22 - :root { 23 - --brand-color: rgb(16, 131, 254); 24 - --primary-color: rgb(255, 255, 255); 25 - --secondary-color: rgb(133, 152, 173); 26 - --bg-primary-color: rgb(7, 10, 13); 27 - --bg-secondary-color: rgb(13, 18, 23); 28 - --border-color: rgb(40, 45, 55); 29 - } 30 - } 31 - 32 - :root.dark-mode { 33 - --brand-color: rgb(16, 131, 254); 34 - --primary-color: rgb(255, 255, 255); 35 - --secondary-color: rgb(133, 152, 173); 36 - --bg-primary-color: rgb(7, 10, 13); 37 - --bg-secondary-color: rgb(13, 18, 23); 38 - --border-color: rgb(40, 45, 55); 39 - } 40 - 41 - * { margin: 0; padding: 0; box-sizing: border-box; } 9 + {{> admin/partials/base_css.hbs}} 42 10 43 11 body { 44 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 45 - background: var(--bg-secondary-color); 46 - color: var(--primary-color); 47 - text-rendering: optimizeLegibility; 48 - -webkit-font-smoothing: antialiased; 49 12 min-height: 100vh; 50 13 } 51 14 52 - /* When logged in, use sidebar layout */ 53 - .layout { display: flex; min-height: 100vh; } 54 - 55 - .sidebar { 56 - width: 220px; 57 - background: var(--bg-primary-color); 58 - border-right: 1px solid var(--border-color); 59 - padding: 20px 0; 60 - position: fixed; 61 - top: 0; left: 0; bottom: 0; 62 - overflow-y: auto; 63 - display: flex; 64 - flex-direction: column; 65 - } 66 - 67 - .sidebar-title { 68 - font-size: 0.8125rem; 69 - font-weight: 700; 70 - padding: 0 20px; 71 - margin-bottom: 4px; 72 - white-space: nowrap; 73 - overflow: hidden; 74 - text-overflow: ellipsis; 75 - } 76 - 77 - .sidebar-subtitle { 78 - font-size: 0.6875rem; 79 - color: var(--secondary-color); 80 - padding: 0 20px; 81 - margin-bottom: 20px; 82 - } 83 - 84 - .sidebar nav { flex: 1; } 85 - 86 - .sidebar nav a { 87 - display: block; 88 - padding: 8px 20px; 89 - font-size: 0.8125rem; 90 - color: var(--secondary-color); 91 - text-decoration: none; 92 - transition: background 0.1s, color 0.1s; 93 - } 94 - 95 - .sidebar nav a:hover { 96 - background: var(--bg-secondary-color); 97 - color: var(--primary-color); 98 - } 99 - 100 - .sidebar-footer { 101 - padding: 16px 20px 0; 102 - border-top: 1px solid var(--border-color); 103 - margin-top: 16px; 104 - } 105 - 106 - .sidebar-footer .session-info { 107 - font-size: 0.75rem; 108 - color: var(--secondary-color); 109 - margin-bottom: 8px; 110 - } 111 - 112 - .sidebar-footer form { display: inline; } 113 - 114 - .sidebar-footer button { 115 - background: none; 116 - border: none; 117 - font-size: 0.75rem; 118 - color: var(--secondary-color); 119 - cursor: pointer; 120 - padding: 0; 121 - text-decoration: underline; 122 - } 123 - 124 - .sidebar-footer button:hover { color: var(--primary-color); } 125 - 126 - .main { 127 - margin-left: 220px; 128 - flex: 1; 129 - padding: 32px; 130 - max-width: 960px; 131 - } 132 - 133 15 /* Standalone centered layout (when not logged in) */ 134 16 .centered { 135 17 display: flex; ··· 184 66 transition: opacity 0.15s; 185 67 } 186 68 187 - .error-link:hover { opacity: 0.85; } 188 - 189 - @media (max-width: 768px) { 190 - .sidebar { display: none; } 191 - .main { margin-left: 0; } 69 + .error-link:hover { 70 + opacity: 0.85; 192 71 } 193 72 </style> 194 73 </head> ··· 196 75 {{#if handle}} 197 76 {{!-- Logged-in user: show sidebar layout --}} 198 77 <div class="layout"> 199 - <aside class="sidebar"> 200 - <div class="sidebar-title">{{pds_hostname}}</div> 201 - <div class="sidebar-subtitle">Admin Portal</div> 202 - <nav> 203 - <a href="/admin/">Dashboard</a> 204 - <a href="/admin/accounts">Accounts</a> 205 - {{#if can_manage_invites}} 206 - <a href="/admin/invite-codes">Invite Codes</a> 207 - {{/if}} 208 - {{#if can_create_account}} 209 - <a href="/admin/create-account">Create Account</a> 210 - {{/if}} 211 - </nav> 212 - <div class="sidebar-footer"> 213 - <div class="session-info">Signed in as {{handle}}</div> 214 - <form method="POST" action="/admin/logout"> 215 - <button type="submit">Sign out</button> 216 - </form> 217 - </div> 218 - </aside> 78 + {{> admin/partials/sidebar.hbs}} 219 79 220 80 <main class="main"> 221 81 <div class="error-card" style="text-align:center; margin: 60px auto;">
+3 -235
html_templates/admin/invite_codes.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Invite Codes - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 154 - 155 - .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 - margin-bottom: 24px; 159 - } 160 - 161 - .flash-success { 162 - background: rgba(22, 163, 74, 0.1); 163 - color: var(--success-color); 164 - border: 1px solid rgba(22, 163, 74, 0.2); 165 - border-radius: 8px; 166 - padding: 10px 14px; 167 - font-size: 0.875rem; 168 - margin-bottom: 20px; 169 - } 170 - 171 - .flash-error { 172 - background: rgba(220, 38, 38, 0.1); 173 - color: var(--danger-color); 174 - border: 1px solid rgba(220, 38, 38, 0.2); 175 - border-radius: 8px; 176 - padding: 10px 14px; 177 - font-size: 0.875rem; 178 - margin-bottom: 20px; 179 - } 9 + {{> admin/partials/base_css.hbs}} 180 10 181 11 .create-form { 182 12 display: flex; ··· 212 42 border-color: var(--brand-color); 213 43 } 214 44 215 - .btn { 216 - display: inline-flex; 217 - align-items: center; 218 - justify-content: center; 219 - padding: 10px 20px; 220 - font-size: 0.875rem; 221 - font-weight: 500; 222 - border: none; 223 - border-radius: 8px; 224 - cursor: pointer; 225 - transition: opacity 0.15s; 226 - text-decoration: none; 227 - } 228 - 229 - .btn:hover { 230 - opacity: 0.85; 231 - } 232 - 233 - .btn-primary { 234 - background: var(--brand-color); 235 - color: #fff; 236 - } 237 - 238 45 .btn-small { 239 46 padding: 6px 12px; 240 47 font-size: 0.75rem; ··· 353 160 .load-more a:hover { 354 161 text-decoration: underline; 355 162 } 356 - 357 - @media (max-width: 768px) { 358 - .sidebar { 359 - display: none; 360 - } 361 - 362 - .main { 363 - margin-left: 0; 364 - } 365 - } 366 163 </style> 367 164 </head> 368 165 <body> 369 166 <div class="layout"> 370 - <aside class="sidebar"> 371 - <div class="sidebar-title">{{pds_hostname}}</div> 372 - <div class="sidebar-subtitle">Admin Portal</div> 373 - <nav> 374 - <a href="/admin/dashboard">Dashboard</a> 375 - {{#if can_view_accounts}} 376 - <a href="/admin/accounts">Accounts</a> 377 - {{/if}} 378 - {{#if can_manage_invites}} 379 - <a href="/admin/invite-codes" class="active">Invite Codes</a> 380 - {{/if}} 381 - {{#if can_create_account}} 382 - <a href="/admin/create-account">Create Account</a> 383 - {{/if}} 384 - {{#if can_request_crawl}} 385 - <a href="/admin/request-crawl">Request Crawl</a> 386 - {{/if}} 387 - </nav> 388 - <div class="sidebar-footer"> 389 - <div class="session-info">Signed in as {{handle}}</div> 390 - <form method="POST" action="/admin/logout"> 391 - <button type="submit">Sign out</button> 392 - </form> 393 - </div> 394 - </aside> 167 + {{> admin/partials/sidebar.hbs}} 395 168 396 169 <main class="main"> 397 - {{#if flash_success}} 398 - <div class="flash-success">{{flash_success}}</div> 399 - {{/if}} 400 - {{#if flash_error}} 401 - <div class="flash-error">{{flash_error}}</div> 402 - {{/if}} 170 + {{> admin/partials/flash.hbs}} 403 171 404 172 <h1 class="page-title">Invite Codes</h1> 405 173
+1 -56
html_templates/admin/login.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Admin Login - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - } 20 - 21 - @media (prefers-color-scheme: dark) { 22 - :root { 23 - --brand-color: rgb(16, 131, 254); 24 - --primary-color: rgb(255, 255, 255); 25 - --secondary-color: rgb(133, 152, 173); 26 - --bg-primary-color: rgb(7, 10, 13); 27 - --bg-secondary-color: rgb(13, 18, 23); 28 - --border-color: rgb(40, 45, 55); 29 - } 30 - } 31 - 32 - :root.dark-mode { 33 - --brand-color: rgb(16, 131, 254); 34 - --primary-color: rgb(255, 255, 255); 35 - --secondary-color: rgb(133, 152, 173); 36 - --bg-primary-color: rgb(7, 10, 13); 37 - --bg-secondary-color: rgb(13, 18, 23); 38 - --border-color: rgb(40, 45, 55); 39 - } 40 - 41 - * { margin: 0; padding: 0; box-sizing: border-box; } 9 + {{> admin/partials/base_css.hbs}} 42 10 43 11 body { 44 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 45 - background: var(--bg-secondary-color); 46 - color: var(--primary-color); 47 - text-rendering: optimizeLegibility; 48 - -webkit-font-smoothing: antialiased; 49 12 min-height: 100vh; 50 13 display: flex; 51 14 align-items: center; ··· 104 67 border-color: var(--brand-color); 105 68 } 106 69 107 - .btn { 108 - display: inline-flex; 109 - align-items: center; 110 - justify-content: center; 111 - padding: 10px 20px; 112 - font-size: 0.875rem; 113 - font-weight: 500; 114 - border: none; 115 - border-radius: 8px; 116 - cursor: pointer; 117 - transition: opacity 0.15s; 118 - text-decoration: none; 119 - } 120 - 121 - .btn:hover { opacity: 0.85; } 122 - 123 70 .btn-primary { 124 - background: var(--brand-color); 125 - color: #fff; 126 71 width: 100%; 127 72 } 128 73
+216
html_templates/admin/partials/base_css.hbs
··· 1 + :root, 2 + :root.light-mode { 3 + --brand-color: rgb(16, 131, 254); 4 + --primary-color: rgb(7, 10, 13); 5 + --secondary-color: rgb(66, 86, 108); 6 + --bg-primary-color: rgb(255, 255, 255); 7 + --bg-secondary-color: rgb(240, 242, 245); 8 + --border-color: rgb(220, 225, 230); 9 + --danger-color: rgb(220, 38, 38); 10 + --success-color: rgb(22, 163, 74); 11 + --warning-color: rgb(234, 179, 8); 12 + --table-stripe: rgba(0, 0, 0, 0.02); 13 + } 14 + 15 + @media (prefers-color-scheme: dark) { 16 + :root { 17 + --brand-color: rgb(16, 131, 254); 18 + --primary-color: rgb(255, 255, 255); 19 + --secondary-color: rgb(133, 152, 173); 20 + --bg-primary-color: rgb(7, 10, 13); 21 + --bg-secondary-color: rgb(13, 18, 23); 22 + --border-color: rgb(40, 45, 55); 23 + --table-stripe: rgba(255, 255, 255, 0.02); 24 + } 25 + } 26 + 27 + :root.dark-mode { 28 + --brand-color: rgb(16, 131, 254); 29 + --primary-color: rgb(255, 255, 255); 30 + --secondary-color: rgb(133, 152, 173); 31 + --bg-primary-color: rgb(7, 10, 13); 32 + --bg-secondary-color: rgb(13, 18, 23); 33 + --border-color: rgb(40, 45, 55); 34 + --table-stripe: rgba(255, 255, 255, 0.02); 35 + } 36 + 37 + * { 38 + margin: 0; 39 + padding: 0; 40 + box-sizing: border-box; 41 + } 42 + 43 + body { 44 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 45 + background: var(--bg-secondary-color); 46 + color: var(--primary-color); 47 + text-rendering: optimizeLegibility; 48 + -webkit-font-smoothing: antialiased; 49 + } 50 + 51 + .layout { 52 + display: flex; 53 + min-height: 100vh; 54 + } 55 + 56 + .sidebar { 57 + width: 220px; 58 + background: var(--bg-primary-color); 59 + border-right: 1px solid var(--border-color); 60 + padding: 20px 0; 61 + position: fixed; 62 + top: 0; 63 + left: 0; 64 + bottom: 0; 65 + overflow-y: auto; 66 + display: flex; 67 + flex-direction: column; 68 + } 69 + 70 + .sidebar-title { 71 + font-size: 0.8125rem; 72 + font-weight: 700; 73 + padding: 0 20px; 74 + margin-bottom: 4px; 75 + white-space: nowrap; 76 + overflow: hidden; 77 + text-overflow: ellipsis; 78 + } 79 + 80 + .sidebar-subtitle { 81 + font-size: 0.6875rem; 82 + color: var(--secondary-color); 83 + padding: 0 20px; 84 + margin-bottom: 20px; 85 + } 86 + 87 + .sidebar nav { 88 + flex: 1; 89 + } 90 + 91 + .sidebar nav a { 92 + display: block; 93 + padding: 8px 20px; 94 + font-size: 0.8125rem; 95 + color: var(--secondary-color); 96 + text-decoration: none; 97 + transition: background 0.1s, color 0.1s; 98 + } 99 + 100 + .sidebar nav a:hover { 101 + background: var(--bg-secondary-color); 102 + color: var(--primary-color); 103 + } 104 + 105 + .sidebar nav a.active { 106 + color: var(--brand-color); 107 + font-weight: 500; 108 + } 109 + 110 + .sidebar-footer { 111 + padding: 16px 20px 0; 112 + border-top: 1px solid var(--border-color); 113 + margin-top: 16px; 114 + } 115 + 116 + .sidebar-footer .session-info { 117 + font-size: 0.75rem; 118 + color: var(--secondary-color); 119 + margin-bottom: 8px; 120 + } 121 + 122 + .sidebar-footer form { 123 + display: inline; 124 + } 125 + 126 + .sidebar-footer button { 127 + background: none; 128 + border: none; 129 + font-size: 0.75rem; 130 + color: var(--secondary-color); 131 + cursor: pointer; 132 + padding: 0; 133 + text-decoration: underline; 134 + } 135 + 136 + .sidebar-footer button:hover { 137 + color: var(--primary-color); 138 + } 139 + 140 + .main { 141 + margin-left: 220px; 142 + flex: 1; 143 + padding: 32px; 144 + max-width: 960px; 145 + } 146 + 147 + .page-title { 148 + font-size: 1.5rem; 149 + font-weight: 700; 150 + margin-bottom: 24px; 151 + } 152 + 153 + .flash-success { 154 + background: rgba(22, 163, 74, 0.1); 155 + color: var(--success-color); 156 + border: 1px solid rgba(22, 163, 74, 0.2); 157 + border-radius: 8px; 158 + padding: 10px 14px; 159 + font-size: 0.875rem; 160 + margin-bottom: 20px; 161 + } 162 + 163 + .flash-error { 164 + background: rgba(220, 38, 38, 0.1); 165 + color: var(--danger-color); 166 + border: 1px solid rgba(220, 38, 38, 0.2); 167 + border-radius: 8px; 168 + padding: 10px 14px; 169 + font-size: 0.875rem; 170 + margin-bottom: 20px; 171 + } 172 + 173 + .btn { 174 + display: inline-flex; 175 + align-items: center; 176 + justify-content: center; 177 + padding: 10px 20px; 178 + font-size: 0.875rem; 179 + font-weight: 500; 180 + border: none; 181 + border-radius: 8px; 182 + cursor: pointer; 183 + transition: opacity 0.15s; 184 + text-decoration: none; 185 + } 186 + 187 + .btn:hover { 188 + opacity: 0.85; 189 + } 190 + 191 + .btn-primary { 192 + background: var(--brand-color); 193 + color: #fff; 194 + } 195 + 196 + .btn-danger { 197 + background: var(--danger-color); 198 + color: #fff; 199 + border-color: var(--danger-color); 200 + } 201 + 202 + .btn-warning { 203 + background: var(--warning-color); 204 + color: #000; 205 + border-color: var(--warning-color); 206 + } 207 + 208 + @media (max-width: 768px) { 209 + .sidebar { 210 + display: none; 211 + } 212 + 213 + .main { 214 + margin-left: 0; 215 + } 216 + }
+6
html_templates/admin/partials/flash.hbs
··· 1 + {{#if flash_success}} 2 + <div class="flash-success">{{flash_success}}</div> 3 + {{/if}} 4 + {{#if flash_error}} 5 + <div class="flash-error">{{flash_error}}</div> 6 + {{/if}}
+25
html_templates/admin/partials/sidebar.hbs
··· 1 + <aside class="sidebar"> 2 + <div class="sidebar-title">{{pds_hostname}}</div> 3 + <div class="sidebar-subtitle">Admin Portal</div> 4 + <nav> 5 + <a href="/admin/dashboard" {{#if (eq active_page "dashboard")}}class="active"{{/if}}>Dashboard</a> 6 + {{#if can_view_accounts}} 7 + <a href="/admin/accounts" {{#if (eq active_page "accounts")}}class="active"{{/if}}>Accounts</a> 8 + {{/if}} 9 + {{#if can_manage_invites}} 10 + <a href="/admin/invite-codes" {{#if (eq active_page "invite_codes")}}class="active"{{/if}}>Invite Codes</a> 11 + {{/if}} 12 + {{#if can_create_account}} 13 + <a href="/admin/create-account" {{#if (eq active_page "create_account")}}class="active"{{/if}}>Create Account</a> 14 + {{/if}} 15 + {{#if can_request_crawl}} 16 + <a href="/admin/request-crawl" {{#if (eq active_page "request_crawl")}}class="active"{{/if}}>Request Crawl</a> 17 + {{/if}} 18 + </nav> 19 + <div class="sidebar-footer"> 20 + <div class="session-info">Signed in as {{handle}}</div> 21 + <form method="POST" action="/admin/logout"> 22 + <button type="submit">Sign out</button> 23 + </form> 24 + </div> 25 + </aside>
+3 -231
html_templates/admin/request_crawl.hbs
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Request Crawl - {{pds_hostname}}</title> 8 8 <style> 9 - :root, 10 - :root.light-mode { 11 - --brand-color: rgb(16, 131, 254); 12 - --primary-color: rgb(7, 10, 13); 13 - --secondary-color: rgb(66, 86, 108); 14 - --bg-primary-color: rgb(255, 255, 255); 15 - --bg-secondary-color: rgb(240, 242, 245); 16 - --border-color: rgb(220, 225, 230); 17 - --danger-color: rgb(220, 38, 38); 18 - --success-color: rgb(22, 163, 74); 19 - --warning-color: rgb(234, 179, 8); 20 - --table-stripe: rgba(0, 0, 0, 0.02); 21 - } 22 - 23 - @media (prefers-color-scheme: dark) { 24 - :root { 25 - --brand-color: rgb(16, 131, 254); 26 - --primary-color: rgb(255, 255, 255); 27 - --secondary-color: rgb(133, 152, 173); 28 - --bg-primary-color: rgb(7, 10, 13); 29 - --bg-secondary-color: rgb(13, 18, 23); 30 - --border-color: rgb(40, 45, 55); 31 - --table-stripe: rgba(255, 255, 255, 0.02); 32 - } 33 - } 34 - 35 - :root.dark-mode { 36 - --brand-color: rgb(16, 131, 254); 37 - --primary-color: rgb(255, 255, 255); 38 - --secondary-color: rgb(133, 152, 173); 39 - --bg-primary-color: rgb(7, 10, 13); 40 - --bg-secondary-color: rgb(13, 18, 23); 41 - --border-color: rgb(40, 45, 55); 42 - --table-stripe: rgba(255, 255, 255, 0.02); 43 - } 44 - 45 - * { 46 - margin: 0; 47 - padding: 0; 48 - box-sizing: border-box; 49 - } 50 - 51 - body { 52 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 53 - background: var(--bg-secondary-color); 54 - color: var(--primary-color); 55 - text-rendering: optimizeLegibility; 56 - -webkit-font-smoothing: antialiased; 57 - } 58 - 59 - .layout { 60 - display: flex; 61 - min-height: 100vh; 62 - } 63 - 64 - .sidebar { 65 - width: 220px; 66 - background: var(--bg-primary-color); 67 - border-right: 1px solid var(--border-color); 68 - padding: 20px 0; 69 - position: fixed; 70 - top: 0; 71 - left: 0; 72 - bottom: 0; 73 - overflow-y: auto; 74 - display: flex; 75 - flex-direction: column; 76 - } 77 - 78 - .sidebar-title { 79 - font-size: 0.8125rem; 80 - font-weight: 700; 81 - padding: 0 20px; 82 - margin-bottom: 4px; 83 - white-space: nowrap; 84 - overflow: hidden; 85 - text-overflow: ellipsis; 86 - } 87 - 88 - .sidebar-subtitle { 89 - font-size: 0.6875rem; 90 - color: var(--secondary-color); 91 - padding: 0 20px; 92 - margin-bottom: 20px; 93 - } 94 - 95 - .sidebar nav { 96 - flex: 1; 97 - } 98 - 99 - .sidebar nav a { 100 - display: block; 101 - padding: 8px 20px; 102 - font-size: 0.8125rem; 103 - color: var(--secondary-color); 104 - text-decoration: none; 105 - transition: background 0.1s, color 0.1s; 106 - } 107 - 108 - .sidebar nav a:hover { 109 - background: var(--bg-secondary-color); 110 - color: var(--primary-color); 111 - } 112 - 113 - .sidebar nav a.active { 114 - color: var(--brand-color); 115 - font-weight: 500; 116 - } 117 - 118 - .sidebar-footer { 119 - padding: 16px 20px 0; 120 - border-top: 1px solid var(--border-color); 121 - margin-top: 16px; 122 - } 123 - 124 - .sidebar-footer .session-info { 125 - font-size: 0.75rem; 126 - color: var(--secondary-color); 127 - margin-bottom: 8px; 128 - } 129 - 130 - .sidebar-footer form { 131 - display: inline; 132 - } 133 - 134 - .sidebar-footer button { 135 - background: none; 136 - border: none; 137 - font-size: 0.75rem; 138 - color: var(--secondary-color); 139 - cursor: pointer; 140 - padding: 0; 141 - text-decoration: underline; 142 - } 143 - 144 - .sidebar-footer button:hover { 145 - color: var(--primary-color); 146 - } 147 - 148 - .main { 149 - margin-left: 220px; 150 - flex: 1; 151 - padding: 32px; 152 - max-width: 960px; 153 - } 9 + {{> admin/partials/base_css.hbs}} 154 10 155 11 .page-title { 156 - font-size: 1.5rem; 157 - font-weight: 700; 158 12 margin-bottom: 8px; 159 13 } 160 14 ··· 162 16 font-size: 0.875rem; 163 17 color: var(--secondary-color); 164 18 margin-bottom: 24px; 165 - } 166 - 167 - .flash-success { 168 - background: rgba(22, 163, 74, 0.1); 169 - color: var(--success-color); 170 - border: 1px solid rgba(22, 163, 74, 0.2); 171 - border-radius: 8px; 172 - padding: 10px 14px; 173 - font-size: 0.875rem; 174 - margin-bottom: 20px; 175 - } 176 - 177 - .flash-error { 178 - background: rgba(220, 38, 38, 0.1); 179 - color: var(--danger-color); 180 - border: 1px solid rgba(220, 38, 38, 0.2); 181 - border-radius: 8px; 182 - padding: 10px 14px; 183 - font-size: 0.875rem; 184 - margin-bottom: 20px; 185 19 } 186 20 187 21 .form-card { ··· 225 59 color: var(--secondary-color); 226 60 margin-top: 4px; 227 61 } 228 - 229 - .btn { 230 - display: inline-flex; 231 - align-items: center; 232 - justify-content: center; 233 - padding: 10px 20px; 234 - font-size: 0.875rem; 235 - font-weight: 500; 236 - border: none; 237 - border-radius: 8px; 238 - cursor: pointer; 239 - transition: opacity 0.15s; 240 - text-decoration: none; 241 - } 242 - 243 - .btn:hover { 244 - opacity: 0.85; 245 - } 246 - 247 - .btn-primary { 248 - background: var(--brand-color); 249 - color: #fff; 250 - } 251 - 252 - @media (max-width: 768px) { 253 - .sidebar { 254 - display: none; 255 - } 256 - 257 - .main { 258 - margin-left: 0; 259 - } 260 - } 261 62 </style> 262 63 </head> 263 64 <body> 264 65 <div class="layout"> 265 - <aside class="sidebar"> 266 - <div class="sidebar-title">{{pds_hostname}}</div> 267 - <div class="sidebar-subtitle">Admin Portal</div> 268 - <nav> 269 - <a href="/admin/dashboard">Dashboard</a> 270 - {{#if can_view_accounts}} 271 - <a href="/admin/accounts">Accounts</a> 272 - {{/if}} 273 - {{#if can_manage_invites}} 274 - <a href="/admin/invite-codes">Invite Codes</a> 275 - {{/if}} 276 - {{#if can_create_account}} 277 - <a href="/admin/create-account">Create Account</a> 278 - {{/if}} 279 - {{#if can_request_crawl}} 280 - <a href="/admin/request-crawl" class="active">Request Crawl</a> 281 - {{/if}} 282 - </nav> 283 - <div class="sidebar-footer"> 284 - <div class="session-info">Signed in as {{handle}}</div> 285 - <form method="POST" action="/admin/logout"> 286 - <button type="submit">Sign out</button> 287 - </form> 288 - </div> 289 - </aside> 66 + {{> admin/partials/sidebar.hbs}} 290 67 291 68 <main class="main"> 292 - {{#if flash_success}} 293 - <div class="flash-success">{{flash_success}}</div> 294 - {{/if}} 295 - {{#if flash_error}} 296 - <div class="flash-error">{{flash_error}}</div> 297 - {{/if}} 69 + {{> admin/partials/flash.hbs}} 298 70 299 71 <h1 class="page-title">Request Crawl</h1> 300 72 <p class="page-description">Request a relay to crawl this PDS. This sends your PDS hostname to the relay so it
+21 -19
src/main.rs
··· 477 477 } 478 478 479 479 // Background cleanup for admin sessions 480 - let cleanup_pool = state.pds_gatekeeper_pool.clone(); 481 480 let admin_enabled = state.admin_rbac_config.is_some(); 482 - let admin_session_ttl_in_mins = state.app_config.admin_session_ttl_hours * 60; 483 - tokio::spawn(async move { 484 - let mut interval = tokio::time::interval(Duration::from_secs(300)); 485 - loop { 486 - interval.tick().await; 487 - if admin_enabled { 488 - if let Err(e) = admin::session::cleanup_expired_sessions(&cleanup_pool).await { 489 - tracing::error!("Failed to cleanup expired admin sessions: {}", e); 490 - } 491 - if let Err(e) = admin::store::cleanup_stale_auth_requests( 492 - &cleanup_pool, 493 - admin_session_ttl_in_mins as i64, 494 - ) 495 - .await 496 - { 497 - tracing::error!("Failed to cleanup stale OAuth auth requests: {}", e); 481 + if admin_enabled { 482 + let admin_session_ttl_in_mins = state.app_config.admin_session_ttl_hours * 60; 483 + let cleanup_pool = state.pds_gatekeeper_pool.clone(); 484 + tokio::spawn(async move { 485 + let mut interval = tokio::time::interval(Duration::from_secs(300)); 486 + loop { 487 + interval.tick().await; 488 + if admin_enabled { 489 + if let Err(e) = admin::session::cleanup_expired_sessions(&cleanup_pool).await { 490 + tracing::error!("Failed to cleanup expired admin sessions: {}", e); 491 + } 492 + if let Err(e) = admin::store::cleanup_stale_auth_requests( 493 + &cleanup_pool, 494 + admin_session_ttl_in_mins as i64, 495 + ) 496 + .await 497 + { 498 + tracing::error!("Failed to cleanup stale OAuth auth requests: {}", e); 499 + } 498 500 } 499 501 } 500 - } 501 - }); 502 + }); 503 + } 502 504 503 505 let request_logging = env::var("GATEKEEPER_REQUEST_LOGGING") 504 506 .map(|v| v.eq_ignore_ascii_case("true") || v == "1")