Panel: Yggdrasil auth server (authlib-injector), skins and capes
- Yggdrasil API per the authlib-injector spec: metadata with signing key and skin domains, authenticate/refresh/validate/invalidate/signout, join/hasJoined, profile lookup, texture upload, and the minecraftservices endpoints (chat certificates, publickeys, attributes, blocklist) - 4096-bit signing key generated once into the data volume; textures and chat certificates signed SHA1withRSA (verified with Java's own crypto) - Skins/capes stored content-addressed after validation and re-encoding; cape library with public/group/private visibility; head avatars API - Launcher login returns a game session; launcher sessions recorded for launcher-only servers; authlib-injector download mirror - Schema v2: player UUIDs (offline UUID backfilled), skins, capes, tokens, sessions, chat keys, game server tables - Remove Microsoft sign-in from the engine and panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
29 files changed
+2436
-400
No files matched your search
@@ -9,7 +9,7 @@ use argon2::Argon2;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::request::Parts;
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use scopenet_shared::{offline_uuid, PublicUser};
|
||||
use scopenet_shared::PublicUser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
@@ -81,6 +81,13 @@ pub struct UserRow {
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub last_login: Option<String>,
|
||||
/// Dashed player UUID.
|
||||
pub uuid: String,
|
||||
pub skin_hash: Option<String>,
|
||||
pub skin_model: String,
|
||||
pub cape_id: Option<i64>,
|
||||
/// Why the account is disabled (shown to the player when they're refused).
|
||||
pub status_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl UserRow {
|
||||
@@ -100,13 +107,46 @@ pub async fn public_user(state: &AppState, user: &UserRow) -> AppResult<PublicUs
|
||||
Ok(PublicUser {
|
||||
id: user.id,
|
||||
username: user.username.clone(),
|
||||
uuid: offline_uuid(&user.username),
|
||||
uuid: user.uuid.clone(),
|
||||
role: user.role.clone(),
|
||||
groups: user_groups(state, user.id).await?,
|
||||
})
|
||||
}
|
||||
|
||||
fn bearer(parts: &Parts) -> Option<&str> {
|
||||
/// Insert an account. Every account gets its offline-mode UUID, so offline
|
||||
/// and panel-authenticated servers identify players the same way.
|
||||
pub async fn create_user(
|
||||
state: &AppState,
|
||||
username: &str,
|
||||
password: &str,
|
||||
email: Option<&str>,
|
||||
role: &str,
|
||||
status: &str,
|
||||
) -> AppResult<i64> {
|
||||
let hash = hash_password(password)?;
|
||||
sqlx::query_scalar(
|
||||
"INSERT INTO users (username, password_hash, email, role, status, created_at, uuid) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
||||
)
|
||||
.bind(username)
|
||||
.bind(hash)
|
||||
.bind(email.map(str::trim).filter(|e| !e.is_empty()))
|
||||
.bind(role)
|
||||
.bind(status)
|
||||
.bind(crate::db::now())
|
||||
.bind(scopenet_shared::offline_uuid(username))
|
||||
.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"),
|
||||
e => e.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn find_user_by_name(state: &AppState, name: &str) -> AppResult<Option<UserRow>> {
|
||||
Ok(sqlx::query_as("SELECT * FROM users WHERE username = ?").bind(name.trim()).fetch_optional(&state.db).await?)
|
||||
}
|
||||
|
||||
pub fn bearer(parts: &Parts) -> Option<&str> {
|
||||
parts
|
||||
.headers
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
|
||||
Reference in new issue
Block a user