Integrate Vantage live map for guild claims [skip ci]

This commit is contained in:
SCOPEDD committed 2026-09-30 11:02:51 -04:00
1 parent 52de8c3031
commit 602e7fd0f0
17 files changed
+470 -21

No files matched your search

+3
View File
@@ -506,6 +506,9 @@ const MIGRATIONS: &[&str] = &[
PRIMARY KEY (post_id, user_uuid)
);
"#,
r#"
ALTER TABLE game_servers ADD COLUMN map_url TEXT NOT NULL DEFAULT '';
"#,
];
pub async fn connect(data_dir: &Path) -> Result<SqlitePool> {
+20 -2
View File
@@ -61,6 +61,7 @@ fn clip(s: &str, max: usize) -> String {
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct ServerRow {
pub instance_id: String,
pub map_url: String,
pub id: i64,
pub name: String,
#[serde(skip)]
@@ -832,6 +833,8 @@ pub struct ServerInput {
allowed_groups: Vec<String>,
#[serde(default)]
require_launcher: bool,
#[serde(default)]
map_url: String,
}
fn default_access() -> String {
@@ -849,6 +852,18 @@ impl ServerInput {
if self.access == "groups" && self.allowed_groups.is_empty() {
return Err(AppError::bad_request("pick at least one group"));
}
if !self.map_url.trim().is_empty() {
let url = reqwest::Url::parse(self.map_url.trim()).map_err(|_| AppError::bad_request("map URL must be an absolute URL"))?;
if url.scheme() != "https" && !(url.scheme() == "http" && matches!(url.host_str(), Some("localhost" | "127.0.0.1"))) {
return Err(AppError::bad_request("map URL must use HTTPS (or localhost for development)"));
}
if url.username() != "" || url.password().is_some() || url.fragment().is_some() || url.query().is_some() {
return Err(AppError::bad_request("map URL cannot contain credentials, query parameters, or a fragment"));
}
if !matches!(url.path().rsplit('/').next(), Some("manifest.json" | "world.json")) {
return Err(AppError::bad_request("map URL must end with manifest.json or world.json"));
}
}
Ok(())
}
}
@@ -865,7 +880,7 @@ pub async fn create(_: AdminUser, State(state): State<AppState>, Json(input): Js
input.validate()?;
let token = new_token();
let id: i64 = sqlx::query_scalar(
"INSERT INTO game_servers (name, token_hash, token_hint, access, allowed_groups, require_launcher, created_at, instance_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
"INSERT INTO game_servers (name, token_hash, token_hint, access, allowed_groups, require_launcher, created_at, instance_id, map_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
)
.bind(input.name.trim())
.bind(hash_token(&token))
@@ -875,6 +890,7 @@ pub async fn create(_: AdminUser, State(state): State<AppState>, Json(input): Js
.bind(input.require_launcher)
.bind(now())
.bind(&input.instance_id)
.bind(input.map_url.trim())
.fetch_one(&state.db)
.await?;
let server = get_server(&state, id).await?;
@@ -889,12 +905,13 @@ pub async fn update(
) -> AppResult<Json<ServerView>> {
input.validate()?;
get_server(&state, id).await?;
sqlx::query("UPDATE game_servers SET name = ?, access = ?, allowed_groups = ?, require_launcher = ?, instance_id = ? WHERE id = ?")
sqlx::query("UPDATE game_servers SET name = ?, access = ?, allowed_groups = ?, require_launcher = ?, instance_id = ?, map_url = ? WHERE id = ?")
.bind(input.name.trim())
.bind(&input.access)
.bind(serde_json::to_string(&input.allowed_groups).unwrap_or_else(|_| "[]".into()))
.bind(input.require_launcher)
.bind(&input.instance_id)
.bind(input.map_url.trim())
.bind(id)
.execute(&state.db)
.await?;
@@ -1078,6 +1095,7 @@ pub async fn public_servers(State(state): State<AppState>) -> AppResult<Json<Val
"id": s.id,
"name": s.name,
"instance_id": s.instance_id,
"map_url": s.map_url,
"online": is_online,
"players_online": if is_online { s.online_count } else { 0 },
"players_max": s.max_players,
@@ -18,6 +18,7 @@
<label class="field">Name<input bind:value={draft.name} maxlength="48" placeholder="Survival SMP" /></label>
<label class="field">Launcher instance<select bind:value={draft.instance_id}><option value="">Unlinked</option>{#each instances as instance}<option value={instance.id}>{instance.name}</option>{/each}</select></label>
<label class="field">Vantage live map manifest URL<input type="url" bind:value={draft.map_url} placeholder="https://maps.example.com/world.json" /><span class="tiny muted">Optional. Use a public Vantage render or a session-gated same-origin endpoint. The URL is visible to launcher users.</span></label>
<div class="field">
<span class="lbl">Who can join</span>
<div class="opts">
+2 -1
View File
@@ -92,6 +92,7 @@ export interface AuthServerInfo { public_url: string; yggdrasil_url: string; pub
export interface GameServer {
instance_id: string;
map_url: string;
id: number;
name: string;
token_hint: string;
@@ -140,7 +141,7 @@ export interface ServerDetail {
export interface UserActivity { servers: PlayerStats[]; events: ServerEvent[]; online_on: { id: number; name: string }[] }
export type ServerDraft = { instance_id?: string; name: string; access: 'all' | 'members' | 'groups'; allowed_groups: string[]; require_launcher: boolean };
export type ServerDraft = { instance_id?: string; name: string; map_url?: string; access: 'all' | 'members' | 'groups'; allowed_groups: string[]; require_launcher: boolean };
export interface HostedDownload {
platform: 'windows' | 'mac' | 'linux';
+1 -1
View File
@@ -78,7 +78,7 @@
function openEdit() {
if (!s) return;
draft = { instance_id: s.instance_id, name: s.name, access: s.access, allowed_groups: [...s.allowed_groups], require_launcher: s.require_launcher };
draft = { instance_id: s.instance_id, name: s.name, map_url: s.map_url, access: s.access, allowed_groups: [...s.allowed_groups], require_launcher: s.require_launcher };
editOpen = true;
}
async function saveEdit() {