Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/web/src/admin.rs +26 −7
| @@ -24,8 +24,10 @@ use crate::session::{RequireAdmin, verify_csrf}; | |||
| 24 | use crate::{AppState, setting_keys}; | 24 | use crate::{AppState, setting_keys}; |
| 25 | 25 | ||
| 26 | /// The admin dashboard shell: a left tab rail and the active tab's content. | 26 | /// The admin dashboard shell: a left tab rail and the active tab's content. |
| 27 | /// `show_invites` hides the Invites tab when self-registration is on (invites | ||
| 28 | /// are only useful when registration is otherwise closed). | ||
| 27 | #[allow(clippy::needless_pass_by_value)] // `content` is embedded once. | 29 | #[allow(clippy::needless_pass_by_value)] // `content` is embedded once. |
| 28 | fn admin_shell(active: &str, content: Markup) -> Markup { | 30 | fn admin_shell(active: &str, content: Markup, show_invites: bool) -> Markup { |
| 29 | let tab = |href: &str, key: &str, label: &str| { | 31 | let tab = |href: &str, key: &str, label: &str| { |
| 30 | html! { a href=(href) aria-current=[(active == key).then_some("page")] { (label) } } | 32 | html! { a href=(href) aria-current=[(active == key).then_some("page")] { (label) } } |
| 31 | }; | 33 | }; |
| @@ -38,7 +40,7 @@ fn admin_shell(active: &str, content: Markup) -> Markup { | |||
| 38 | (tab("/admin/users", "users", "Users")) | 40 | (tab("/admin/users", "users", "Users")) |
| 39 | (tab("/admin/repos", "repos", "Repositories")) | 41 | (tab("/admin/repos", "repos", "Repositories")) |
| 40 | (tab("/admin/groups", "groups", "Groups")) | 42 | (tab("/admin/groups", "groups", "Groups")) |
| 41 | (tab("/admin/invites", "invites", "Invites")) | 43 | @if show_invites { (tab("/admin/invites", "invites", "Invites")) } |
| 42 | (tab("/admin/settings", "settings", "Settings")) | 44 | (tab("/admin/settings", "settings", "Settings")) |
| 43 | } | 45 | } |
| 44 | } | 46 | } |
| @@ -56,8 +58,13 @@ fn render( | |||
| 56 | active: &str, | 58 | active: &str, |
| 57 | content: Markup, | 59 | content: Markup, |
| 58 | ) -> Response { | 60 | ) -> Response { |
| 61 | let show_invites = !state.allow_registration(); | ||
| 59 | let (jar, chrome) = build_chrome(state, jar, Some(user), uri.path().to_string()); | 62 | let (jar, chrome) = build_chrome(state, jar, Some(user), uri.path().to_string()); |
| 60 | (jar, page(&chrome, "Admin", admin_shell(active, content))).into_response() | 63 | ( |
| 64 | jar, | ||
| 65 | page(&chrome, "Admin", admin_shell(active, content, show_invites)), | ||
| 66 | ) | ||
| 67 | .into_response() | ||
| 61 | } | 68 | } |
| 62 | 69 | ||
| 63 | /// Rows shown per page in the admin list views. | 70 | /// Rows shown per page in the admin list views. |
| @@ -557,6 +564,11 @@ pub async fn invites( | |||
| 557 | jar: CookieJar, | 564 | jar: CookieJar, |
| 558 | uri: Uri, | 565 | uri: Uri, |
| 559 | ) -> AppResult<Response> { | 566 | ) -> AppResult<Response> { |
| 567 | // Invites only matter when self-registration is closed; otherwise the page | ||
| 568 | // is hidden and its route redirects to the overview. | ||
| 569 | if state.allow_registration() { | ||
| 570 | return Ok(Redirect::to("/admin").into_response()); | ||
| 571 | } | ||
| 560 | let invites = state.store.list_signup_invites().await?; | 572 | let invites = state.store.list_signup_invites().await?; |
| 561 | let base = state.config.instance.url.trim_end_matches('/').to_string(); | 573 | let base = state.config.instance.url.trim_end_matches('/').to_string(); |
| 562 | let (_, chrome) = build_chrome( | 574 | let (_, chrome) = build_chrome( |
| @@ -606,10 +618,14 @@ pub async fn invites( | |||
| 606 | } | 618 | } |
| 607 | } | 619 | } |
| 608 | }; | 620 | }; |
| 609 | // Clear the flash cookie after showing it. | 621 | // Clear the flash cookie after showing it once. The removal must carry the |
| 610 | let jar = jar.remove(axum_extra::extract::cookie::Cookie::from( | 622 | // same path the cookie was set with ("/admin/invites"), or the browser keeps |
| 611 | "fabrica_flash_invite", | 623 | // it and the link reappears on reload. |
| 612 | )); | 624 | let jar = jar.remove( |
| 625 | axum_extra::extract::cookie::Cookie::build("fabrica_flash_invite") | ||
| 626 | .path("/admin/invites") | ||
| 627 | .build(), | ||
| 628 | ); | ||
| 613 | Ok(render(&state, jar, user, &uri, "invites", content)) | 629 | Ok(render(&state, jar, user, &uri, "invites", content)) |
| 614 | } | 630 | } |
| 615 | 631 | ||
| @@ -633,6 +649,9 @@ pub async fn invite_create( | |||
| 633 | if verify_csrf(&jar, &headers, Some(&form.csrf)).is_err() { | 649 | if verify_csrf(&jar, &headers, Some(&form.csrf)).is_err() { |
| 634 | return AppError::Forbidden.into_response(); | 650 | return AppError::Forbidden.into_response(); |
| 635 | } | 651 | } |
| 652 | if state.allow_registration() { | ||
| 653 | return Redirect::to("/admin").into_response(); | ||
| 654 | } | ||
| 636 | let token = auth::new_session_token(); | 655 | let token = auth::new_session_token(); |
| 637 | let note = form.note.trim(); | 656 | let note = form.note.trim(); |
| 638 | let created = state | 657 | let created = state |