diff --git a/.sqlx/query-83840fa1a0712a326a26ae146a819f67a44fae927f294780d89892270075ebb7.json b/.sqlx/query-83840fa1a0712a326a26ae146a819f67a44fae927f294780d89892270075ebb7.json new file mode 100644 index 0000000000..430560e55d --- /dev/null +++ b/.sqlx/query-83840fa1a0712a326a26ae146a819f67a44fae927f294780d89892270075ebb7.json @@ -0,0 +1,289 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n u.id as `id: u64`,\n u.token as `token: tokens::tokens::users::UserToken`,\n u.username,\n u.display_name,\n u.username_is_not_customized as `username_is_not_customized: bool`,\n u.email_address,\n u.email_confirmed as `email_confirmed: bool`,\n u.is_without_password as `is_without_password: bool`,\n u.ip_address_creation,\n u.maybe_source as `maybe_source: enums::by_table::users::user_signup_source::UserSignupSource`,\n u.maybe_signup_method as `maybe_signup_method: enums::by_table::users::user_signup_method::UserSignupMethod`,\n u.created_at,\n u.maybe_referral_url,\n u.is_temporary as `is_temporary: bool`,\n us.subscription_namespace,\n us.subscription_product_slug,\n us.maybe_stripe_subscription_id,\n us.maybe_stripe_customer_id,\n us.maybe_stripe_product_id,\n us.maybe_stripe_price_id,\n us.maybe_stripe_subscription_status,\n us.maybe_stripe_recurring_interval,\n us.maybe_stripe_invoice_is_paid as `maybe_stripe_invoice_is_paid: bool`,\n us.maybe_cancel_at,\n us.maybe_canceled_at\nFROM users AS u\nINNER JOIN user_subscriptions AS us ON u.token = us.user_token\nWHERE\n u.id < ?\n AND u.user_deleted_at IS NULL\n AND u.mod_deleted_at IS NULL\nORDER BY u.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT | BINARY | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "token: tokens::tokens::users::UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "username_is_not_customized: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 5, + "name": "email_address", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 6, + "name": "email_confirmed: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 7, + "name": "is_without_password: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 8, + "name": "ip_address_creation", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 160 + } + }, + { + "ordinal": 9, + "name": "maybe_source: enums::by_table::users::user_signup_source::UserSignupSource", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 10, + "name": "maybe_signup_method: enums::by_table::users::user_signup_method::UserSignupMethod", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 64 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 12, + "name": "maybe_referral_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 13, + "name": "is_temporary: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 14, + "name": "subscription_namespace", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 15, + "name": "subscription_product_slug", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 16, + "name": "maybe_stripe_subscription_id", + "type_info": { + "type": "VarString", + "flags": "UNIQUE_KEY | BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 17, + "name": "maybe_stripe_customer_id", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 18, + "name": "maybe_stripe_product_id", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 19, + "name": "maybe_stripe_price_id", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 20, + "name": "maybe_stripe_subscription_status", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 21, + "name": "maybe_stripe_recurring_interval", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 22, + "name": "maybe_stripe_invoice_is_paid: bool", + "type_info": { + "type": "Tiny", + "flags": "", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 23, + "name": "maybe_cancel_at", + "type_info": { + "type": "Timestamp", + "flags": "BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 24, + "name": "maybe_canceled_at", + "type_info": { + "type": "Timestamp", + "flags": "BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true + ] + }, + "hash": "83840fa1a0712a326a26ae146a819f67a44fae927f294780d89892270075ebb7" +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/list_subscribing_users_by_signup_date.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/list_subscribing_users_by_signup_date.rs new file mode 100644 index 0000000000..85ad6682bb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/list_subscribing_users_by_signup_date.rs @@ -0,0 +1,55 @@ +use chrono::{DateTime, Utc}; +use enums::by_table::users::user_signup_method::UserSignupMethod; +use enums::by_table::users::user_signup_source::UserSignupSource; +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::users::UserToken; +use utoipa::ToSchema; + +pub const MODERATOR_LIST_SUBSCRIBING_USERS_BY_SIGNUP_DATE_PATH: &str = "/v1/moderation/users/list_subscribers_by_signup_date"; + +#[derive(Deserialize, ToSchema)] +pub struct ModeratorListSubscribingUsersBySignupDateRequest { + /// Optional cursor for pagination. Pass the `next_cursor` from a previous response. + pub id_cursor: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct ModeratorListSubscribingUsersBySignupDateResponse { + pub success: bool, + pub users: Vec, + /// The cursor for the next page. `None` if there are no more results. + pub next_cursor: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct ModeratorListSubscribingUsersBySignupDateEntry { + // -- User fields -- + pub id: u64, + pub token: UserToken, + pub username: String, + pub display_name: String, + pub username_is_not_customized: bool, + pub email_address: String, + pub email_confirmed: bool, + pub is_without_password: bool, + pub ip_address_creation: String, + pub maybe_source: Option, + pub maybe_signup_method: Option, + pub created_at: DateTime, + pub maybe_referral_url: Option, + pub is_temporary: bool, + + // TODO: Use enum types/strong types - + // -- Subscription fields -- + pub subscription_namespace: String, + pub subscription_product_slug: String, + pub maybe_stripe_subscription_id: Option, + pub maybe_stripe_customer_id: Option, + pub maybe_stripe_product_id: Option, + pub maybe_stripe_price_id: Option, + pub maybe_stripe_subscription_status: Option, + pub maybe_stripe_recurring_interval: Option, + pub maybe_stripe_invoice_is_paid: Option, + pub maybe_cancel_at: Option>, + pub maybe_canceled_at: Option>, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/mod.rs index 11906cfcb9..56e6c75710 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/user/mod.rs @@ -1,3 +1,4 @@ +pub mod list_subscribing_users_by_signup_date; pub mod list_users_by_signup_date; pub mod user_lookup; -pub mod user_lookup_by_stripe_customer_id; +pub mod user_lookup_by_stripe_customer_id; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/list/list_subscribing_users_by_signup_date_for_moderation.rs b/crates/schema/database/mysql_queries/src/queries/users/user/list/list_subscribing_users_by_signup_date_for_moderation.rs new file mode 100644 index 0000000000..7c2614fbf4 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/users/user/list/list_subscribing_users_by_signup_date_for_moderation.rs @@ -0,0 +1,122 @@ +use chrono::{DateTime, Utc}; +use log::warn; +use sqlx::MySqlPool; + +use enums::by_table::users::user_signup_method::UserSignupMethod; +use enums::by_table::users::user_signup_source::UserSignupSource; +use errors::AnyhowResult; +use tokens::tokens::users::UserToken; + +pub struct ListSubscribingUsersBySignupDateRow { + pub id: u64, + pub token: UserToken, + pub username: String, + pub display_name: String, + pub username_is_not_customized: bool, + pub email_address: String, + pub email_confirmed: bool, + pub is_without_password: bool, + pub ip_address_creation: String, + pub maybe_source: Option, + pub maybe_signup_method: Option, + pub created_at: DateTime, + pub maybe_referral_url: Option, + pub is_temporary: bool, + + // Subscription fields + pub subscription_namespace: String, + pub subscription_product_slug: String, + pub maybe_stripe_subscription_id: Option, + pub maybe_stripe_customer_id: Option, + pub maybe_stripe_product_id: Option, + pub maybe_stripe_price_id: Option, + pub maybe_stripe_subscription_status: Option, + pub maybe_stripe_recurring_interval: Option, + pub maybe_stripe_invoice_is_paid: Option, + pub maybe_cancel_at: Option>, + pub maybe_canceled_at: Option>, +} + +pub struct ListSubscribingUsersBySignupDateArgs { + pub maybe_id_cursor: Option, + pub limit: u32, +} + +pub struct ListSubscribingUsersBySignupDateResult { + pub users: Vec, + pub next_cursor: Option, +} + +pub async fn list_subscribing_users_by_signup_date_for_moderation( + args: ListSubscribingUsersBySignupDateArgs, + mysql_pool: &MySqlPool, +) -> AnyhowResult { + // Fetch limit + 1 so we can detect whether there's a next page. + let fetch_limit = (args.limit + 1) as i64; + + // Use i64::MAX when no cursor so all rows are included. + let id_cursor = args.maybe_id_cursor + .map(|c| c as i64) + .unwrap_or(i64::MAX); + + let rows = sqlx::query_as!( + ListSubscribingUsersBySignupDateRow, + r#" +SELECT + u.id as `id: u64`, + u.token as `token: tokens::tokens::users::UserToken`, + u.username, + u.display_name, + u.username_is_not_customized as `username_is_not_customized: bool`, + u.email_address, + u.email_confirmed as `email_confirmed: bool`, + u.is_without_password as `is_without_password: bool`, + u.ip_address_creation, + u.maybe_source as `maybe_source: enums::by_table::users::user_signup_source::UserSignupSource`, + u.maybe_signup_method as `maybe_signup_method: enums::by_table::users::user_signup_method::UserSignupMethod`, + u.created_at, + u.maybe_referral_url, + u.is_temporary as `is_temporary: bool`, + us.subscription_namespace, + us.subscription_product_slug, + us.maybe_stripe_subscription_id, + us.maybe_stripe_customer_id, + us.maybe_stripe_product_id, + us.maybe_stripe_price_id, + us.maybe_stripe_subscription_status, + us.maybe_stripe_recurring_interval, + us.maybe_stripe_invoice_is_paid as `maybe_stripe_invoice_is_paid: bool`, + us.maybe_cancel_at, + us.maybe_canceled_at +FROM users AS u +INNER JOIN user_subscriptions AS us ON u.token = us.user_token +WHERE + u.id < ? + AND u.user_deleted_at IS NULL + AND u.mod_deleted_at IS NULL +ORDER BY u.id DESC +LIMIT ? + "#, + id_cursor, + fetch_limit, + ) + .fetch_all(mysql_pool) + .await + .map_err(|err| { + warn!("list_subscribing_users_by_signup_date_for_moderation query error: {:?}", err); + anyhow::anyhow!("query error") + })?; + + let has_next = rows.len() as u32 > args.limit; + let users: Vec = rows.into_iter().take(args.limit as usize).collect(); + let next_cursor = if has_next { + users.last().map(|u| u.id) + } else { + None + }; + + Ok(ListSubscribingUsersBySignupDateResult { + users, + next_cursor, + }) +} diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/list/mod.rs b/crates/schema/database/mysql_queries/src/queries/users/user/list/mod.rs index 9b741b436f..b16adfea3a 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/list/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/list/mod.rs @@ -1 +1,2 @@ +pub mod list_subscribing_users_by_signup_date_for_moderation; pub mod list_users_by_signup_date_for_moderation; diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index a4e1879d44..8aea0de780 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -278,6 +278,7 @@ use artcraft_api_defs::subscriptions::get_session_subscription::*; use artcraft_api_defs::media_file::list_batch_generated_media_files::*; // Handler modules with locally-defined types use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*; +use artcraft_api_defs::moderation::user::list_subscribing_users_by_signup_date::*; use artcraft_api_defs::moderation::user::list_users_by_signup_date::*; use artcraft_api_defs::moderation::user::user_lookup::*; use artcraft_api_defs::moderation::user::user_lookup_by_stripe_customer_id::*; @@ -467,6 +468,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler, // Moderation crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler, + crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler, crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler, crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler, crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler, @@ -659,6 +661,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux EditUserFeatureFlagsError, EditUserFeatureFlagsOption, EditUserFeatureFlagsRequest, + ModeratorListSubscribingUsersBySignupDateRequest, + ModeratorListSubscribingUsersBySignupDateResponse, + ModeratorListSubscribingUsersBySignupDateEntry, ModeratorListUsersBySignupDateRequest, ModeratorListUsersBySignupDateResponse, ModeratorListUsersBySignupDateEntry, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/mod.rs index 5f6e0c46b5..8d59eaaf39 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/mod.rs @@ -1,3 +1,4 @@ +pub mod moderator_list_subscribing_users_by_signup_date; pub mod moderator_list_users_by_signup_date; pub mod moderator_user_lookup_by_stripe_customer_id_handler; pub mod moderator_user_lookup_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/moderator_list_subscribing_users_by_signup_date.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/moderator_list_subscribing_users_by_signup_date.rs new file mode 100644 index 0000000000..6e45c7a854 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user/moderator_list_subscribing_users_by_signup_date.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::warn; + +use artcraft_api_defs::moderation::user::list_subscribing_users_by_signup_date::{ + ModeratorListSubscribingUsersBySignupDateEntry, + ModeratorListSubscribingUsersBySignupDateRequest, + ModeratorListSubscribingUsersBySignupDateResponse, +}; +use mysql_queries::queries::users::user::list::list_subscribing_users_by_signup_date_for_moderation::{ + list_subscribing_users_by_signup_date_for_moderation, + ListSubscribingUsersBySignupDateArgs, +}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase}; +use crate::state::server_state::ServerState; + +/// List subscribing users by signup date (descending), for moderators. +#[utoipa::path( + post, + tag = "Moderation", + path = "/v1/moderation/users/list_subscribers_by_signup_date", + request_body = ModeratorListSubscribingUsersBySignupDateRequest, + responses( + (status = 200, description = "Success", body = ModeratorListSubscribingUsersBySignupDateResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_list_subscribing_users_by_signup_date_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, CommonWebError> { + + let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection) + .await + .map_err(|_| CommonWebError::NotAuthorized)?; + + let result = list_subscribing_users_by_signup_date_for_moderation( + ListSubscribingUsersBySignupDateArgs { + maybe_id_cursor: request.id_cursor, + limit: 100, + }, + &server_state.mysql_pool, + ).await + .map_err(|err| { + warn!("moderator_list_subscribing_users_by_signup_date error: {:?}", err); + CommonWebError::ServerError + })?; + + let users = result.users.into_iter().map(|u| ModeratorListSubscribingUsersBySignupDateEntry { + id: u.id, + token: u.token, + username: u.username, + display_name: u.display_name, + username_is_not_customized: u.username_is_not_customized, + email_address: u.email_address, + email_confirmed: u.email_confirmed, + is_without_password: u.is_without_password, + ip_address_creation: u.ip_address_creation, + maybe_source: u.maybe_source, + maybe_signup_method: u.maybe_signup_method, + created_at: u.created_at, + maybe_referral_url: u.maybe_referral_url, + is_temporary: u.is_temporary, + subscription_namespace: u.subscription_namespace, + subscription_product_slug: u.subscription_product_slug, + maybe_stripe_subscription_id: u.maybe_stripe_subscription_id, + maybe_stripe_customer_id: u.maybe_stripe_customer_id, + maybe_stripe_product_id: u.maybe_stripe_product_id, + maybe_stripe_price_id: u.maybe_stripe_price_id, + maybe_stripe_subscription_status: u.maybe_stripe_subscription_status, + maybe_stripe_recurring_interval: u.maybe_stripe_recurring_interval, + maybe_stripe_invoice_is_paid: u.maybe_stripe_invoice_is_paid, + maybe_cancel_at: u.maybe_cancel_at, + maybe_canceled_at: u.maybe_canceled_at, + }).collect(); + + Ok(Json(ModeratorListSubscribingUsersBySignupDateResponse { + success: true, + users, + next_cursor: result.next_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs index bcdddd422b..750618bf76 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs @@ -31,6 +31,7 @@ use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_cou use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler; use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler; use crate::http_server::endpoints::moderation::user_bans::list_banned_users::list_banned_users_handler; +use crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler; use crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler; use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler; use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler; @@ -65,6 +66,10 @@ pub fn add_moderator_routes (app: App) -> App .route(web::post().to(moderator_list_users_by_signup_date_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/list_subscribers_by_signup_date") + .route(web::post().to(moderator_list_subscribing_users_by_signup_date_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service(web::scope("/jobs") .service(web::resource("/user/{user_token}/list")