Add launcher preferences and modular landing studio [skip ci]

This commit is contained in:
SCOPEDD committed 2026-09-30 06:48:03 -04:00
1 parent 454620f21e
commit 6deff62a3a
25 files changed
+822 -682

No files matched your search

+35
View File
@@ -66,6 +66,30 @@ pub struct LandingBlock {
pub options: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct LandingTheme {
pub background: String,
pub surface: String,
pub accent: String,
pub text: String,
pub muted: String,
pub max_width: u16,
pub radius: u8,
pub font: String,
pub footer_text: String,
}
impl Default for LandingTheme {
fn default() -> Self {
Self {
background: "#090a0f".into(), surface: "#151620".into(), accent: "#6d6af5".into(),
text: "#f1f2f6".into(), muted: "#8b8f9a".into(), max_width: 1140, radius: 14,
font: "Inter".into(), footer_text: "Your worlds, one click away.".into(),
}
}
}
impl Default for LandingBlock {
fn default() -> Self {
Self {
@@ -98,6 +122,7 @@ pub struct LandingConfig {
pub blocks: Vec<LandingBlock>,
pub faqs: Vec<FaqItem>,
pub custom_css: String,
pub theme: LandingTheme,
}
impl Default for LandingConfig {
@@ -145,6 +170,7 @@ impl Default for LandingConfig {
},
],
custom_css: "".into(),
theme: LandingTheme::default(),
}
}
}
@@ -178,6 +204,15 @@ pub async fn admin_get_landing(_: AdminUser, State(state): State<AppState>) -> A
}
pub async fn admin_put_landing(_: AdminUser, State(state): State<AppState>, Json(cfg): Json<LandingConfig>) -> AppResult<Json<LandingConfig>> {
if cfg.blocks.len() > 100 || cfg.custom_css.len() > 100_000 {
return Err(AppError::bad_request("landing page exceeds the editor limits"));
}
let mut ids = std::collections::HashSet::new();
for block in &cfg.blocks {
if block.id.is_empty() || !ids.insert(&block.id) {
return Err(AppError::bad_request("landing section IDs must be unique"));
}
}
store::kv_set(&state, "landing_page", &cfg).await?;
Ok(Json(cfg))
}
+15
View File
@@ -233,6 +233,7 @@ async fn landing_page_config_and_permissions() {
assert_eq!(v["brand_name"], "SCOPENET");
assert_eq!(v["enabled"], true);
assert!(v["blocks"].as_array().unwrap().len() >= 5);
assert_eq!(v["theme"]["accent"], "#6d6af5");
// 2. Unauthenticated user cannot modify landing config
let (s, _) = t.call("PUT", "/api/admin/landing", None, Some(json!({"brand_name": "Hacked"}))).await;
@@ -243,10 +244,24 @@ async fn landing_page_config_and_permissions() {
let mut updated = v.clone();
updated["brand_name"] = json!("PixelCraft");
updated["hero_title"] = json!("Welcome to PixelCraft");
updated["theme"]["accent"] = json!("#17bebb");
updated["blocks"].as_array_mut().unwrap().push(json!({
"id": "b_custom_text", "type": "text", "enabled": true, "title": "Our story",
"subtitle": "", "options": { "body": "Built by players." }
}));
let (s, v2) = t.call("PUT", "/api/admin/landing", Some(&admin), Some(updated)).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(v2["brand_name"], "PixelCraft");
assert_eq!(v2["hero_title"], "Welcome to PixelCraft");
assert_eq!(v2["theme"]["accent"], "#17bebb");
assert_eq!(v2["blocks"].as_array().unwrap().last().unwrap()["type"], "text");
// Duplicate IDs would break drag ordering and keyed public rendering.
let mut duplicate = v2.clone();
let first = duplicate["blocks"][0].clone();
duplicate["blocks"].as_array_mut().unwrap().push(first);
let (s, _) = t.call("PUT", "/api/admin/landing", Some(&admin), Some(duplicate)).await;
assert_eq!(s, StatusCode::BAD_REQUEST);
// 4. Public endpoint reflects the updated config
let (s, v3) = t.call("GET", "/api/v1/landing", None, None).await;