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
@@ -77,6 +77,107 @@ const MIGRATIONS: &[&str] = &[
|
||||
);
|
||||
CREATE INDEX events_created ON events(created_at);
|
||||
"#,
|
||||
// 2: Yggdrasil auth server (UUIDs, skins, capes, sessions) and game
|
||||
// server integration (plugin/mod tracking)
|
||||
r#"
|
||||
ALTER TABLE users ADD COLUMN uuid TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE users ADD COLUMN skin_hash TEXT;
|
||||
ALTER TABLE users ADD COLUMN skin_model TEXT NOT NULL DEFAULT 'classic';
|
||||
ALTER TABLE users ADD COLUMN cape_id INTEGER;
|
||||
ALTER TABLE users ADD COLUMN status_reason TEXT;
|
||||
|
||||
CREATE TABLE capes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
allowed_groups TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE ygg_tokens (
|
||||
access_token TEXT PRIMARY KEY,
|
||||
client_token TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX ygg_tokens_user ON ygg_tokens(user_id);
|
||||
CREATE TABLE ygg_sessions (
|
||||
server_id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
ip TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE player_keys (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
private_pem TEXT NOT NULL,
|
||||
public_pem TEXT NOT NULL,
|
||||
signature_v1 TEXT NOT NULL,
|
||||
signature_v2 TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
refreshed_after TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE launcher_sessions (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX launcher_sessions_user ON launcher_sessions(user_id, created_at);
|
||||
|
||||
CREATE TABLE game_servers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
token_hint TEXT NOT NULL,
|
||||
access TEXT NOT NULL DEFAULT 'all',
|
||||
allowed_groups TEXT NOT NULL DEFAULT '[]',
|
||||
require_launcher INTEGER NOT NULL DEFAULT 0,
|
||||
software TEXT,
|
||||
mc_version TEXT,
|
||||
plugin_version TEXT,
|
||||
online_mode INTEGER,
|
||||
max_players INTEGER NOT NULL DEFAULT 0,
|
||||
online_count INTEGER NOT NULL DEFAULT 0,
|
||||
tps REAL,
|
||||
last_seen TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE server_online (
|
||||
server_id INTEGER NOT NULL REFERENCES game_servers(id) ON DELETE CASCADE,
|
||||
uuid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
joined_at TEXT NOT NULL,
|
||||
PRIMARY KEY (server_id, uuid)
|
||||
);
|
||||
CREATE TABLE player_stats (
|
||||
server_id INTEGER NOT NULL REFERENCES game_servers(id) ON DELETE CASCADE,
|
||||
uuid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
playtime_secs INTEGER NOT NULL DEFAULT 0,
|
||||
joins INTEGER NOT NULL DEFAULT 0,
|
||||
deaths INTEGER NOT NULL DEFAULT 0,
|
||||
player_kills INTEGER NOT NULL DEFAULT 0,
|
||||
mob_kills INTEGER NOT NULL DEFAULT 0,
|
||||
blocks_broken INTEGER NOT NULL DEFAULT 0,
|
||||
blocks_placed INTEGER NOT NULL DEFAULT 0,
|
||||
messages INTEGER NOT NULL DEFAULT 0,
|
||||
first_seen TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
PRIMARY KEY (server_id, uuid)
|
||||
);
|
||||
CREATE INDEX player_stats_uuid ON player_stats(uuid);
|
||||
CREATE TABLE server_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_id INTEGER NOT NULL REFERENCES game_servers(id) ON DELETE CASCADE,
|
||||
uuid TEXT,
|
||||
name TEXT,
|
||||
kind TEXT NOT NULL,
|
||||
detail TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX server_events_server ON server_events(server_id, id);
|
||||
CREATE INDEX server_events_uuid ON server_events(uuid, id);
|
||||
"#,
|
||||
];
|
||||
|
||||
pub async fn connect(data_dir: &Path) -> Result<SqlitePool> {
|
||||
@@ -112,6 +213,20 @@ async fn migrate(pool: &SqlitePool) -> Result<()> {
|
||||
tx.commit().await?;
|
||||
tracing::info!("applied database migration {version}");
|
||||
}
|
||||
backfill_uuids(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts created before the auth server existed get the offline-mode UUID
|
||||
/// for their name — the one offline servers already knew them by.
|
||||
async fn backfill_uuids(pool: &SqlitePool) -> Result<()> {
|
||||
let missing: Vec<(i64, String)> = sqlx::query_as("SELECT id, username FROM users WHERE uuid = ''").fetch_all(pool).await?;
|
||||
for (id, name) in missing {
|
||||
sqlx::query("UPDATE users SET uuid = ? WHERE id = ?").bind(scopenet_shared::offline_uuid(&name)).bind(id).execute(pool).await?;
|
||||
}
|
||||
if sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM sqlite_master WHERE name = 'users_uuid'").fetch_one(pool).await? == 0 {
|
||||
sqlx::raw_sql("CREATE UNIQUE INDEX users_uuid ON users(uuid)").execute(pool).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user