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:
43 files changed
+9317
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user