feat(admin): move pending registrations to their own page (#1388)

This commit is contained in:
ArcticWarmth authored and GitHub committed 2026-10-14 13:45:27 +02:00
1 parent 97b2ac91ca
commit abcb5cb814
31 files changed
+839 -328

No files matched your search

+2
View File
@@ -13,6 +13,7 @@ pub mod guilds;
pub mod jobs;
mod message_actions;
pub mod messages;
pub mod registrations;
pub mod reports;
pub mod system;
mod system_actions;
@@ -63,6 +64,7 @@ pub fn build_router(config: AdminConfig) -> Router {
.merge(applications::router())
.merge(codes::router())
.merge(discovery::router())
.merge(registrations::router())
.merge(jobs::router())
.merge(messages::router())
.merge(system::router())
+159
View File
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
api::{client::AdminApiClient, types::PendingRegistrationStatus},
middleware::{
auth::AuthContext,
csrf::CsrfToken,
flash::{self, FlashData},
htmx,
},
state::AppState,
templates::{self, layout_sidebar::pending_registration_count_markup},
utils::forms::clean_string,
};
use axum::{
Form, Router,
extract::State,
http::HeaderMap,
response::{Html, IntoResponse, Response},
routing::{get, post},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct PendingRegistrationForm {
#[serde(default)]
user_id: Option<String>,
}
pub fn router() -> Router<AppState> {
Router::new()
.route("/pending-registrations", get(pending_registrations_page))
.route(
"/pending-registrations/approve",
post(approve_pending_registration),
)
.route(
"/pending-registrations/reject",
post(reject_pending_registration),
)
}
async fn pending_registrations_page(
State(state): State<AppState>,
auth: axum::Extension<AuthContext>,
csrf: axum::Extension<CsrfToken>,
) -> Response {
let config = state.config();
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
let pending_registrations = match client.list_pending_registrations().await {
Ok(pending_registrations) => Some(pending_registrations),
Err(error) => {
tracing::warn!(%error, "admin API request failed: list pending registrations");
None
}
};
let markup = templates::pages::pending_registrations::pending_registrations_page(
config,
&auth.0,
&csrf.0.0,
pending_registrations.as_deref(),
);
Html(markup.into_string()).into_response()
}
async fn approve_pending_registration(
state: State<AppState>,
headers: HeaderMap,
auth: axum::Extension<AuthContext>,
csrf: axum::Extension<CsrfToken>,
form: Form<PendingRegistrationForm>,
) -> Response {
decide_pending_registration(
state,
headers,
auth,
csrf,
form,
PendingRegistrationStatus::Approved,
)
.await
}
async fn reject_pending_registration(
state: State<AppState>,
headers: HeaderMap,
auth: axum::Extension<AuthContext>,
csrf: axum::Extension<CsrfToken>,
form: Form<PendingRegistrationForm>,
) -> Response {
decide_pending_registration(
state,
headers,
auth,
csrf,
form,
PendingRegistrationStatus::Rejected,
)
.await
}
async fn decide_pending_registration(
State(state): State<AppState>,
headers: HeaderMap,
auth: axum::Extension<AuthContext>,
csrf: axum::Extension<CsrfToken>,
Form(form): Form<PendingRegistrationForm>,
status: PendingRegistrationStatus,
) -> Response {
let config = state.config();
let (success_message, error_message) = match status {
PendingRegistrationStatus::Approved => {
("Registration approved", "Failed to approve registration")
}
PendingRegistrationStatus::Rejected => {
("Registration rejected", "Failed to reject registration")
}
};
let flash = match clean_string(form.user_id.as_deref().unwrap_or("")) {
None => FlashData::error("User ID is required"),
Some(user_id) => {
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
match client.decide_pending_registration(&user_id, status).await {
Ok(pending_registrations) => {
let flash = FlashData::success(success_message);
if htmx::targets(&headers, "pending-registration-list") {
let markup = maud::html! {
(templates::pages::pending_registrations::pending_registration_list(
config,
&csrf.0.0,
&pending_registrations,
))
(pending_registration_count_markup(
u32::try_from(pending_registrations.len()).ok(),
true,
))
};
let mut response = Html(markup.into_string()).into_response();
htmx::add_toast_header(&mut response, &flash);
return response;
}
flash
}
Err(error) => {
tracing::warn!(%error, ?status, "admin API request failed: decide pending registration");
FlashData::error(error_message)
}
}
}
};
if htmx::is_htmx_request(&headers) {
return htmx::toast_response(&flash);
}
flash::redirect_with_flash(
&format!("{}/pending-registrations", config.base_path),
flash,
config.secure_cookies(),
)
}
-58
View File
@@ -304,48 +304,6 @@ pub async fn instance_config_post(
},
None => FlashData::error("Registration URL ID is required"),
},
"approve_pending_registration" => match form.clean("user_id") {
Some(user_id) => match client.approve_pending_registration(&user_id).await {
Ok(instance_config) => {
let flash = FlashData::success("Registration approved");
if htmx::targets(&headers, "pending-registration-list") {
return render_pending_registration_list_response(
config,
&csrf.0.0,
&instance_config,
&flash,
);
}
flash
}
Err(error) => {
tracing::warn!(%error, "admin API request failed: approve pending registration");
FlashData::error("Failed to approve registration")
}
},
None => FlashData::error("User ID is required"),
},
"reject_pending_registration" => match form.clean("user_id") {
Some(user_id) => match client.reject_pending_registration(&user_id).await {
Ok(instance_config) => {
let flash = FlashData::success("Registration rejected");
if htmx::targets(&headers, "pending-registration-list") {
return render_pending_registration_list_response(
config,
&csrf.0.0,
&instance_config,
&flash,
);
}
flash
}
Err(error) => {
tracing::warn!(%error, "admin API request failed: reject pending registration");
FlashData::error("Failed to reject registration")
}
},
None => FlashData::error("User ID is required"),
},
_ => FlashData::error("Unknown instance config action"),
};
if htmx::is_htmx_request(&headers) {
@@ -381,22 +339,6 @@ fn render_registration_url_list_response(
)
}
fn render_pending_registration_list_response(
config: &AdminConfig,
csrf_token: &str,
instance_config: &crate::api::types::InstanceConfigResponse,
flash: &FlashData,
) -> Response {
render_fragment_with_toast(
templates::pages::instance_config::pending_registration_list(
config,
csrf_token,
&instance_config.registration.pending_registrations,
),
flash,
)
}
fn render_fragment_with_toast(markup: Markup, flash: &FlashData) -> Response {
let mut response = Html(markup.into_string()).into_response();
htmx::add_toast_header(&mut response, flash);