Add Docker image, Compose, CI/CD workflows and docs
- Multi-arch (amd64/arm64) panel image: static musl binary on scratch, cross-compiled with cargo-zigbuild (no QEMU), non-root, healthcheck - docker-compose.yml + .env.example for one-command deployment - CI: fmt, clippy, tests, web type-checks, Windows launcher build, real-network installs of vanilla/Fabric/Quilt/Forge/NeoForge, Docker build - Release: Windows NSIS installer with baked-in panel URL, GHCR image push and GitHub release (tag push or manual dispatch) - README, admin guide, Microsoft auth setup, architecture, development - rustfmt config and formatting pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
54 files changed
+1019
-219
No files matched your search
@@ -63,7 +63,12 @@ fn multipart(fields: &[(&str, &str)], file: (&str, &[u8])) -> (String, Vec<u8>)
|
||||
for (k, v) in fields {
|
||||
write!(body, "--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n").unwrap();
|
||||
}
|
||||
write!(body, "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: application/octet-stream\r\n\r\n", file.0).unwrap();
|
||||
write!(
|
||||
body,
|
||||
"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: application/octet-stream\r\n\r\n",
|
||||
file.0
|
||||
)
|
||||
.unwrap();
|
||||
body.extend_from_slice(file.1);
|
||||
write!(body, "\r\n--{boundary}--\r\n").unwrap();
|
||||
(format!("multipart/form-data; boundary={boundary}"), body)
|
||||
@@ -105,9 +110,7 @@ async fn admin_only_routes_are_guarded() {
|
||||
assert_eq!(s, StatusCode::UNAUTHORIZED);
|
||||
|
||||
let admin = t.login("admin", "supersecret").await;
|
||||
let (s, v) = t
|
||||
.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Steve", "password": "password123"})))
|
||||
.await;
|
||||
let (s, v) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Steve", "password": "password123"}))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!(v["uuid"], scopenet_shared::offline_uuid("Steve"));
|
||||
|
||||
@@ -188,12 +191,20 @@ async fn instance_visibility_and_zip_upload() {
|
||||
// Plain zip without version info → needs the fallback fields.
|
||||
let zip = zip_bytes(&[("MyPack/mods/cool.jar", b"jarjar"), ("MyPack/config/x.toml", b"a=1"), ("MyPack/logs/latest.log", b"junk")]);
|
||||
let (ct, body) = multipart(&[], ("pack.zip", &zip));
|
||||
let req = Request::post("/api/admin/instances/public/import/upload").header("authorization", format!("Bearer {admin}")).header("content-type", &ct).body(Body::from(body)).unwrap();
|
||||
let req = Request::post("/api/admin/instances/public/import/upload")
|
||||
.header("authorization", format!("Bearer {admin}"))
|
||||
.header("content-type", &ct)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
let (s, v) = t.send(req).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST, "{v}");
|
||||
|
||||
let (ct, body) = multipart(&[("mc_version", "1.20.1"), ("loader", "vanilla")], ("pack.zip", &zip));
|
||||
let req = Request::post("/api/admin/instances/public/import/upload").header("authorization", format!("Bearer {admin}")).header("content-type", &ct).body(Body::from(body)).unwrap();
|
||||
let req = Request::post("/api/admin/instances/public/import/upload")
|
||||
.header("authorization", format!("Bearer {admin}"))
|
||||
.header("content-type", &ct)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
let (s, v) = t.send(req).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
let paths: Vec<&str> = v["files"].as_array().unwrap().iter().map(|f| f["path"].as_str().unwrap()).collect();
|
||||
@@ -232,7 +243,11 @@ async fn mrpack_upload_sets_versions_and_overrides() {
|
||||
("client-overrides/config/a.json", b"{\"client\":1}"),
|
||||
]);
|
||||
let (ct, body) = multipart(&[], ("fab.mrpack", &zip));
|
||||
let req = Request::post("/api/admin/instances/pack/import/upload").header("authorization", format!("Bearer {admin}")).header("content-type", &ct).body(Body::from(body)).unwrap();
|
||||
let req = Request::post("/api/admin/instances/pack/import/upload")
|
||||
.header("authorization", format!("Bearer {admin}"))
|
||||
.header("content-type", &ct)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
let (s, v) = t.send(req).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!(v["instance"]["mc_version"], "1.21.1");
|
||||
@@ -261,11 +276,19 @@ async fn branding_roundtrip_and_media_validation() {
|
||||
assert_eq!(m["branding"]["colors"]["accent"], "#ff5500");
|
||||
|
||||
let (ct, body) = multipart(&[], ("evil.svg", b"<svg onload=alert(1)>"));
|
||||
let req = Request::post("/api/admin/uploads").header("authorization", format!("Bearer {admin}")).header("content-type", &ct).body(Body::from(body)).unwrap();
|
||||
let req = Request::post("/api/admin/uploads")
|
||||
.header("authorization", format!("Bearer {admin}"))
|
||||
.header("content-type", &ct)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
let (s, _) = t.send(req).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
let (ct, body) = multipart(&[], ("logo.png", b"\x89PNG"));
|
||||
let req = Request::post("/api/admin/uploads").header("authorization", format!("Bearer {admin}")).header("content-type", &ct).body(Body::from(body)).unwrap();
|
||||
let req = Request::post("/api/admin/uploads")
|
||||
.header("authorization", format!("Bearer {admin}"))
|
||||
.header("content-type", &ct)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
let (s, v) = t.send(req).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
assert!(v["url"].as_str().unwrap().starts_with("/uploads/"));
|
||||
|
||||
Reference in new issue
Block a user