staff audit logs list (#1362)

This commit is contained in:
Brandon Thomas
2026-04-14 02:52:56 -04:00
committed by GitHub
parent 7ff78c6d94
commit af763669cc
18 changed files with 705 additions and 1 deletions
+6
View File
@@ -60,8 +60,14 @@ then the story, then the fine print.
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
This avoids long fully-qualified paths in function signatures and bodies.
## MySQL
- MySQL tables: primary key `id` columns must be `BIGINT(20) UNSIGNED`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
## TypeScript / Frontend
- TypeScript with React, Vite, Zustand, Three.js
- Nx monorepo under `crates/frontend/`
- Two spaces for indentation
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n sal.id,\n sal.token as `token: StaffAuditLogToken`,\n sal.audit_action,\n sal.maybe_entity_type,\n sal.maybe_entity_token,\n target_user.username as maybe_target_username,\n target_user.display_name as maybe_target_display_name,\n sal.staff_user_token as `staff_user_token: UserToken`,\n staff_user.username as staff_username,\n staff_user.display_name as staff_display_name,\n sal.staff_ip_address,\n sal.created_at\nFROM staff_audit_logs sal\nLEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token\nLEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token\nORDER BY sal.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: StaffAuditLogToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "audit_action",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_entity_type",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_target_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "maybe_target_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "staff_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "staff_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 9,
"name": "staff_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "staff_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "3846628765bd34ea2ce01e15a0db732121ce473c5c542d0e971ad29b2b421193"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n sal.id,\n sal.token as `token: StaffAuditLogToken`,\n sal.audit_action,\n sal.maybe_entity_type,\n sal.maybe_entity_token,\n target_user.username as maybe_target_username,\n target_user.display_name as maybe_target_display_name,\n sal.staff_user_token as `staff_user_token: UserToken`,\n staff_user.username as staff_username,\n staff_user.display_name as staff_display_name,\n sal.staff_ip_address,\n sal.created_at\nFROM staff_audit_logs sal\nLEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token\nLEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token\nWHERE sal.id < ?\nORDER BY sal.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: StaffAuditLogToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "audit_action",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_entity_type",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_target_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "maybe_target_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "staff_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "staff_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 9,
"name": "staff_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "staff_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "9132f832752ea40736bedf4729c76af7fe019cc69ed5cc280fce87e68c3f4846"
}
@@ -0,0 +1,31 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Recreate with the original signed BIGINT id.
CREATE TABLE staff_audit_logs (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
token VARCHAR(32) NOT NULL,
audit_action VARCHAR(32) NOT NULL,
maybe_entity_type VARCHAR(16) DEFAULT NULL,
maybe_entity_token VARCHAR(32) NOT NULL,
staff_user_token VARCHAR(32) DEFAULT NULL,
staff_ip_address VARCHAR(40) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_maybe_entity_type (maybe_entity_type),
KEY index_maybe_entity_token (maybe_entity_token),
KEY index_staff_user_token (staff_user_token),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS staff_audit_logs;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS staff_audit_logs;
@@ -0,0 +1,39 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE staff_audit_logs (
-- Not used for anything except replication.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" of the audit log item (in case we add a UI or notes)
token VARCHAR(32) NOT NULL,
-- The type of action taken
audit_action VARCHAR(32) NOT NULL,
-- The type of entity acted upon (if any)
maybe_entity_type VARCHAR(16) DEFAULT NULL,
-- The token of the entity mutated (if any)
maybe_entity_token VARCHAR(32) NOT NULL,
-- The staff user who performed the action.
staff_user_token VARCHAR(32) DEFAULT NULL,
-- For abuse tracking.
-- Wide enough for IPv4/6
staff_ip_address VARCHAR(40) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_maybe_entity_type (maybe_entity_type),
KEY index_maybe_entity_token (maybe_entity_token),
KEY index_staff_user_token (staff_user_token),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+38
View File
@@ -0,0 +1,38 @@
# SQL Migrations
Incremental schema changes applied in order by timestamp prefix.
## Naming Convention
Migration directories follow the format:
```
YYYY-MM-DD-HHMMSS-NNNN_description_goes_here/
up.sql
down.sql
```
These are created with the `diesel` tool, and in particular:
> `diesel migration generate description_goes_here`
- `NNNN` is a zero-padded sequence number for ordering multiple migrations on the same timestamp.
- `up.sql` applies the migration forward.
- `down.sql` reverses it.
## Table Conventions
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
- All SQL files should start with the standard IntelliJ noinspection comments:
```sql
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
```
## Rules
- Every `up.sql` must have a corresponding `down.sql` that cleanly reverses the migration.
- For `CREATE TABLE` migrations, the `down.sql` is typically `DROP TABLE IF EXISTS table_name;`.
- For `ALTER TABLE` migrations, the `down.sql` reverses the alteration (e.g. `DROP COLUMN`).
@@ -0,0 +1,26 @@
# Squashed / Materialized Schemas
Fully materialized table definitions representing the current state of each table
after all migrations have been applied. These are not executed — they exist as
human-readable documentation of the current schema.
## When to Update
Update the corresponding squashed file whenever a migration changes a table's schema
(adding columns, changing types, dropping and recreating, etc.).
## Table Conventions
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
- All SQL files should start with the standard IntelliJ noinspection comments:
```sql
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
```
## File Organization
Files are named after their table (e.g. `users.sql`, `media_files.sql`).
Some tables are grouped into subdirectories (e.g. `user_tables/`).
@@ -5,7 +5,7 @@
CREATE TABLE staff_audit_logs (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" of the audit log item (in case we add a UI or notes)
token VARCHAR(32) NOT NULL,
@@ -52,6 +52,11 @@ schema metadata. For **new tables**, you may need to:
2. Run `cargo sqlx prepare` to regenerate the cache
3. Or manually create cache entries for simple INSERT/UPDATE queries (they only need parameter count)
### Table Conventions
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
### Error Types
New queries should return `Result<T, sqlx::Error>` instead of the legacy `AnyhowResult<T>`.
@@ -0,0 +1,96 @@
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use tokens::tokens::staff_audit_logs::StaffAuditLogToken;
use tokens::tokens::users::UserToken;
pub struct StaffAuditLogListItem {
pub id: u64,
pub token: StaffAuditLogToken,
pub audit_action: String,
pub maybe_entity_type: Option<String>,
pub maybe_entity_token: Option<String>,
pub maybe_target_username: Option<String>,
pub maybe_target_display_name: Option<String>,
pub staff_user_token: Option<UserToken>,
pub staff_username: Option<String>,
pub staff_display_name: Option<String>,
pub staff_ip_address: String,
pub created_at: DateTime<Utc>,
}
pub struct ListStaffAuditLogsArgs<'a> {
pub maybe_cursor_id: Option<u64>,
pub limit: u32,
pub mysql_pool: &'a MySqlPool,
}
pub async fn list_staff_audit_logs(
args: ListStaffAuditLogsArgs<'_>,
) -> Result<Vec<StaffAuditLogListItem>, sqlx::Error> {
let limit = args.limit as i64;
let items = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query_as!(
StaffAuditLogListItem,
r#"
SELECT
sal.id,
sal.token as `token: StaffAuditLogToken`,
sal.audit_action,
sal.maybe_entity_type,
sal.maybe_entity_token,
target_user.username as maybe_target_username,
target_user.display_name as maybe_target_display_name,
sal.staff_user_token as `staff_user_token: UserToken`,
staff_user.username as staff_username,
staff_user.display_name as staff_display_name,
sal.staff_ip_address,
sal.created_at
FROM staff_audit_logs sal
LEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token
LEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token
WHERE sal.id < ?
ORDER BY sal.id DESC
LIMIT ?
"#,
cursor_id as u64,
limit,
)
.fetch_all(args.mysql_pool)
.await?
}
None => {
sqlx::query_as!(
StaffAuditLogListItem,
r#"
SELECT
sal.id,
sal.token as `token: StaffAuditLogToken`,
sal.audit_action,
sal.maybe_entity_type,
sal.maybe_entity_token,
target_user.username as maybe_target_username,
target_user.display_name as maybe_target_display_name,
sal.staff_user_token as `staff_user_token: UserToken`,
staff_user.username as staff_username,
staff_user.display_name as staff_display_name,
sal.staff_ip_address,
sal.created_at
FROM staff_audit_logs sal
LEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token
LEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token
ORDER BY sal.id DESC
LIMIT ?
"#,
limit,
)
.fetch_all(args.mysql_pool)
.await?
}
};
Ok(items)
}
@@ -1 +1,2 @@
pub mod insert_staff_audit_log;
pub mod list_staff_audit_logs;
@@ -86,6 +86,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi
use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*;
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::staff_audit_logs::moderator_list_staff_audit_logs_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::*;
@@ -510,6 +511,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler,
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::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_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,
@@ -895,6 +897,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListAllImpersonationRequestsQueryParams,
ListAllImpersonationRequestsSuccessResponse,
ListImpersonationRequestsPathInfo,
ListStaffAuditLogsQueryParams,
ListStaffAuditLogsSuccessResponse,
ListImpersonationRequestsQueryParams,
ListUserImpersonationRequestsSuccessResponse,
ListMediaFilesByBatchPathInfo,
@@ -960,6 +964,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
RenameMediaFileRequest,
RequestDetailsResponse,
ResultDetailsResponse,
StaffAuditLogResponse,
SearchFeaturedMediaFileListItem,
SearchFeaturedMediaFilesError,
SearchFeaturedMediaFilesQueryParams,
@@ -2,6 +2,7 @@ pub mod alerts;
pub mod info;
pub mod ip_bans;
pub mod jobs;
pub mod staff_audit_logs;
pub mod user;
pub mod user_bans;
pub mod user_feature_flags;
@@ -0,0 +1 @@
pub mod moderator_list_staff_audit_logs_handler;
@@ -0,0 +1,146 @@
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::staff_audit_logs::list_staff_audit_logs::{
list_staff_audit_logs, ListStaffAuditLogsArgs,
};
use tokens::tokens::staff_audit_logs::StaffAuditLogToken;
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 = "modstfaud";
const DEFAULT_LIMIT: u32 = 50;
const MAX_LIMIT: u32 = 1000;
// --- Request ---
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListStaffAuditLogsQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
// --- Response ---
#[derive(Serialize, ToSchema)]
pub struct ListStaffAuditLogsSuccessResponse {
pub success: bool,
pub audit_logs: Vec<StaffAuditLogResponse>,
pub maybe_cursor: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct StaffAuditLogResponse {
pub token: StaffAuditLogToken,
pub audit_action: String,
pub maybe_entity_type: Option<String>,
pub maybe_entity_token: Option<String>,
pub maybe_target_username: Option<String>,
pub maybe_target_display_name: Option<String>,
pub staff_user_token: Option<UserToken>,
pub staff_username: Option<String>,
pub staff_display_name: Option<String>,
pub staff_ip_address: String,
pub created_at: DateTime<Utc>,
}
// --- Handler ---
/// List all staff audit logs. Moderators only.
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/staff_audit_logs/list",
params(
ListStaffAuditLogsQueryParams,
),
responses(
(status = 200, description = "Success", body = ListStaffAuditLogsSuccessResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderator_list_staff_audit_logs_handler(
http_request: HttpRequest,
query: Query<ListStaffAuditLogsQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListStaffAuditLogsSuccessResponse>, 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_staff_audit_logs(
ListStaffAuditLogsArgs {
maybe_cursor_id,
limit,
mysql_pool: &server_state.mysql_pool,
},
).await.map_err(|err| {
warn!("Failed to list staff audit logs: {:?}", 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 audit_logs = records.into_iter().map(|r| {
StaffAuditLogResponse {
token: r.token,
audit_action: r.audit_action,
maybe_entity_type: r.maybe_entity_type,
maybe_entity_token: r.maybe_entity_token,
maybe_target_username: r.maybe_target_username,
maybe_target_display_name: r.maybe_target_display_name,
staff_user_token: r.staff_user_token,
staff_username: r.staff_username,
staff_display_name: r.staff_display_name,
staff_ip_address: r.staff_ip_address,
created_at: r.created_at,
}
}).collect();
Ok(Json(ListStaffAuditLogsSuccessResponse {
success: true,
audit_logs,
maybe_cursor,
}))
}
@@ -35,6 +35,7 @@ use crate::http_server::endpoints::moderation::user::moderator_list_users_by_sig
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;
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_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;
@@ -106,6 +107,12 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/staff_audit_logs")
.service(web::resource("/list")
.route(web::get().to(moderator_list_staff_audit_logs_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/user_sessions")
.service(web::resource("/impersonate")
.route(web::post().to(moderator_user_session_impersonation_request_handler))