Microservice to bring 2FA to self hosted PDSes

moved to a css file

+909 -1016
+1 -1
Cargo.toml
··· 48 48 p256 = { version = "0.13", features = ["ecdsa", "jwk"] } 49 49 jose-jwk = { version = "0.1", features = ["p256"] } 50 50 base64 = "0.22" 51 - url = "2" 51 +
+2 -169
html_templates/admin/account_detail.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>{{account.handle}} - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .page-title { 12 - margin-bottom: 4px; 13 - } 14 - 15 - .page-subtitle { 16 - font-size: 0.8125rem; 17 - color: var(--secondary-color); 18 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 19 - margin-bottom: 24px; 20 - word-break: break-all; 21 - } 22 - 23 - .detail-section { 24 - background: var(--bg-primary-color); 25 - border: 1px solid var(--border-color); 26 - border-radius: 10px; 27 - padding: 20px; 28 - margin-bottom: 16px; 29 - } 30 - 31 - .detail-section h3 { 32 - font-size: 0.875rem; 33 - font-weight: 600; 34 - margin-bottom: 12px; 35 - } 36 - 37 - .detail-row { 38 - display: flex; 39 - justify-content: space-between; 40 - align-items: center; 41 - padding: 8px 0; 42 - font-size: 0.8125rem; 43 - border-bottom: 1px solid var(--border-color); 44 - } 45 - 46 - .detail-row:last-child { 47 - border-bottom: none; 48 - } 49 - 50 - .detail-row .label { 51 - color: var(--secondary-color); 52 - flex-shrink: 0; 53 - } 54 - 55 - .detail-row .value { 56 - font-weight: 500; 57 - word-break: break-all; 58 - text-align: right; 59 - max-width: 65%; 60 - } 61 - 62 - .badge { 63 - display: inline-block; 64 - padding: 2px 8px; 65 - border-radius: 4px; 66 - font-size: 0.75rem; 67 - font-weight: 500; 68 - } 69 - 70 - .badge-success { 71 - background: rgba(22, 163, 74, 0.1); 72 - color: var(--success-color); 73 - } 74 - 75 - .badge-danger { 76 - background: rgba(220, 38, 38, 0.1); 77 - color: var(--danger-color); 78 - } 79 - 80 - .badge-warning { 81 - background: rgba(234, 179, 8, 0.1); 82 - color: var(--warning-color); 83 - } 84 - 85 - .actions { 86 - display: flex; 87 - flex-wrap: wrap; 88 - gap: 8px; 89 - margin-top: 8px; 90 - } 91 - 92 - .actions form { 93 - display: inline; 94 - } 95 - 96 - .btn { 97 - padding: 8px 16px; 98 - font-size: 0.8125rem; 99 - border: 1px solid var(--border-color); 100 - background: var(--bg-primary-color); 101 - color: var(--primary-color); 102 - } 103 - 104 - .btn-primary { 105 - border-color: var(--brand-color); 106 - } 107 - 108 - .btn-danger { 109 - border-color: var(--danger-color); 110 - } 111 - 112 - .btn-warning { 113 - border-color: var(--warning-color); 114 - } 115 - 116 - .password-box { 117 - background: rgba(22, 163, 74, 0.08); 118 - border: 1px solid rgba(22, 163, 74, 0.2); 119 - border-radius: 10px; 120 - padding: 16px 20px; 121 - margin-bottom: 16px; 122 - } 123 - 124 - .password-box .pw-label { 125 - font-size: 0.75rem; 126 - font-weight: 600; 127 - color: var(--success-color); 128 - margin-bottom: 6px; 129 - } 130 - 131 - .password-box .pw-value { 132 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 133 - font-size: 1rem; 134 - font-weight: 600; 135 - user-select: all; 136 - } 137 - 138 - .back-link { 139 - display: inline-block; 140 - color: var(--brand-color); 141 - text-decoration: none; 142 - font-size: 0.8125rem; 143 - margin-bottom: 16px; 144 - } 145 - 146 - .back-link:hover { 147 - text-decoration: underline; 148 - } 149 - 150 - .collection-list { 151 - max-height: 200px; 152 - overflow-y: auto; 153 - padding: 8px 0; 154 - } 155 - 156 - .collection-item { 157 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 158 - font-size: 0.75rem; 159 - padding: 4px 0; 160 - color: var(--secondary-color); 161 - border-bottom: 1px solid var(--border-color); 162 - } 163 - 164 - .collection-item:last-child { 165 - border-bottom: none; 166 - } 167 - 168 - .threat-sig { 169 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 170 - font-size: 0.75rem; 171 - padding: 4px 0; 172 - color: var(--secondary-color); 173 - } 174 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 175 9 </head> 176 10 <body> 177 11 <div class="layout"> ··· 288 122 {{#if repo_rev}} 289 123 <div class="detail-row"> 290 124 <span class="label">Revision</span> 291 - <span class="value" 292 - style="font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.75rem;">{{repo_rev}}</span> 125 + <span class="value mono-text">{{repo_rev}}</span> 293 126 </div> 294 127 {{/if}} 295 128 </div>
+1 -86
html_templates/admin/accounts.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Accounts - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .search-form { 12 - display: flex; 13 - gap: 8px; 14 - margin-bottom: 24px; 15 - } 16 - 17 - .search-form input { 18 - flex: 1; 19 - padding: 10px 12px; 20 - font-size: 0.875rem; 21 - border: 1px solid var(--border-color); 22 - border-radius: 8px; 23 - background: var(--bg-primary-color); 24 - color: var(--primary-color); 25 - outline: none; 26 - } 27 - 28 - .search-form input:focus { 29 - border-color: var(--brand-color); 30 - } 31 - 32 - .table-container { 33 - background: var(--bg-primary-color); 34 - border: 1px solid var(--border-color); 35 - border-radius: 10px; 36 - overflow: hidden; 37 - } 38 - 39 - table { 40 - width: 100%; 41 - border-collapse: collapse; 42 - } 43 - 44 - thead th { 45 - text-align: left; 46 - padding: 12px 16px; 47 - font-size: 0.75rem; 48 - font-weight: 600; 49 - color: var(--secondary-color); 50 - text-transform: uppercase; 51 - letter-spacing: 0.5px; 52 - border-bottom: 1px solid var(--border-color); 53 - } 54 - 55 - tbody tr { 56 - border-bottom: 1px solid var(--border-color); 57 - } 58 - 59 - tbody tr:last-child { 60 - border-bottom: none; 61 - } 62 - 63 - tbody tr:nth-child(even) { 64 - background: var(--table-stripe); 65 - } 66 - 67 - tbody td { 68 - padding: 10px 16px; 69 - font-size: 0.8125rem; 70 - } 71 - 72 - tbody td a { 73 - color: var(--brand-color); 74 - text-decoration: none; 75 - } 76 - 77 - tbody td a:hover { 78 - text-decoration: underline; 79 - } 80 - 81 - .empty-state { 82 - text-align: center; 83 - padding: 40px 20px; 84 - color: var(--secondary-color); 85 - font-size: 0.875rem; 86 - } 87 - 88 - .did-cell { 89 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 90 - font-size: 0.75rem; 91 - color: var(--secondary-color); 92 - } 93 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 94 9 </head> 95 10 <body> 96 11 <div class="layout">
+1 -112
html_templates/admin/create_account.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Create Account - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .form-card { 12 - background: var(--bg-primary-color); 13 - border: 1px solid var(--border-color); 14 - border-radius: 10px; 15 - padding: 24px; 16 - max-width: 480px; 17 - } 18 - 19 - .form-group { 20 - margin-bottom: 16px; 21 - } 22 - 23 - .form-group label { 24 - display: block; 25 - font-size: 0.8125rem; 26 - font-weight: 500; 27 - margin-bottom: 6px; 28 - color: var(--primary-color); 29 - } 30 - 31 - .form-group input { 32 - width: 100%; 33 - padding: 10px 12px; 34 - font-size: 0.875rem; 35 - border: 1px solid var(--border-color); 36 - border-radius: 8px; 37 - background: var(--bg-primary-color); 38 - color: var(--primary-color); 39 - outline: none; 40 - transition: border-color 0.15s; 41 - } 42 - 43 - .form-group input:focus { 44 - border-color: var(--brand-color); 45 - } 46 - 47 - .form-group .hint { 48 - font-size: 0.75rem; 49 - color: var(--secondary-color); 50 - margin-top: 4px; 51 - } 52 - 53 - .success-card { 54 - background: var(--bg-primary-color); 55 - border: 1px solid var(--border-color); 56 - border-radius: 10px; 57 - padding: 24px; 58 - max-width: 480px; 59 - } 60 - 61 - .success-card h3 { 62 - font-size: 1rem; 63 - font-weight: 600; 64 - color: var(--success-color); 65 - margin-bottom: 16px; 66 - } 67 - 68 - .detail-row { 69 - display: flex; 70 - justify-content: space-between; 71 - align-items: center; 72 - padding: 8px 0; 73 - font-size: 0.8125rem; 74 - border-bottom: 1px solid var(--border-color); 75 - } 76 - 77 - .detail-row:last-child { 78 - border-bottom: none; 79 - } 80 - 81 - .detail-row .label { 82 - color: var(--secondary-color); 83 - } 84 - 85 - .detail-row .value { 86 - font-weight: 500; 87 - word-break: break-all; 88 - text-align: right; 89 - max-width: 65%; 90 - display: flex; 91 - align-items: center; 92 - gap: 6px; 93 - } 94 - 95 - .password-highlight { 96 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 97 - background: rgba(22, 163, 74, 0.08); 98 - padding: 2px 6px; 99 - border-radius: 4px; 100 - user-select: all; 101 - } 102 - 103 - .copy-btn { 104 - background: none; 105 - border: 1px solid var(--border-color); 106 - border-radius: 4px; 107 - padding: 2px 6px; 108 - font-size: 0.6875rem; 109 - cursor: pointer; 110 - color: var(--secondary-color); 111 - transition: color 0.15s, border-color 0.15s; 112 - white-space: nowrap; 113 - } 114 - 115 - .copy-btn:hover { 116 - color: var(--primary-color); 117 - border-color: var(--primary-color); 118 - } 119 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 120 9 </head> 121 10 <body> 122 11 <div class="layout">
+1 -85
html_templates/admin/dashboard.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Dashboard - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .cards { 12 - display: grid; 13 - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); 14 - gap: 16px; 15 - margin-bottom: 32px; 16 - } 17 - 18 - .card { 19 - background: var(--bg-primary-color); 20 - border: 1px solid var(--border-color); 21 - border-radius: 10px; 22 - padding: 20px; 23 - } 24 - 25 - .card-label { 26 - font-size: 0.75rem; 27 - font-weight: 500; 28 - color: var(--secondary-color); 29 - text-transform: uppercase; 30 - letter-spacing: 0.5px; 31 - margin-bottom: 6px; 32 - } 33 - 34 - .card-value { 35 - font-size: 1.25rem; 36 - font-weight: 600; 37 - } 38 - 39 - .card-value.success { 40 - color: var(--success-color); 41 - } 42 - 43 - .card-value.danger { 44 - color: var(--danger-color); 45 - } 46 - 47 - .detail-section { 48 - background: var(--bg-primary-color); 49 - border: 1px solid var(--border-color); 50 - border-radius: 10px; 51 - padding: 20px; 52 - margin-bottom: 16px; 53 - } 54 - 55 - .detail-section h3 { 56 - font-size: 0.875rem; 57 - font-weight: 600; 58 - margin-bottom: 12px; 59 - } 60 - 61 - .detail-row { 62 - display: flex; 63 - justify-content: space-between; 64 - padding: 6px 0; 65 - font-size: 0.8125rem; 66 - border-bottom: 1px solid var(--border-color); 67 - } 68 - 69 - .detail-row:last-child { 70 - border-bottom: none; 71 - } 72 - 73 - .detail-row .label { 74 - color: var(--secondary-color); 75 - } 76 - 77 - .detail-row .value { 78 - font-weight: 500; 79 - word-break: break-all; 80 - text-align: right; 81 - max-width: 60%; 82 - } 83 - 84 - .detail-row .value a { 85 - color: var(--brand-color); 86 - text-decoration: none; 87 - } 88 - 89 - .detail-row .value a:hover { 90 - text-decoration: underline; 91 - } 92 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 93 9 </head> 94 10 <body> 95 11 <div class="layout">
+2 -66
html_templates/admin/error.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Error - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - body { 12 - min-height: 100vh; 13 - } 14 - 15 - /* Standalone centered layout (when not logged in) */ 16 - .centered { 17 - display: flex; 18 - align-items: center; 19 - justify-content: center; 20 - min-height: 100vh; 21 - } 22 - 23 - .error-card { 24 - background: var(--bg-primary-color); 25 - border: 1px solid var(--border-color); 26 - border-radius: 12px; 27 - padding: 40px; 28 - text-align: center; 29 - max-width: 480px; 30 - width: 100%; 31 - margin: 20px; 32 - } 33 - 34 - .error-icon { 35 - font-size: 2.5rem; 36 - margin-bottom: 16px; 37 - color: var(--danger-color); 38 - } 39 - 40 - .error-title { 41 - font-size: 1.25rem; 42 - font-weight: 700; 43 - margin-bottom: 8px; 44 - } 45 - 46 - .error-message { 47 - font-size: 0.875rem; 48 - color: var(--secondary-color); 49 - margin-bottom: 24px; 50 - line-height: 1.5; 51 - } 52 - 53 - .error-link { 54 - display: inline-flex; 55 - align-items: center; 56 - justify-content: center; 57 - padding: 10px 20px; 58 - font-size: 0.875rem; 59 - font-weight: 500; 60 - border: none; 61 - border-radius: 8px; 62 - cursor: pointer; 63 - text-decoration: none; 64 - background: var(--brand-color); 65 - color: #fff; 66 - transition: opacity 0.15s; 67 - } 68 - 69 - .error-link:hover { 70 - opacity: 0.85; 71 - } 72 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 73 9 </head> 74 10 <body> 75 11 {{#if handle}} ··· 78 14 {{> admin/partials/sidebar.hbs}} 79 15 80 16 <main class="main"> 81 - <div class="error-card" style="text-align:center; margin: 60px auto;"> 17 + <div class="error-card error-card--inset"> 82 18 <div class="error-icon">!</div> 83 19 <div class="error-title">{{error_title}}</div> 84 20 <div class="error-message">{{error_message}}</div>
+2 -157
html_templates/admin/invite_codes.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Invite Codes - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .create-form { 12 - display: flex; 13 - gap: 8px; 14 - align-items: flex-end; 15 - margin-bottom: 24px; 16 - } 17 - 18 - .create-form .form-group { 19 - display: flex; 20 - flex-direction: column; 21 - gap: 4px; 22 - } 23 - 24 - .create-form label { 25 - font-size: 0.75rem; 26 - font-weight: 500; 27 - color: var(--secondary-color); 28 - } 29 - 30 - .create-form input[type="number"] { 31 - padding: 10px 12px; 32 - font-size: 0.875rem; 33 - border: 1px solid var(--border-color); 34 - border-radius: 8px; 35 - background: var(--bg-primary-color); 36 - color: var(--primary-color); 37 - outline: none; 38 - width: 120px; 39 - } 40 - 41 - .create-form input[type="number"]:focus { 42 - border-color: var(--brand-color); 43 - } 44 - 45 - .btn-small { 46 - padding: 6px 12px; 47 - font-size: 0.75rem; 48 - } 49 - 50 - .btn-outline-danger { 51 - background: transparent; 52 - color: var(--danger-color); 53 - border: 1px solid var(--danger-color); 54 - } 55 - 56 - .code-box { 57 - background: rgba(22, 163, 74, 0.08); 58 - border: 1px solid rgba(22, 163, 74, 0.2); 59 - border-radius: 10px; 60 - padding: 16px 20px; 61 - margin-bottom: 24px; 62 - } 63 - 64 - .code-box .code-label { 65 - font-size: 0.75rem; 66 - font-weight: 600; 67 - color: var(--success-color); 68 - margin-bottom: 6px; 69 - } 70 - 71 - .code-box .code-value { 72 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 73 - font-size: 1rem; 74 - font-weight: 600; 75 - user-select: all; 76 - } 77 - 78 - .table-container { 79 - background: var(--bg-primary-color); 80 - border: 1px solid var(--border-color); 81 - border-radius: 10px; 82 - overflow: hidden; 83 - } 84 - 85 - table { 86 - width: 100%; 87 - border-collapse: collapse; 88 - } 89 - 90 - thead th { 91 - text-align: left; 92 - padding: 12px 16px; 93 - font-size: 0.75rem; 94 - font-weight: 600; 95 - color: var(--secondary-color); 96 - text-transform: uppercase; 97 - letter-spacing: 0.5px; 98 - border-bottom: 1px solid var(--border-color); 99 - } 100 - 101 - tbody tr { 102 - border-bottom: 1px solid var(--border-color); 103 - } 104 - 105 - tbody tr:last-child { 106 - border-bottom: none; 107 - } 108 - 109 - tbody tr:nth-child(even) { 110 - background: var(--table-stripe); 111 - } 112 - 113 - tbody td { 114 - padding: 10px 16px; 115 - font-size: 0.8125rem; 116 - } 117 - 118 - .code-cell { 119 - font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 120 - font-size: 0.75rem; 121 - } 122 - 123 - .badge { 124 - display: inline-block; 125 - padding: 2px 8px; 126 - border-radius: 4px; 127 - font-size: 0.75rem; 128 - font-weight: 500; 129 - } 130 - 131 - .badge-success { 132 - background: rgba(22, 163, 74, 0.1); 133 - color: var(--success-color); 134 - } 135 - 136 - .badge-danger { 137 - background: rgba(220, 38, 38, 0.1); 138 - color: var(--danger-color); 139 - } 140 - 141 - .empty-state { 142 - text-align: center; 143 - padding: 40px 20px; 144 - color: var(--secondary-color); 145 - font-size: 0.875rem; 146 - } 147 - 148 - .load-more { 149 - text-align: center; 150 - padding: 16px; 151 - } 152 - 153 - .load-more a { 154 - color: var(--brand-color); 155 - text-decoration: none; 156 - font-size: 0.875rem; 157 - font-weight: 500; 158 - } 159 - 160 - .load-more a:hover { 161 - text-decoration: underline; 162 - } 163 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 164 9 </head> 165 10 <body> 166 11 <div class="layout"> ··· 221 66 <td> 222 67 {{#unless this.disabled}} 223 68 <form method="POST" action="/admin/invite-codes/disable" 224 - style="display:inline;"> 69 + class="inline-form"> 225 70 <input type="hidden" name="codes" value="{{this.code}}"/> 226 71 <button type="submit" class="btn btn-small btn-outline-danger">Disable 227 72 </button>
+1 -68
html_templates/admin/login.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Admin Login - {{pds_hostname}}</title> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 9 <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 10 body { 12 11 min-height: 100vh; 13 12 display: flex; 14 13 align-items: center; 15 14 justify-content: center; 16 - } 17 - 18 - .login-card { 19 - background: var(--bg-primary-color); 20 - border: 1px solid var(--border-color); 21 - border-radius: 12px; 22 - padding: 40px; 23 - width: 100%; 24 - max-width: 400px; 25 - margin: 20px; 26 - } 27 - 28 - .login-title { 29 - font-size: 1.5rem; 30 - font-weight: 700; 31 - text-align: center; 32 - margin-bottom: 4px; 33 - } 34 - 35 - .login-subtitle { 36 - font-size: 0.875rem; 37 - color: var(--secondary-color); 38 - text-align: center; 39 - margin-bottom: 32px; 40 - } 41 - 42 - .form-group { 43 - margin-bottom: 20px; 44 - } 45 - 46 - .form-group label { 47 - display: block; 48 - font-size: 0.875rem; 49 - font-weight: 500; 50 - margin-bottom: 6px; 51 - color: var(--primary-color); 52 - } 53 - 54 - .form-group input { 55 - width: 100%; 56 - padding: 10px 12px; 57 - font-size: 0.875rem; 58 - border: 1px solid var(--border-color); 59 - border-radius: 8px; 60 - background: var(--bg-primary-color); 61 - color: var(--primary-color); 62 - outline: none; 63 - transition: border-color 0.15s; 64 - } 65 - 66 - .form-group input:focus { 67 - border-color: var(--brand-color); 68 - } 69 - 70 - .btn-primary { 71 - width: 100%; 72 - } 73 - 74 - .error-msg { 75 - background: rgba(220, 38, 38, 0.1); 76 - color: var(--danger-color); 77 - border: 1px solid rgba(220, 38, 38, 0.2); 78 - border-radius: 8px; 79 - padding: 10px 14px; 80 - font-size: 0.875rem; 81 - margin-bottom: 20px; 82 15 } 83 16 </style> 84 17 </head>
-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 - }
+1 -55
html_templates/admin/request_crawl.hbs
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"/> 6 6 <meta name="referrer" content="origin-when-cross-origin"/> 7 7 <title>Request Crawl - {{pds_hostname}}</title> 8 - <style> 9 - {{> admin/partials/base_css.hbs}} 10 - 11 - .page-title { 12 - margin-bottom: 8px; 13 - } 14 - 15 - .page-description { 16 - font-size: 0.875rem; 17 - color: var(--secondary-color); 18 - margin-bottom: 24px; 19 - } 20 - 21 - .form-card { 22 - background: var(--bg-primary-color); 23 - border: 1px solid var(--border-color); 24 - border-radius: 10px; 25 - padding: 24px; 26 - max-width: 480px; 27 - } 28 - 29 - .form-group { 30 - margin-bottom: 16px; 31 - } 32 - 33 - .form-group label { 34 - display: block; 35 - font-size: 0.8125rem; 36 - font-weight: 500; 37 - margin-bottom: 6px; 38 - color: var(--primary-color); 39 - } 40 - 41 - .form-group input { 42 - width: 100%; 43 - padding: 10px 12px; 44 - font-size: 0.875rem; 45 - border: 1px solid var(--border-color); 46 - border-radius: 8px; 47 - background: var(--bg-primary-color); 48 - color: var(--primary-color); 49 - outline: none; 50 - transition: border-color 0.15s; 51 - } 52 - 53 - .form-group input:focus { 54 - border-color: var(--brand-color); 55 - } 56 - 57 - .form-group .hint { 58 - font-size: 0.75rem; 59 - color: var(--secondary-color); 60 - margin-top: 4px; 61 - } 62 - </style> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 63 9 </head> 64 10 <body> 65 11 <div class="layout">
+3 -1
src/admin/mod.rs
··· 4 4 pub mod rbac; 5 5 pub mod routes; 6 6 pub mod session; 7 + pub mod static_files; 7 8 pub mod store; 8 9 9 10 use axum::{Router, middleware as ax_middleware, routing::get, routing::post}; ··· 19 20 .route("/", get(routes::dashboard)) 20 21 .route("/login", get(oauth::get_login).post(oauth::post_login)) 21 22 .route("/oauth/callback", get(oauth::oauth_callback)) 22 - .route("/client-metadata.json", get(oauth::client_metadata_json)); 23 + .route("/client-metadata.json", get(oauth::client_metadata_json)) 24 + .route("/static/{*path}", get(static_files::serve_static)); 23 25 24 26 // Routes that DO require authentication (via admin_auth middleware) 25 27 let protected_routes = Router::new()
+46
src/admin/static_files.rs
··· 1 + use axum::{ 2 + extract::Path, 3 + http::{StatusCode, header}, 4 + response::{IntoResponse, Response}, 5 + }; 6 + 7 + use crate::StaticFiles; 8 + 9 + /// Serve embedded static files at /admin/static/*path 10 + pub async fn serve_static(Path(path): Path<String>) -> Response { 11 + let file = match StaticFiles::get(&path) { 12 + Some(file) => file, 13 + None => return StatusCode::NOT_FOUND.into_response(), 14 + }; 15 + 16 + let mime_type = mime_from_path(&path); 17 + 18 + ( 19 + [ 20 + (header::CONTENT_TYPE, mime_type), 21 + (header::CACHE_CONTROL, cache_control_value()), 22 + ], 23 + file.data.to_vec(), 24 + ) 25 + .into_response() 26 + } 27 + 28 + fn mime_from_path(path: &str) -> &'static str { 29 + match path.rsplit('.').next() { 30 + Some("css") => "text/css; charset=utf-8", 31 + Some("js") => "application/javascript; charset=utf-8", 32 + Some("json") => "application/json", 33 + Some("svg") => "image/svg+xml", 34 + Some("png") => "image/png", 35 + Some("jpg") | Some("jpeg") => "image/jpeg", 36 + _ => "application/octet-stream", 37 + } 38 + } 39 + 40 + fn cache_control_value() -> &'static str { 41 + if cfg!(debug_assertions) { 42 + "no-cache" 43 + } else { 44 + "public, max-age=3600, stale-while-revalidate=86400" 45 + } 46 + }
+4
src/main.rs
··· 62 62 #[include = "*.hbs"] 63 63 struct HtmlTemplates; 64 64 65 + #[derive(RustEmbed)] 66 + #[folder = "static"] 67 + pub struct StaticFiles; 68 + 65 69 /// Mostly the env variables that are used in the app 66 70 #[derive(Clone, Debug)] 67 71 pub struct AppConfig {
+844
static/css/admin.css
··· 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 + /* --- Reset & Base ------------------------------------------- */ 38 + 39 + * { 40 + margin: 0; 41 + padding: 0; 42 + box-sizing: border-box; 43 + } 44 + 45 + body { 46 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 47 + background: var(--bg-secondary-color); 48 + color: var(--primary-color); 49 + text-rendering: optimizeLegibility; 50 + -webkit-font-smoothing: antialiased; 51 + } 52 + 53 + /* --- Layout: Sidebar + Main --------------------------------- */ 54 + 55 + .layout { 56 + display: flex; 57 + min-height: 100vh; 58 + } 59 + 60 + .sidebar { 61 + width: 220px; 62 + background: var(--bg-primary-color); 63 + border-right: 1px solid var(--border-color); 64 + padding: 20px 0; 65 + position: fixed; 66 + top: 0; 67 + left: 0; 68 + bottom: 0; 69 + overflow-y: auto; 70 + display: flex; 71 + flex-direction: column; 72 + } 73 + 74 + .sidebar-title { 75 + font-size: 0.8125rem; 76 + font-weight: 700; 77 + padding: 0 20px; 78 + margin-bottom: 4px; 79 + white-space: nowrap; 80 + overflow: hidden; 81 + text-overflow: ellipsis; 82 + } 83 + 84 + .sidebar-subtitle { 85 + font-size: 0.6875rem; 86 + color: var(--secondary-color); 87 + padding: 0 20px; 88 + margin-bottom: 20px; 89 + } 90 + 91 + .sidebar nav { 92 + flex: 1; 93 + } 94 + 95 + .sidebar nav a { 96 + display: block; 97 + padding: 8px 20px; 98 + font-size: 0.8125rem; 99 + color: var(--secondary-color); 100 + text-decoration: none; 101 + transition: background 0.1s, color 0.1s; 102 + } 103 + 104 + .sidebar nav a:hover { 105 + background: var(--bg-secondary-color); 106 + color: var(--primary-color); 107 + } 108 + 109 + .sidebar nav a.active { 110 + color: var(--brand-color); 111 + font-weight: 500; 112 + } 113 + 114 + .sidebar-footer { 115 + padding: 16px 20px 0; 116 + border-top: 1px solid var(--border-color); 117 + margin-top: 16px; 118 + } 119 + 120 + .sidebar-footer .session-info { 121 + font-size: 0.75rem; 122 + color: var(--secondary-color); 123 + margin-bottom: 8px; 124 + } 125 + 126 + .sidebar-footer form { 127 + display: inline; 128 + } 129 + 130 + .sidebar-footer button { 131 + background: none; 132 + border: none; 133 + font-size: 0.75rem; 134 + color: var(--secondary-color); 135 + cursor: pointer; 136 + padding: 0; 137 + text-decoration: underline; 138 + } 139 + 140 + .sidebar-footer button:hover { 141 + color: var(--primary-color); 142 + } 143 + 144 + .main { 145 + margin-left: 220px; 146 + flex: 1; 147 + padding: 32px; 148 + max-width: 960px; 149 + } 150 + 151 + /* --- Common Components -------------------------------------- */ 152 + 153 + .page-title { 154 + font-size: 1.5rem; 155 + font-weight: 700; 156 + margin-bottom: 24px; 157 + } 158 + 159 + .page-subtitle { 160 + font-size: 0.8125rem; 161 + color: var(--secondary-color); 162 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 163 + margin-top: -20px; 164 + margin-bottom: 24px; 165 + word-break: break-all; 166 + } 167 + 168 + .page-description { 169 + font-size: 0.875rem; 170 + color: var(--secondary-color); 171 + margin-top: -16px; 172 + margin-bottom: 24px; 173 + } 174 + 175 + .flash-success { 176 + background: rgba(22, 163, 74, 0.1); 177 + color: var(--success-color); 178 + border: 1px solid rgba(22, 163, 74, 0.2); 179 + border-radius: 8px; 180 + padding: 10px 14px; 181 + font-size: 0.875rem; 182 + margin-bottom: 20px; 183 + } 184 + 185 + .flash-error { 186 + background: rgba(220, 38, 38, 0.1); 187 + color: var(--danger-color); 188 + border: 1px solid rgba(220, 38, 38, 0.2); 189 + border-radius: 8px; 190 + padding: 10px 14px; 191 + font-size: 0.875rem; 192 + margin-bottom: 20px; 193 + } 194 + 195 + /* --- Buttons ------------------------------------------------ */ 196 + 197 + .btn { 198 + display: inline-flex; 199 + align-items: center; 200 + justify-content: center; 201 + padding: 10px 20px; 202 + font-size: 0.875rem; 203 + font-weight: 500; 204 + border: none; 205 + border-radius: 8px; 206 + cursor: pointer; 207 + transition: opacity 0.15s; 208 + text-decoration: none; 209 + } 210 + 211 + .btn:hover { 212 + opacity: 0.85; 213 + } 214 + 215 + .btn-primary { 216 + background: var(--brand-color); 217 + color: #fff; 218 + } 219 + 220 + .btn-danger { 221 + background: var(--danger-color); 222 + color: #fff; 223 + border-color: var(--danger-color); 224 + } 225 + 226 + .btn-warning { 227 + background: var(--warning-color); 228 + color: #000; 229 + border-color: var(--warning-color); 230 + } 231 + 232 + .btn-small { 233 + padding: 6px 12px; 234 + font-size: 0.75rem; 235 + } 236 + 237 + .btn-outline-danger { 238 + background: transparent; 239 + color: var(--danger-color); 240 + border: 1px solid var(--danger-color); 241 + } 242 + 243 + /* --- Detail Sections ---------------------------------------- */ 244 + 245 + .detail-section { 246 + background: var(--bg-primary-color); 247 + border: 1px solid var(--border-color); 248 + border-radius: 10px; 249 + padding: 20px; 250 + margin-bottom: 16px; 251 + } 252 + 253 + .detail-section h3 { 254 + font-size: 0.875rem; 255 + font-weight: 600; 256 + margin-bottom: 12px; 257 + } 258 + 259 + .detail-row { 260 + display: flex; 261 + justify-content: space-between; 262 + align-items: center; 263 + padding: 8px 0; 264 + font-size: 0.8125rem; 265 + border-bottom: 1px solid var(--border-color); 266 + } 267 + 268 + .detail-row:last-child { 269 + border-bottom: none; 270 + } 271 + 272 + .detail-row .label { 273 + color: var(--secondary-color); 274 + flex-shrink: 0; 275 + } 276 + 277 + .detail-row .value { 278 + font-weight: 500; 279 + word-break: break-all; 280 + text-align: right; 281 + max-width: 65%; 282 + } 283 + 284 + .detail-row .value a { 285 + color: var(--brand-color); 286 + text-decoration: none; 287 + } 288 + 289 + .detail-row .value a:hover { 290 + text-decoration: underline; 291 + } 292 + 293 + /* --- Tables ------------------------------------------------- */ 294 + 295 + .table-container { 296 + background: var(--bg-primary-color); 297 + border: 1px solid var(--border-color); 298 + border-radius: 10px; 299 + overflow: hidden; 300 + } 301 + 302 + table { 303 + width: 100%; 304 + border-collapse: collapse; 305 + } 306 + 307 + thead th { 308 + text-align: left; 309 + padding: 12px 16px; 310 + font-size: 0.75rem; 311 + font-weight: 600; 312 + color: var(--secondary-color); 313 + text-transform: uppercase; 314 + letter-spacing: 0.5px; 315 + border-bottom: 1px solid var(--border-color); 316 + } 317 + 318 + tbody tr { 319 + border-bottom: 1px solid var(--border-color); 320 + } 321 + 322 + tbody tr:last-child { 323 + border-bottom: none; 324 + } 325 + 326 + tbody tr:nth-child(even) { 327 + background: var(--table-stripe); 328 + } 329 + 330 + tbody td { 331 + padding: 10px 16px; 332 + font-size: 0.8125rem; 333 + } 334 + 335 + tbody td a { 336 + color: var(--brand-color); 337 + text-decoration: none; 338 + } 339 + 340 + tbody td a:hover { 341 + text-decoration: underline; 342 + } 343 + 344 + /* --- Badges ------------------------------------------------- */ 345 + 346 + .badge { 347 + display: inline-block; 348 + padding: 2px 8px; 349 + border-radius: 4px; 350 + font-size: 0.75rem; 351 + font-weight: 500; 352 + } 353 + 354 + .badge-success { 355 + background: rgba(22, 163, 74, 0.1); 356 + color: var(--success-color); 357 + } 358 + 359 + .badge-danger { 360 + background: rgba(220, 38, 38, 0.1); 361 + color: var(--danger-color); 362 + } 363 + 364 + .badge-warning { 365 + background: rgba(234, 179, 8, 0.1); 366 + color: var(--warning-color); 367 + } 368 + 369 + /* --- Forms -------------------------------------------------- */ 370 + 371 + .form-card { 372 + background: var(--bg-primary-color); 373 + border: 1px solid var(--border-color); 374 + border-radius: 10px; 375 + padding: 24px; 376 + max-width: 480px; 377 + } 378 + 379 + .form-group { 380 + margin-bottom: 16px; 381 + } 382 + 383 + .form-group label { 384 + display: block; 385 + font-size: 0.8125rem; 386 + font-weight: 500; 387 + margin-bottom: 6px; 388 + color: var(--primary-color); 389 + } 390 + 391 + .form-group input { 392 + width: 100%; 393 + padding: 10px 12px; 394 + font-size: 0.875rem; 395 + border: 1px solid var(--border-color); 396 + border-radius: 8px; 397 + background: var(--bg-primary-color); 398 + color: var(--primary-color); 399 + outline: none; 400 + transition: border-color 0.15s; 401 + } 402 + 403 + .form-group input:focus { 404 + border-color: var(--brand-color); 405 + } 406 + 407 + .form-group .hint { 408 + font-size: 0.75rem; 409 + color: var(--secondary-color); 410 + margin-top: 4px; 411 + } 412 + 413 + .search-form { 414 + display: flex; 415 + gap: 8px; 416 + margin-bottom: 24px; 417 + } 418 + 419 + .search-form input { 420 + flex: 1; 421 + padding: 10px 12px; 422 + font-size: 0.875rem; 423 + border: 1px solid var(--border-color); 424 + border-radius: 8px; 425 + background: var(--bg-primary-color); 426 + color: var(--primary-color); 427 + outline: none; 428 + } 429 + 430 + .search-form input:focus { 431 + border-color: var(--brand-color); 432 + } 433 + 434 + .create-form { 435 + display: flex; 436 + gap: 8px; 437 + align-items: flex-end; 438 + margin-bottom: 24px; 439 + } 440 + 441 + .create-form .form-group { 442 + display: flex; 443 + flex-direction: column; 444 + gap: 4px; 445 + } 446 + 447 + .create-form label { 448 + font-size: 0.75rem; 449 + font-weight: 500; 450 + color: var(--secondary-color); 451 + } 452 + 453 + .create-form input[type="number"] { 454 + padding: 10px 12px; 455 + font-size: 0.875rem; 456 + border: 1px solid var(--border-color); 457 + border-radius: 8px; 458 + background: var(--bg-primary-color); 459 + color: var(--primary-color); 460 + outline: none; 461 + width: 120px; 462 + } 463 + 464 + .create-form input[type="number"]:focus { 465 + border-color: var(--brand-color); 466 + } 467 + 468 + /* --- Utility ------------------------------------------------ */ 469 + 470 + .mono-text { 471 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 472 + font-size: 0.75rem; 473 + } 474 + 475 + .inline-form { 476 + display: inline; 477 + } 478 + 479 + .empty-state { 480 + text-align: center; 481 + padding: 40px 20px; 482 + color: var(--secondary-color); 483 + font-size: 0.875rem; 484 + } 485 + 486 + .load-more { 487 + text-align: center; 488 + padding: 16px; 489 + } 490 + 491 + .load-more a { 492 + color: var(--brand-color); 493 + text-decoration: none; 494 + font-size: 0.875rem; 495 + font-weight: 500; 496 + } 497 + 498 + .load-more a:hover { 499 + text-decoration: underline; 500 + } 501 + 502 + /* --- Dashboard: Cards --------------------------------------- */ 503 + 504 + .cards { 505 + display: grid; 506 + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); 507 + gap: 16px; 508 + margin-bottom: 32px; 509 + } 510 + 511 + .card { 512 + background: var(--bg-primary-color); 513 + border: 1px solid var(--border-color); 514 + border-radius: 10px; 515 + padding: 20px; 516 + } 517 + 518 + .card-label { 519 + font-size: 0.75rem; 520 + font-weight: 500; 521 + color: var(--secondary-color); 522 + text-transform: uppercase; 523 + letter-spacing: 0.5px; 524 + margin-bottom: 6px; 525 + } 526 + 527 + .card-value { 528 + font-size: 1.25rem; 529 + font-weight: 600; 530 + } 531 + 532 + .card-value.success { 533 + color: var(--success-color); 534 + } 535 + 536 + .card-value.danger { 537 + color: var(--danger-color); 538 + } 539 + 540 + /* --- Accounts: DID Cell ------------------------------------- */ 541 + 542 + .did-cell { 543 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 544 + font-size: 0.75rem; 545 + color: var(--secondary-color); 546 + } 547 + 548 + /* --- Account Detail ----------------------------------------- */ 549 + 550 + .actions { 551 + display: flex; 552 + flex-wrap: wrap; 553 + gap: 8px; 554 + margin-top: 8px; 555 + } 556 + 557 + .actions form { 558 + display: inline; 559 + } 560 + 561 + .actions .btn { 562 + padding: 8px 16px; 563 + font-size: 0.8125rem; 564 + border: 1px solid var(--border-color); 565 + background: var(--bg-primary-color); 566 + color: var(--primary-color); 567 + } 568 + 569 + .actions .btn-primary { 570 + background: var(--brand-color); 571 + color: #fff; 572 + border-color: var(--brand-color); 573 + } 574 + 575 + .actions .btn-danger { 576 + background: var(--danger-color); 577 + color: #fff; 578 + border-color: var(--danger-color); 579 + } 580 + 581 + .actions .btn-warning { 582 + background: var(--warning-color); 583 + color: #000; 584 + border-color: var(--warning-color); 585 + } 586 + 587 + .password-box { 588 + background: rgba(22, 163, 74, 0.08); 589 + border: 1px solid rgba(22, 163, 74, 0.2); 590 + border-radius: 10px; 591 + padding: 16px 20px; 592 + margin-bottom: 16px; 593 + } 594 + 595 + .password-box .pw-label { 596 + font-size: 0.75rem; 597 + font-weight: 600; 598 + color: var(--success-color); 599 + margin-bottom: 6px; 600 + } 601 + 602 + .password-box .pw-value { 603 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 604 + font-size: 1rem; 605 + font-weight: 600; 606 + user-select: all; 607 + } 608 + 609 + .back-link { 610 + display: inline-block; 611 + color: var(--brand-color); 612 + text-decoration: none; 613 + font-size: 0.8125rem; 614 + margin-bottom: 16px; 615 + } 616 + 617 + .back-link:hover { 618 + text-decoration: underline; 619 + } 620 + 621 + .collection-list { 622 + max-height: 200px; 623 + overflow-y: auto; 624 + padding: 8px 0; 625 + } 626 + 627 + .collection-item { 628 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 629 + font-size: 0.75rem; 630 + padding: 4px 0; 631 + color: var(--secondary-color); 632 + border-bottom: 1px solid var(--border-color); 633 + } 634 + 635 + .collection-item:last-child { 636 + border-bottom: none; 637 + } 638 + 639 + .threat-sig { 640 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 641 + font-size: 0.75rem; 642 + padding: 4px 0; 643 + color: var(--secondary-color); 644 + } 645 + 646 + /* --- Create Account ----------------------------------------- */ 647 + 648 + .success-card { 649 + background: var(--bg-primary-color); 650 + border: 1px solid var(--border-color); 651 + border-radius: 10px; 652 + padding: 24px; 653 + max-width: 480px; 654 + } 655 + 656 + .success-card h3 { 657 + font-size: 1rem; 658 + font-weight: 600; 659 + color: var(--success-color); 660 + margin-bottom: 16px; 661 + } 662 + 663 + .success-card .detail-row .value { 664 + display: flex; 665 + align-items: center; 666 + gap: 6px; 667 + } 668 + 669 + .password-highlight { 670 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 671 + background: rgba(22, 163, 74, 0.08); 672 + padding: 2px 6px; 673 + border-radius: 4px; 674 + user-select: all; 675 + } 676 + 677 + .copy-btn { 678 + background: none; 679 + border: 1px solid var(--border-color); 680 + border-radius: 4px; 681 + padding: 2px 6px; 682 + font-size: 0.6875rem; 683 + cursor: pointer; 684 + color: var(--secondary-color); 685 + transition: color 0.15s, border-color 0.15s; 686 + white-space: nowrap; 687 + } 688 + 689 + .copy-btn:hover { 690 + color: var(--primary-color); 691 + border-color: var(--primary-color); 692 + } 693 + 694 + /* --- Invite Codes ------------------------------------------- */ 695 + 696 + .code-box { 697 + background: rgba(22, 163, 74, 0.08); 698 + border: 1px solid rgba(22, 163, 74, 0.2); 699 + border-radius: 10px; 700 + padding: 16px 20px; 701 + margin-bottom: 24px; 702 + } 703 + 704 + .code-box .code-label { 705 + font-size: 0.75rem; 706 + font-weight: 600; 707 + color: var(--success-color); 708 + margin-bottom: 6px; 709 + } 710 + 711 + .code-box .code-value { 712 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 713 + font-size: 1rem; 714 + font-weight: 600; 715 + user-select: all; 716 + } 717 + 718 + .code-cell { 719 + font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; 720 + font-size: 0.75rem; 721 + } 722 + 723 + /* --- Login Page --------------------------------------------- */ 724 + 725 + .login-card { 726 + background: var(--bg-primary-color); 727 + border: 1px solid var(--border-color); 728 + border-radius: 12px; 729 + padding: 40px; 730 + width: 100%; 731 + max-width: 400px; 732 + margin: 20px; 733 + } 734 + 735 + .login-title { 736 + font-size: 1.5rem; 737 + font-weight: 700; 738 + text-align: center; 739 + margin-bottom: 4px; 740 + } 741 + 742 + .login-subtitle { 743 + font-size: 0.875rem; 744 + color: var(--secondary-color); 745 + text-align: center; 746 + margin-bottom: 32px; 747 + } 748 + 749 + .login-card .form-group { 750 + margin-bottom: 20px; 751 + } 752 + 753 + .login-card .form-group label { 754 + font-size: 0.875rem; 755 + } 756 + 757 + .login-card .btn-primary { 758 + width: 100%; 759 + } 760 + 761 + .error-msg { 762 + background: rgba(220, 38, 38, 0.1); 763 + color: var(--danger-color); 764 + border: 1px solid rgba(220, 38, 38, 0.2); 765 + border-radius: 8px; 766 + padding: 10px 14px; 767 + font-size: 0.875rem; 768 + margin-bottom: 20px; 769 + } 770 + 771 + /* --- Error Page ---------------------------------------------- */ 772 + 773 + .centered { 774 + display: flex; 775 + align-items: center; 776 + justify-content: center; 777 + min-height: 100vh; 778 + } 779 + 780 + .error-card { 781 + background: var(--bg-primary-color); 782 + border: 1px solid var(--border-color); 783 + border-radius: 12px; 784 + padding: 40px; 785 + text-align: center; 786 + max-width: 480px; 787 + width: 100%; 788 + margin: 20px; 789 + } 790 + 791 + .error-card--inset { 792 + margin: 60px auto; 793 + } 794 + 795 + .error-icon { 796 + font-size: 2.5rem; 797 + margin-bottom: 16px; 798 + color: var(--danger-color); 799 + } 800 + 801 + .error-title { 802 + font-size: 1.25rem; 803 + font-weight: 700; 804 + margin-bottom: 8px; 805 + } 806 + 807 + .error-message { 808 + font-size: 0.875rem; 809 + color: var(--secondary-color); 810 + margin-bottom: 24px; 811 + line-height: 1.5; 812 + } 813 + 814 + .error-link { 815 + display: inline-flex; 816 + align-items: center; 817 + justify-content: center; 818 + padding: 10px 20px; 819 + font-size: 0.875rem; 820 + font-weight: 500; 821 + border: none; 822 + border-radius: 8px; 823 + cursor: pointer; 824 + text-decoration: none; 825 + background: var(--brand-color); 826 + color: #fff; 827 + transition: opacity 0.15s; 828 + } 829 + 830 + .error-link:hover { 831 + opacity: 0.85; 832 + } 833 + 834 + /* --- Responsive --------------------------------------------- */ 835 + 836 + @media (max-width: 768px) { 837 + .sidebar { 838 + display: none; 839 + } 840 + 841 + .main { 842 + margin-left: 0; 843 + } 844 + }