Signed by hanna SSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/store/src/users.rs +19 −0 crates/web/src/admin.rs +18 −2 crates/web/src/pages.rs +3 −0 crates/store/src/users.rs +19 −0 Expand 519 hidden lines 520 520 Ok ( updated > 0 ) 521 521 } 522 522 523 + 524 + 525 + 526 + 527 + 528 + pub async fn primary_email_verified ( & self , user_id : & str ) -> Result < bool , StoreError > { 529 + let row = sqlx:: query ( 530 + "SELECT verified_at FROM user_emails WHERE user_id = $1 AND is_primary = $2" , 531 + ) 532 + . bind ( user_id) 533 + . bind ( true ) 534 + . fetch_optional ( & self . pool) 535 + . await ?; 536 + let verified = row 537 + . and_then ( |r| r. try_get :: < Option < i64 > , _ > ( "verified_at" ) . ok ( ) . flatten ( ) ) 538 + . is_some ( ) ; 539 + Ok ( verified) 540 + } 541 + 523 542 524 543 525 544
crates/web/src/admin.rs +18 −2 Expand 161 hidden lines 162 162 let ( page, offset) = pager. resolve ( ) ; 163 163 let total = state. store. admin_counts ( ) . await ?. users; 164 164 let all = state. store. list_users_paged ( PAGE_SIZE , offset) . await ?; 165 + 166 + 167 + let mut verified: std:: collections:: HashSet < String > = std:: collections:: HashSet :: new ( ) ; 168 + for u in & all { 169 + if state 170 + . store 171 + . primary_email_verified ( & u. id) 172 + . await 173 + . unwrap_or ( false ) 174 + { 175 + verified. insert ( u. id. clone ( ) ) ; 176 + } 177 + } 165 178 166 179 let everyone = state. store. list_users ( ) . await ?; 167 180 let ( _, chrome) = build_chrome ( Expand 14 hidden lines 182 195 span class="entry-row-title" { 183 196 a href={ "/" ( u. username) } { ( u. username) } 184 197 @if u. is_admin { " " span class="badge" { "admin" } } 198 + @if verified. contains ( & u. id) { " " span class="badge badge-verified" { "verified" } } 185 199 @if u. disabled_at. is_some ( ) { " " span class="badge badge-private" { "disabled" } } 186 200 } 187 201 div class="entry-row-meta muted" { ( u. email) } 188 202 } 189 203 div class="entry-row-actions admin-actions" { 190 - ( post_button ( & format ! ( "/admin/users/{}/verify" , u. id) , & csrf, "Verify" , "btn" ) ) 204 + @if !verified. contains ( & u. id) { 205 + ( post_button ( & format ! ( "/admin/users/{}/verify" , u. id) , & csrf, "Verify" , "btn" ) ) 206 + } 191 207 ( toggle_button ( & format ! ( "/admin/users/{}/admin" , u. id) , & csrf, "admin" , !u. is_admin, 192 208 if u. is_admin { "Revoke admin" } else { "Make admin" } ) ) 193 209 ( toggle_button ( & format ! ( "/admin/users/{}/disable" , u. id) , & csrf, "disabled" , u. disabled_at. is_none ( ) , Expand 20 hidden lines 214 230 } 215 231 p class="muted field-hint" { "Leaving the password blank creates an account that must be activated by the user (they'll need an invite/reset)." } 216 232 div class="admin-form-actions" { 217 - label class="checkbox" { input type ="checkbox" name="admin" value="1" ; " Administrator" } 218 233 button class="btn btn-primary inline-btn" type ="submit" { "Create user" } 234 + label class="checkbox" { input type ="checkbox" name="admin" value="1" ; " Administrator" } 219 235 } 220 236 } 221 237 }
crates/web/src/pages.rs +3 −0 Expand 1270 hidden lines 1271 1271 . set_password ( & invite. user_id, Some ( & hash) ) 1272 1272 . await ?; 1273 1273 state. store. mark_invite_used ( & invite. id) . await ?; 1274 + 1275 + 1276 + let _ = state. store. verify_primary_email ( & invite. user_id) . await ; 1274 1277 let user = state 1275 1278 . store 1276 1279 . user_by_id ( & invite. user_id)