| 7 |
7 |
|
use chrono::{DateTime, Duration, Utc};
|
| 8 |
8 |
|
use uuid::Uuid;
|
| 9 |
9 |
|
|
| 10 |
|
- |
use mt_core::types::{CommunityRole, ModAction};
|
|
10 |
+ |
use mt_core::types::{CommunityRole, CommunityState, ModAction};
|
| 11 |
11 |
|
|
| 12 |
12 |
|
use crate::auth;
|
| 13 |
13 |
|
use crate::templates::*;
|
| 313 |
313 |
|
}
|
| 314 |
314 |
|
Ok((community, role))
|
| 315 |
315 |
|
}
|
|
316 |
+ |
|
|
317 |
+ |
// ============================================================================
|
|
318 |
+ |
// Superadmin authorization
|
|
319 |
+ |
// ============================================================================
|
|
320 |
+ |
|
|
321 |
+ |
/// Whether `user` is the configured platform admin.
|
|
322 |
+ |
///
|
|
323 |
+ |
/// Platform admin is a single user (env var `PLATFORM_ADMIN_ID`); a real
|
|
324 |
+ |
/// permissions system is deferred. See `docs/todo.md` § Community Moderation
|
|
325 |
+ |
/// Enforcement.
|
|
326 |
+ |
pub(crate) fn is_platform_admin(state: &AppState, user: &auth::SessionUser) -> bool {
|
|
327 |
+ |
state
|
|
328 |
+ |
.config
|
|
329 |
+ |
.platform_admin_id
|
|
330 |
+ |
.is_some_and(|id| id == user.user_id)
|
|
331 |
+ |
}
|
|
332 |
+ |
|
|
333 |
+ |
/// True if the user can perform mod actions in this community: either a
|
|
334 |
+ |
/// community Owner/Moderator, or the platform admin (who can act on any
|
|
335 |
+ |
/// community). Used by [`check_community_state`] and by the state-change route.
|
|
336 |
+ |
pub(crate) fn is_mod_or_superadmin(
|
|
337 |
+ |
state: &AppState,
|
|
338 |
+ |
user: &auth::SessionUser,
|
|
339 |
+ |
role: &Option<CommunityRole>,
|
|
340 |
+ |
) -> bool {
|
|
341 |
+ |
is_mod_or_owner(role) || is_platform_admin(state, user)
|
|
342 |
+ |
}
|
|
343 |
+ |
|
|
344 |
+ |
/// Fetch community + verify the user is a mod, owner, or platform admin.
|
|
345 |
+ |
///
|
|
346 |
+ |
/// Returns `(community, role)` — `role` is `None` when the user is the platform
|
|
347 |
+ |
/// admin but holds no role in this specific community.
|
|
348 |
+ |
#[tracing::instrument(skip_all)]
|
|
349 |
+ |
pub(crate) async fn require_mod_or_superadmin(
|
|
350 |
+ |
state: &AppState,
|
|
351 |
+ |
slug: &str,
|
|
352 |
+ |
user: &auth::SessionUser,
|
|
353 |
+ |
) -> Result<(mt_db::queries::CommunityRow, Option<CommunityRole>), Response> {
|
|
354 |
+ |
let community = get_community(&state.db, slug).await?;
|
|
355 |
+ |
let role = get_role(&state.db, user.user_id, community.id).await?;
|
|
356 |
+ |
if !is_mod_or_superadmin(state, user, &role) {
|
|
357 |
+ |
return Err((StatusCode::FORBIDDEN, "Forbidden").into_response());
|
|
358 |
+ |
}
|
|
359 |
+ |
Ok((community, role))
|
|
360 |
+ |
}
|
|
361 |
+ |
|
|
362 |
+ |
// ============================================================================
|
|
363 |
+ |
// Community state enforcement
|
|
364 |
+ |
// ============================================================================
|
|
365 |
+ |
|
|
366 |
+ |
/// Whether a write attempt is starting a new thread or extending an existing
|
|
367 |
+ |
/// one. Restricted communities block `NewThread` for non-mods but still accept
|
|
368 |
+ |
/// `ContinueExisting` writes.
|
|
369 |
+ |
#[derive(Debug, Clone, Copy)]
|
|
370 |
+ |
pub(crate) enum WriteScope {
|
|
371 |
+ |
NewThread,
|
|
372 |
+ |
ContinueExisting,
|
|
373 |
+ |
}
|
|
374 |
+ |
|
|
375 |
+ |
/// Convenience: combine role lookup with [`check_community_state`]. Use this
|
|
376 |
+ |
/// in write handlers that don't already need the role for other purposes.
|
|
377 |
+ |
#[tracing::instrument(skip_all)]
|
|
378 |
+ |
pub(crate) async fn check_write_state(
|
|
379 |
+ |
state: &AppState,
|
|
380 |
+ |
community: &mt_db::queries::CommunityRow,
|
|
381 |
+ |
user: &auth::SessionUser,
|
|
382 |
+ |
scope: WriteScope,
|
|
383 |
+ |
) -> Result<(), Response> {
|
|
384 |
+ |
let role = get_role(&state.db, user.user_id, community.id).await?;
|
|
385 |
+ |
let is_mod_or_super = is_mod_or_superadmin(state, user, &role);
|
|
386 |
+ |
check_community_state(community.state, scope, is_mod_or_super)
|
|
387 |
+ |
}
|
|
388 |
+ |
|
|
389 |
+ |
/// Gate a write against the community's [`CommunityState`].
|
|
390 |
+ |
///
|
|
391 |
+ |
/// Mods/owners and the platform admin bypass all state restrictions. Members
|
|
392 |
+ |
/// follow the state's `allows_*` predicates. Returns 403 with a state-specific
|
|
393 |
+ |
/// message on denial — message text is what the user will see in the toast.
|
|
394 |
+ |
///
|
|
395 |
+ |
/// Note: this is independent of [`check_write_access`] (which covers
|
|
396 |
+ |
/// suspension/ban/mute). Call both in write handlers.
|
|
397 |
+ |
#[allow(clippy::result_large_err)]
|
|
398 |
+ |
pub(crate) fn check_community_state(
|
|
399 |
+ |
community_state: CommunityState,
|
|
400 |
+ |
scope: WriteScope,
|
|
401 |
+ |
is_mod_or_super: bool,
|
|
402 |
+ |
) -> Result<(), Response> {
|
|
403 |
+ |
if is_mod_or_super {
|
|
404 |
+ |
return Ok(());
|
|
405 |
+ |
}
|
|
406 |
+ |
let allowed = match scope {
|
|
407 |
+ |
WriteScope::NewThread => community_state.allows_new_threads_for_members(),
|
|
408 |
+ |
WriteScope::ContinueExisting => community_state.allows_writes_for_members(),
|
|
409 |
+ |
};
|
|
410 |
+ |
if allowed {
|
|
411 |
+ |
return Ok(());
|
|
412 |
+ |
}
|
|
413 |
+ |
let msg = match (community_state, scope) {
|
|
414 |
+ |
(CommunityState::Restricted, WriteScope::NewThread) => {
|
|
415 |
+ |
"New threads are restricted in this community."
|
|
416 |
+ |
}
|
|
417 |
+ |
(CommunityState::Frozen, _) => "This community is frozen.",
|
|
418 |
+ |
(CommunityState::Archived, _) => "This community is archived.",
|
|
419 |
+ |
_ => "Action not allowed in the community's current state.",
|
|
420 |
+ |
};
|
|
421 |
+ |
Err((StatusCode::FORBIDDEN, msg).into_response())
|
|
422 |
+ |
}
|