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:
SCOPEDD committed 2026-09-28 13:31:17 -04:00
commit a1f19e86c6
74 files changed
+2108 -95

No files matched your search

+11 -5
View File
@@ -38,6 +38,8 @@ pub fn validate_password(password: &str) -> AppResult<()> {
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
#[serde(default)]
pub version: i64,
pub sub: i64,
pub name: String,
pub role: String,
@@ -56,6 +58,7 @@ impl Keys {
pub fn issue(&self, user: &UserRow) -> AppResult<String> {
let claims = Claims {
version: user.auth_version,
sub: user.id,
name: user.username.clone(),
role: user.role.clone(),
@@ -72,6 +75,8 @@ impl Keys {
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct UserRow {
#[serde(skip)]
pub auth_version: i64,
pub id: i64,
pub username: String,
#[serde(skip)]
@@ -113,8 +118,7 @@ pub async fn public_user(state: &AppState, user: &UserRow) -> AppResult<PublicUs
})
}
/// Insert an account. Every account gets its offline-mode UUID, so offline
/// and panel-authenticated servers identify players the same way.
/// Allocate identity once. Names can change; the account UUID never does.
pub async fn create_user(
state: &AppState,
username: &str,
@@ -133,11 +137,13 @@ pub async fn create_user(
.bind(role)
.bind(status)
.bind(crate::db::now())
.bind(scopenet_shared::offline_uuid(username))
.bind(uuid::Uuid::new_v4().to_string())
.fetch_one(&state.db)
.await
.map_err(|e| match e {
sqlx::Error::Database(d) if d.message().contains("UNIQUE") => AppError::conflict("that username is taken"),
sqlx::Error::Database(d) if d.message().contains("UNIQUE") || d.message().contains("username reserved") => {
AppError::conflict("that username is taken or reserved")
}
e => e.into(),
})
}
@@ -161,7 +167,7 @@ async fn resolve(parts: &Parts, state: &AppState) -> AppResult<Option<UserRow>>
};
let user: Option<UserRow> = sqlx::query_as("SELECT * FROM users WHERE id = ?").bind(claims.sub).fetch_optional(&state.db).await?;
match user {
Some(u) if u.status == "active" => Ok(Some(u)),
Some(u) if u.status == "active" && u.auth_version == claims.version => Ok(Some(u)),
Some(u) if u.status == "pending" => Err(AppError::forbidden("your account is waiting for approval")),
_ => Err(AppError::unauthorized("account disabled or removed")),
}