Add launcher engine (scopenet-core) and admin panel server

- scopenet-shared: wire types for branding, auth, instances, manifests
- scopenet-core: Mojang versions/libraries/assets, automatic Java runtimes,
  Fabric/Quilt/Forge/NeoForge installation, file sync, launch command
  builder, Microsoft auth, servers.dat injection, server list ping
- scopenet-panel: Axum + SQLite admin API with JWT auth, users/groups,
  branding, settings, instances, Modrinth/CurseForge/zip import, file
  hosting and launch stats

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
Claude committed 2026-09-28 04:22:17 +00:00
1 parent 915f40a4c4
commit 8cdf616a94
43 files changed
+9317

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
use anyhow::Result;
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous};
use sqlx::SqlitePool;
use std::path::Path;
use std::str::FromStr;
/// Schema migrations, applied in order. Never edit a released entry —
/// append a new one instead.
const MIGRATIONS: &[&str] = &[
// 1: initial schema
r#"
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL DEFAULT 'player',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
last_login TEXT
);
CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
color TEXT NOT NULL DEFAULT '#7c5cff'
);
CREATE TABLE user_groups (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE instances (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
icon_url TEXT,
banner_url TEXT,
mc_version TEXT NOT NULL,
loader TEXT NOT NULL DEFAULT 'vanilla',
loader_version TEXT,
source_kind TEXT NOT NULL DEFAULT 'vanilla',
source_label TEXT NOT NULL DEFAULT '',
source_ref TEXT NOT NULL DEFAULT '{}',
visibility TEXT NOT NULL DEFAULT 'public',
allowed_groups TEXT NOT NULL DEFAULT '[]',
memory_min INTEGER NOT NULL DEFAULT 1024,
memory_max INTEGER NOT NULL DEFAULT 4096,
jvm_args TEXT NOT NULL DEFAULT '',
server TEXT,
featured INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
sort INTEGER NOT NULL DEFAULT 0,
revision INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE instance_files (
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
path TEXT NOT NULL,
url TEXT NOT NULL,
sha1 TEXT NOT NULL,
size INTEGER NOT NULL,
origin TEXT NOT NULL,
note TEXT,
PRIMARY KEY (instance_id, path)
);
CREATE TABLE kv (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
instance_id TEXT,
username TEXT,
kind TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX events_created ON events(created_at);
"#,
];
pub async fn connect(data_dir: &Path) -> Result<SqlitePool> {
std::fs::create_dir_all(data_dir)?;
let url = format!("sqlite://{}", data_dir.join("panel.db").display());
let opts = SqliteConnectOptions::from_str(&url)?
.create_if_missing(true)
.journal_mode(SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.foreign_keys(true);
let pool = SqlitePoolOptions::new().max_connections(8).connect_with(opts).await?;
migrate(&pool).await?;
Ok(pool)
}
pub async fn connect_memory() -> Result<SqlitePool> {
let opts = SqliteConnectOptions::from_str("sqlite::memory:")?.foreign_keys(true);
let pool = SqlitePoolOptions::new().max_connections(1).connect_with(opts).await?;
migrate(&pool).await?;
Ok(pool)
}
async fn migrate(pool: &SqlitePool) -> Result<()> {
let current: i64 = sqlx::query_scalar("PRAGMA user_version").fetch_one(pool).await?;
for (i, sql) in MIGRATIONS.iter().enumerate() {
let version = i as i64 + 1;
if version <= current {
continue;
}
let mut tx = pool.begin().await?;
sqlx::raw_sql(sql).execute(&mut *tx).await?;
sqlx::raw_sql(&format!("PRAGMA user_version = {version}")).execute(&mut *tx).await?;
tx.commit().await?;
tracing::info!("applied database migration {version}");
}
Ok(())
}
pub fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}