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
@@ -21,6 +21,64 @@ pub async fn profile(State(state): State<AppState>, headers: HeaderMap, AuthUser
|
||||
Ok(Json(yggdrasil::player_profile(&state, &base, &user).await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UsernameInput {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub async fn set_username(
|
||||
State(state): State<AppState>,
|
||||
AuthUser(user): AuthUser,
|
||||
Json(input): Json<UsernameInput>,
|
||||
) -> AppResult<Json<scopenet_shared::AuthResponse>> {
|
||||
let name = input.username.trim();
|
||||
if !scopenet_shared::valid_username(name) {
|
||||
return Err(AppError::bad_request("usernames are 3–16 letters, numbers or underscores"));
|
||||
}
|
||||
state.login_guard.check(&user.username)?;
|
||||
if !crate::auth::verify_password(&input.password, &user.password_hash) {
|
||||
state.login_guard.fail(&user.username);
|
||||
return Err(AppError::unauthorized("incorrect password"));
|
||||
}
|
||||
state.login_guard.succeed(&user.username);
|
||||
let cutoff = (chrono::Utc::now() - chrono::Duration::seconds(90)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let online: bool = sqlx::query_scalar(
|
||||
"SELECT EXISTS(SELECT 1 FROM server_online o JOIN game_servers s ON s.id=o.server_id WHERE o.uuid=? AND s.last_seen>=?)",
|
||||
)
|
||||
.bind(&user.uuid)
|
||||
.bind(cutoff)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if online {
|
||||
return Err(AppError::conflict("disconnect from your servers before changing your username"));
|
||||
}
|
||||
let mut tx = state.db.begin().await?;
|
||||
sqlx::query("UPDATE users SET username=?, auth_version=auth_version+1 WHERE id=?")
|
||||
.bind(name)
|
||||
.bind(user.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
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(),
|
||||
})?;
|
||||
sqlx::query("DELETE FROM ygg_tokens WHERE user_id=?").bind(user.id).execute(&mut *tx).await?;
|
||||
sqlx::query("DELETE FROM ygg_sessions WHERE user_id=?").bind(user.id).execute(&mut *tx).await?;
|
||||
sqlx::query("DELETE FROM launcher_sessions WHERE user_id=?").bind(user.id).execute(&mut *tx).await?;
|
||||
sqlx::query("INSERT INTO events (username, uuid, kind, detail, created_at) VALUES (?, ?, 'username_change', ?, ?)")
|
||||
.bind(name)
|
||||
.bind(&user.uuid)
|
||||
.bind(format!("{} → {name}", user.username))
|
||||
.bind(crate::db::now())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(super::public::signed_in(&state, &reload(&state, user.id).await?).await?))
|
||||
}
|
||||
|
||||
/// Read a `file` (+ optional `model`) multipart upload.
|
||||
pub async fn read_texture_form(form: &mut Multipart) -> AppResult<(Vec<u8>, String)> {
|
||||
let mut model = String::from("classic");
|
||||
|
||||
Reference in new issue
Block a user