new endpoint

This commit is contained in:
Brandon Thomas committed 2026-04-14 02:15:07 -04:00
1 parent 50d31cffd8
commit d9151b9fc8
9 files changed
+546 -2

No files matched your search

@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nWHERE uir.id < ?\nORDER BY uir.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | AUTO_INCREMENT | BINARY",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "impersonator_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "impersonator_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "impersonator_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 4,
"name": "impersonated_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "impersonated_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "impersonated_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "is_redeemed: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_expired: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "expires_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nORDER BY uir.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | AUTO_INCREMENT | BINARY",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "impersonator_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "impersonator_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "impersonator_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 4,
"name": "impersonated_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "impersonated_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "impersonated_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "is_redeemed: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_expired: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "expires_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1"
}
@@ -0,0 +1,95 @@
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use tokens::tokens::users::UserToken;
pub struct UserImpersonationRequestListItem {
pub id: u64,
pub impersonator_user_token: UserToken,
pub impersonator_username: String,
pub impersonator_display_name: String,
pub impersonated_user_token: UserToken,
pub impersonated_username: String,
pub impersonated_display_name: String,
pub is_redeemed: bool,
pub is_expired: bool,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub struct ListUserImpersonationRequestsArgs<'a> {
pub maybe_cursor_id: Option<u64>,
pub limit: u32,
pub mysql_pool: &'a MySqlPool,
}
pub async fn list_user_impersonation_requests(
args: ListUserImpersonationRequestsArgs<'_>,
) -> Result<Vec<UserImpersonationRequestListItem>, sqlx::Error> {
let limit = args.limit as i64;
let items = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query_as!(
UserImpersonationRequestListItem,
r#"
SELECT
uir.id,
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
impersonator.username as impersonator_username,
impersonator.display_name as impersonator_display_name,
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
impersonated.username as impersonated_username,
impersonated.display_name as impersonated_display_name,
uir.is_redeemed as `is_redeemed: bool`,
(uir.expires_at < NOW()) as `is_expired: bool`,
uir.expires_at,
uir.created_at,
uir.updated_at
FROM user_impersonation_requests uir
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
WHERE uir.id < ?
ORDER BY uir.id DESC
LIMIT ?
"#,
cursor_id as u64,
limit,
)
.fetch_all(args.mysql_pool)
.await?
}
None => {
sqlx::query_as!(
UserImpersonationRequestListItem,
r#"
SELECT
uir.id,
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
impersonator.username as impersonator_username,
impersonator.display_name as impersonator_display_name,
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
impersonated.username as impersonated_username,
impersonated.display_name as impersonated_display_name,
uir.is_redeemed as `is_redeemed: bool`,
(uir.expires_at < NOW()) as `is_expired: bool`,
uir.expires_at,
uir.created_at,
uir.updated_at
FROM user_impersonation_requests uir
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
ORDER BY uir.id DESC
LIMIT ?
"#,
limit,
)
.fetch_all(args.mysql_pool)
.await?
}
};
Ok(items)
}
@@ -1,4 +1,5 @@
pub mod insert_user_impersonation_request;
pub mod list_user_impersonation_requests;
pub mod list_user_impersonation_requests_for_user;
pub mod lookup_user_impersonation_request;
pub mod mark_impersonation_token_as_redeemed;
@@ -87,6 +87,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file:
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*;
use crate::http_server::endpoints::prompts::get_prompt_handler::*;
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
@@ -510,6 +511,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler,
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler,
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler,
crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler,
// Credits
crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler,
@@ -596,6 +598,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
WeightsDefaultCoverInfo,
// Endpoint API types
AllImpersonationRequestResponse,
AppStateError,
AppStateLegacyPermissionFlags,
AppStatePermissions,
@@ -889,6 +892,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListFeaturedWeightsError,
ListFeaturedWeightsQueryParams,
ListFeaturedWeightsSuccessResponse,
ListAllImpersonationRequestsQueryParams,
ListAllImpersonationRequestsSuccessResponse,
ListImpersonationRequestsPathInfo,
ListImpersonationRequestsQueryParams,
ListUserImpersonationRequestsSuccessResponse,
@@ -1,2 +1,3 @@
pub mod moderator_list_user_session_impersonation_requests_for_user_handler;
pub mod moderator_list_user_session_impersonation_requests_handler;
pub mod moderator_user_session_impersonation_request_handler;
@@ -64,7 +64,7 @@ pub struct UserImpersonationRequestResponse {
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/user_sessions/impersonation_requests/{user_token}",
path = "/v1/moderation/user_sessions/impersonation_requests/user/{user_token}",
params(
ListImpersonationRequestsQueryParams,
),
@@ -0,0 +1,145 @@
use std::sync::Arc;
use actix_web::web::{Json, Query};
use actix_web::{web, HttpRequest};
use chrono::{DateTime, Utc};
use log::warn;
use utoipa::{IntoParams, ToSchema};
use mysql_queries::queries::user_impersonation_requests::list_user_impersonation_requests::{
list_user_impersonation_requests, ListUserImpersonationRequestsArgs,
};
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modallimp";
const DEFAULT_LIMIT: u32 = 50;
const MAX_LIMIT: u32 = 1000;
// --- Request ---
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListAllImpersonationRequestsQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
// --- Response ---
#[derive(Serialize, ToSchema)]
pub struct ListAllImpersonationRequestsSuccessResponse {
pub success: bool,
pub impersonation_requests: Vec<AllImpersonationRequestResponse>,
pub maybe_cursor: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct AllImpersonationRequestResponse {
pub impersonator_user_token: UserToken,
pub impersonator_username: String,
pub impersonator_display_name: String,
pub impersonated_user_token: UserToken,
pub impersonated_username: String,
pub impersonated_display_name: String,
pub is_redeemed: bool,
pub is_expired: bool,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// --- Handler ---
/// List all impersonation requests (not scoped to a user). Moderators only.
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/user_sessions/impersonation_requests/list",
params(
ListAllImpersonationRequestsQueryParams,
),
responses(
(status = 200, description = "Success", body = ListAllImpersonationRequestsSuccessResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderator_list_user_session_impersonation_requests_handler(
http_request: HttpRequest,
query: Query<ListAllImpersonationRequestsQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListAllImpersonationRequestsSuccessResponse>, AdvancedCommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
AdvancedCommonWebError::NotAuthorized
})?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
.min(MAX_LIMIT);
let maybe_cursor_id = match &query.cursor {
None => None,
Some(cursor_str) => {
let decoded = server_state.opaque_cursors
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
.map_err(|err| {
warn!("Failed to decode cursor: {:?}", err);
AdvancedCommonWebError::BadInputWithSimpleMessage(
"Invalid cursor".to_string())
})?;
decoded.last_id
}
};
let records = list_user_impersonation_requests(
ListUserImpersonationRequestsArgs {
maybe_cursor_id,
limit,
mysql_pool: &server_state.mysql_pool,
},
).await.map_err(|err| {
warn!("Failed to list impersonation requests: {:?}", err);
AdvancedCommonWebError::from_error(err)
})?;
let maybe_cursor = records.last().map(|last| {
server_state.opaque_cursors
.encode_last_id_cursor(CURSOR_NAME, last.id)
}).transpose().map_err(|err| {
warn!("Failed to encode cursor: {:?}", err);
AdvancedCommonWebError::server_error_with_message("Failed to encode cursor")
})?;
let impersonation_requests = records.into_iter().map(|r| {
AllImpersonationRequestResponse {
impersonator_user_token: r.impersonator_user_token,
impersonator_username: r.impersonator_username,
impersonator_display_name: r.impersonator_display_name,
impersonated_user_token: r.impersonated_user_token,
impersonated_username: r.impersonated_username,
impersonated_display_name: r.impersonated_display_name,
is_redeemed: r.is_redeemed,
is_expired: r.is_expired,
expires_at: r.expires_at,
created_at: r.created_at,
updated_at: r.updated_at,
}
}).collect();
Ok(Json(ListAllImpersonationRequestsSuccessResponse {
success: true,
impersonation_requests,
maybe_cursor,
}))
}
@@ -37,6 +37,7 @@ use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handl
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler;
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler;
pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
@@ -110,7 +111,11 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
.route(web::post().to(moderator_user_session_impersonation_request_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/impersonation_requests/{user_token}")
.service(web::resource("/impersonation_requests/list")
.route(web::get().to(moderator_list_user_session_impersonation_requests_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/impersonation_requests/user/{user_token}")
.route(web::get().to(moderator_list_user_session_impersonation_requests_for_user_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)