Complete private authentication, server integrations and activity reporting
Add Fabric/Forge version builds and Paper integration, preserve permanent player identities across renames, harden session authorization, and surface privacy-conscious launcher/server activity in the panel.
This commit is contained in:
commit
a1f19e86c6
74 files changed
+2108
-95
No files matched your search
@@ -0,0 +1,106 @@
|
||||
//! Metadata-only audit trail. Never persist passwords, tokens, chat or command arguments.
|
||||
use crate::{
|
||||
auth::{AdminUser, UserRow},
|
||||
error::AppResult,
|
||||
state::AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Query, Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub async fn record(state: &AppState, user: &UserRow, source: &str, kind: &str, detail: Option<&str>) -> AppResult<()> {
|
||||
sqlx::query("INSERT INTO events (username, uuid, source, kind, detail, created_at) VALUES (?, ?, ?, ?, ?, ?)")
|
||||
.bind(&user.username)
|
||||
.bind(&user.uuid)
|
||||
.bind(source)
|
||||
.bind(kind)
|
||||
.bind(detail.map(|d| d.chars().filter(|c| !c.is_control()).take(256).collect::<String>()))
|
||||
.bind(crate::db::now())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn audit(State(state): State<AppState>, request: Request, next: Next) -> Response {
|
||||
let path = request.uri().path().to_owned();
|
||||
let mutation = matches!(request.method().as_str(), "POST" | "PUT" | "PATCH" | "DELETE")
|
||||
&& (path.starts_with("/api/admin/") || path.starts_with("/api/v1/account/"));
|
||||
let user = if mutation {
|
||||
let claims = request
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.and_then(|t| state.keys.verify(t));
|
||||
if let Some(claims) = claims {
|
||||
sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE id=? AND auth_version=? AND status='active'")
|
||||
.bind(claims.sub)
|
||||
.bind(claims.version)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let detail = format!("{} {path}", request.method());
|
||||
let response = next.run(request).await;
|
||||
if response.status().is_success() {
|
||||
if let Some(user) = user {
|
||||
if let Err(e) = record(&state, &user, "panel", "account_or_admin_change", Some(&detail)).await {
|
||||
tracing::error!("audit write failed: {}", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Filter {
|
||||
#[serde(default)]
|
||||
source: String,
|
||||
#[serde(default)]
|
||||
player: String,
|
||||
#[serde(default)]
|
||||
offset: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
pub struct Entry {
|
||||
id: i64,
|
||||
source: String,
|
||||
server: Option<String>,
|
||||
uuid: Option<String>,
|
||||
name: Option<String>,
|
||||
kind: String,
|
||||
detail: Option<String>,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
pub async fn list(_: AdminUser, State(state): State<AppState>, Query(filter): Query<Filter>) -> AppResult<Json<Vec<Entry>>> {
|
||||
let rows = sqlx::query_as(
|
||||
"SELECT * FROM (
|
||||
SELECT id, source, NULL AS server, uuid, username AS name, kind, detail, created_at FROM events
|
||||
UNION ALL
|
||||
SELECT e.id, 'server' AS source, s.name AS server, e.uuid, e.name, e.kind, e.detail, e.created_at
|
||||
FROM server_events e JOIN game_servers s ON s.id=e.server_id
|
||||
) WHERE (?='' OR source=?) AND (?='' OR name=? COLLATE NOCASE OR uuid=?)
|
||||
ORDER BY created_at DESC, source, id DESC LIMIT 100 OFFSET ?",
|
||||
)
|
||||
.bind(&filter.source)
|
||||
.bind(&filter.source)
|
||||
.bind(&filter.player)
|
||||
.bind(&filter.player)
|
||||
.bind(&filter.player)
|
||||
.bind(filter.offset.min(100_000))
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
Reference in new issue
Block a user