Skip to main content

MT audit remediation: typed enums, batch inserts, indexes, session logging - Add sqlx::Type derives to CommunityRole, BanType, ModAction for typed query returns - Replace COUNT(*) > 0 with EXISTS in ban/mute/endorsement checks - Batch INSERT for set_thread_tags and insert_mentions (eliminates N+1) - Wrap toggle_endorsement in transaction to prevent race conditions - Add 5 performance indexes (migration 023) - Add LIMIT to unbounded queries (list_all_communities, list_community_members) - Log session operation failures in auth.rs instead of silently discarding - Complete .env.example with S3 and INTERNAL_SHARED_SECRET vars - Fix test create_post calls to include is_reply parameter

  • Co-Authored-ByClaude Opus 4.6 <noreply@anthropic.com>

Author: Max J. <87768334+MaxJMath@users.noreply.github.com> - 2026-04-16 05:31 UTC

Commit:

58e52d58f45b430f109bb9c86517dfd76952958f

Parent:

1180376

18 files changed,

+179 insertions,

-83 deletions

OldNewLine
@@ -12,3 +12,13 @@
12
12
13
13
# Platform admin (UUID of the MNW account that can access /_admin)
14
14
# PLATFORM_ADMIN_ID=00000000-0000-0000-0000-000000000000
15
16
# S3 storage (optional — required for image uploads)
17
# S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
18
# S3_BUCKET=mt-uploads
19
# S3_ACCESS_KEY=your-access-key
20
# S3_SECRET_KEY=your-secret-key
21
# S3_REGION=us-east-1
22
23
# Internal API shared secret (HMAC-SHA256 auth for MNW → MT calls)
24
# INTERNAL_SHARED_SECRET=your-shared-secret
OldNewLine
@@ -2074,6 +2074,8 @@
2074
2074
version = "0.3.3"
2075
2075
dependencies = [
2076
2076
"chrono",
2077
"serde",
2078
"sqlx",
2077
2079
]
2078
2080
2079
2081
[[package]]
OldNewLine
@@ -64,11 +64,15 @@
64
64
}
65
65
66
66
async fn save_to_session(&self, session: &Session) {
67
let _ = session.insert(SESSION_USER_ID, self.user_id).await;
68
let _ = session.insert(SESSION_USERNAME, &self.username).await;
69
let _ = session
70
.insert(SESSION_DISPLAY_NAME, &self.display_name)
71
.await;
67
if let Err(e) = session.insert(SESSION_USER_ID, self.user_id).await {
68
tracing::error!(error = %e, "failed to save user_id to session");
69
}
70
if let Err(e) = session.insert(SESSION_USERNAME, &self.username).await {
71
tracing::error!(error = %e, "failed to save username to session");
72
}
73
if let Err(e) = session.insert(SESSION_DISPLAY_NAME, &self.display_name).await {
74
tracing::error!(error = %e, "failed to save display_name to session");
75
}
72
76
}
73
77
}
74
78
@@ -149,8 +153,12 @@
149
153
let challenge = pkce_challenge(&verifier);
150
154
let oauth_state = generate_state_nonce();
151
155
152
let _ = session.insert(SESSION_PKCE_VERIFIER, &verifier).await;
153
let _ = session.insert(SESSION_OAUTH_STATE, &oauth_state).await;
156
if let Err(e) = session.insert(SESSION_PKCE_VERIFIER, &verifier).await {
157
tracing::error!(error = %e, "failed to save PKCE verifier to session");
158
}
159
if let Err(e) = session.insert(SESSION_OAUTH_STATE, &oauth_state).await {
160
tracing::error!(error = %e, "failed to save OAuth state to session");
161
}
154
162
155
163
let url = format!(
156
164
"{}/oauth/authorize?response_type=code&client_id={}&redirect_uri={}&state={}&code_challenge={}&code_challenge_method=S256",
@@ -189,8 +197,12 @@
189
197
};
190
198
191
199
// Clean up OAuth session data
192
let _ = session.remove::<String>(SESSION_OAUTH_STATE).await;
193
let _ = session.remove::<String>(SESSION_PKCE_VERIFIER).await;
200
if let Err(e) = session.remove::<String>(SESSION_OAUTH_STATE).await {
201
tracing::warn!(error = %e, "failed to remove OAuth state from session");
202
}
203
if let Err(e) = session.remove::<String>(SESSION_PKCE_VERIFIER).await {
204
tracing::warn!(error = %e, "failed to remove PKCE verifier from session");
205
}
194
206
195
207
// Exchange code for token (retry up to 2 attempts on network/5xx errors)
196
208
let token_url = format!("{}/oauth/token", state.config.mnw_base_url);
@@ -374,6 +386,8 @@
374
386
/// `POST /auth/logout` — flush session, redirect home.
375
387
#[tracing::instrument(skip_all)]
376
388
pub async fn logout(session: Session) -> impl IntoResponse {
377
let _ = session.flush().await;
389
if let Err(e) = session.flush().await {
390
tracing::warn!(error = %e, "failed to flush session on logout");
391
}
378
392
Redirect::to("/")
379
393
}
OldNewLine
@@ -5,3 +5,5 @@
5
5
6
6
[dependencies]
7
7
chrono = { workspace = true }
8
serde = { workspace = true }
9
sqlx = { workspace = true }
OldNewLine
@@ -89,13 +89,12 @@
89
89
user_id: Uuid,
90
90
community_id: Uuid,
91
91
) -> Result<Option<CommunityRole>, Response> {
92
let role_str = mt_db::queries::get_user_role(db, user_id, community_id)
92
mt_db::queries::get_user_role(db, user_id, community_id)
93
93
.await
94
94
.map_err(|e| {
95
95
tracing::error!(error = ?e, "db error fetching role");
96
96
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response()
97
})?;
98
Ok(role_str.and_then(|s| CommunityRole::from_db(&s)))
97
})
99
98
}
100
99
101
100
/// Look up a user by username, returning 422 if not found.
OldNewLine
@@ -176,7 +176,7 @@
176
176
.map(|b| BanListRow {
177
177
username: b.username,
178
178
display_name: b.display_name,
179
ban_type: b.ban_type,
179
ban_type: b.ban_type.to_string(),
180
180
reason: b.reason,
181
181
expires: b.expires_at.map(mt_core::time_format::relative_timestamp),
182
182
created: mt_core::time_format::relative_timestamp(b.created_at),
@@ -427,7 +427,7 @@
427
427
.into_iter()
428
428
.map(|e| ModLogRow {
429
429
actor: e.actor_username,
430
action: e.action,
430
action: e.action.to_string(),
431
431
target: e.target_username,
432
432
reason: e.reason,
433
433
timestamp: mt_core::time_format::relative_timestamp(e.created_at),
OldNewLine
@@ -89,7 +89,7 @@
89
89
// Upload to S3
90
90
s3.upload(&s3_key, validated_ct, data).await.map_err(|e| {
91
91
tracing::error!(error = %e, "S3 upload failed");
92
(StatusCode::INTERNAL_SERVER_ERROR, "Upload failed.").into_response()
92
StatusCode::INTERNAL_SERVER_ERROR.into_response()
93
93
})?;
94
94
95
95
// Record in DB