diff --git a/.dockerignore b/.dockerignore index 7e333c9..30cfdfe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ target .github data docs +/release-artifacts launcher/src launcher/src-tauri/icons launcher/src-tauri/gen diff --git a/.env.example b/.env.example index 6e46106..10165ea 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,8 @@ -# Copy to .env and adjust. Only ADMIN_PASSWORD really matters. +# Copy to .env and adjust. If ADMIN_PASSWORD is blank, a random first-start +# password is printed in `docker compose logs panel`. PANEL_PORT=8080 ADMIN_USERNAME=admin -ADMIN_PASSWORD=change-me-please +ADMIN_PASSWORD= JWT_SECRET= CURSEFORGE_API_KEY= MAX_UPLOAD_MB=2048 diff --git a/README.md b/README.md index 46cb765..1375e9c 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,12 @@ Brand it, publish vanilla versions or modpacks, manage players, and push changes ### 1. Run the panel ```bash -curl -O https://raw.githubusercontent.com/scopeddlol/SCOPENET-MC/main/docker-compose.yml -curl -o .env https://raw.githubusercontent.com/scopeddlol/SCOPENET-MC/main/.env.example -# edit .env → set ADMIN_PASSWORD -docker compose up -d +git clone https://github.com/scopeddlol/SCOPENET-MC.git +cd SCOPENET-MC +cp .env.example .env +# edit .env → set ADMIN_PASSWORD and PUBLIC_URL for your domain +docker compose config --quiet +docker compose up -d --build ``` Open `http://your-server:8080`, sign in as `admin`, then: @@ -64,7 +66,7 @@ Open `http://your-server:8080`, sign in as `admin`, then: 2. **Instances** → *New instance* → a version, or a Modrinth / CurseForge / .zip modpack. 3. **Settings** → how players sign in. -> Put the panel behind HTTPS (Caddy, Traefik, nginx, Cloudflare Tunnel…) before sharing it. See [docs/admin-guide.md](docs/admin-guide.md). +> Put the panel behind HTTPS (Caddy, Traefik, nginx, Cloudflare Tunnel…) before sharing it. See [docs/deployment.md](docs/deployment.md). ### 2. Build your branded launcher @@ -99,6 +101,8 @@ docker-compose.yml Panel deployment - [Official server setup](docs/server-integration.md) — private authentication, supported versions, activity and UUID protection - [Architecture](docs/architecture.md) — how the pieces fit, API reference - [Development](docs/development.md) — running everything locally, tests +- [Deployment](docs/deployment.md) — local Compose build, health check, upgrades and backups +- [Local 0.4.0 artifacts](release-artifacts/0.4.0/README.md) — Windows installer, server jars and checksums ## Configuration reference diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 0cbebd4..b6c5ba8 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -68,7 +68,7 @@ impl Default for Branding { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(default)] pub struct AboutContent { pub title: String, @@ -77,12 +77,6 @@ pub struct AboutContent { pub links: Vec, } -impl Default for AboutContent { - fn default() -> Self { - Self { title: String::new(), icon_url: None, body: String::new(), links: Vec::new() } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(default)] pub struct Background { diff --git a/docker-compose.yml b/docker-compose.yml index 497b730..4142ea9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ # SCOPENET admin panel # -# cp .env.example .env # set ADMIN_PASSWORD at least +# cp .env.example .env # set ADMIN_PASSWORD and PUBLIC_URL # docker compose up -d --build # # Then open http://localhost:8080 (or your domain behind a reverse proxy). services: panel: - image: ghcr.io/scopeddlol/scopenet-mc-panel:latest - # Or build from source: docker compose build + # Tag the image built from this checkout so an older GHCR release is not used. + image: scopenet-panel:local build: . container_name: scopenet-panel restart: unless-stopped diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..7a85e49 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,40 @@ +# Docker Compose deployment + +This deployment builds the panel and web UI from the checked-out source. It does +not need GitHub Actions or a published GHCR image. + +1. Install Docker Engine with the Compose plugin on a Linux host. Point a DNS + name at that host and arrange HTTPS through your reverse proxy. +2. Check out the commit you want to deploy, then copy `.env.example` to `.env`. + Set `ADMIN_PASSWORD` to a unique password and `PUBLIC_URL` to the public + `https://` URL. Set `SCOPENET_TRUSTED_PROXIES` only to the IP addresses of + proxies that send forwarding headers. +3. Run: + + ```bash + docker compose config --quiet + docker compose up -d --build + docker compose ps + docker compose logs --tail=100 panel + ``` + +The panel listens on `PANEL_PORT` (default `8080`). Docker checks +`/scopenet-panel healthcheck` inside the container. Wait for `healthy` in +`docker compose ps` before directing players to it. If `ADMIN_PASSWORD` is +blank on first start, the generated password appears in the panel logs. + +The named `panel-data` volume holds the database, uploads, hosted launcher +downloads, and the generated JWT signing key. Preserve that volume when +upgrading. To back it up, stop the panel, archive the volume with a temporary +container, then start it again: + +```bash +docker compose stop panel +docker run --rm -v scopenet-mc_panel-data:/data:ro -v "$PWD":/backup alpine tar czf /backup/panel-data.tar.gz -C /data . +docker compose up -d +``` + +If your Compose project has a different name, use the volume name shown by +`docker volume ls`. To upgrade, pull the desired commit and run +`docker compose up -d --build` again. Do not use `docker compose down -v` unless +you intend to delete the panel data. diff --git a/docs/server-integration.md b/docs/server-integration.md index ea1f3b0..5009394 100644 --- a/docs/server-integration.md +++ b/docs/server-integration.md @@ -71,6 +71,6 @@ gradle -p integrations -Ploader=forge -PmcVersion=1.21.1 :forge:build gradle -p integrations -Ploader=fabric -PmcVersion=26.3 :fabric:build ``` -Substitute `fabric`/`forge` and the exact version as required. JARs are in the selected module's `build/libs/`. `-PreleaseVersion=X.Y.Z` stamps the artifact and metadata. The **Server integrations** workflow builds the full matrix and uploads each JAR; **Release** attaches them beside the Windows installer. +Substitute `fabric`/`forge` and the exact version as required. JARs are in the selected module's `build/libs/`. `-PreleaseVersion=X.Y.Z` stamps the artifact and metadata. The locally built 0.4.0 matrix for 1.20.1, 1.21.1, and 26.3 plus Paper is in [`release-artifacts/0.4.0`](../release-artifacts/0.4.0/README.md), with SHA-256 checksums. The **Server integrations** workflow can rebuild the matrix when Actions usage is available again. Before production, test a valid login, wrong password, disabled account, denied group, missing launcher launch, panel outage, name-change/reconnect with the same inventory, chat count, heartbeat retry, and restart on a copy of your server world. Build/API tests do not replace a live Minecraft client/server compatibility check with your modpack. diff --git a/integrations/minecraft/src/1.20.1/java/net/scopenet/minecraft/Compat.java b/integrations/minecraft/src/1.20.1/java/net/scopenet/minecraft/Compat.java new file mode 100644 index 0000000..4462ea9 --- /dev/null +++ b/integrations/minecraft/src/1.20.1/java/net/scopenet/minecraft/Compat.java @@ -0,0 +1,13 @@ +package net.scopenet.minecraft; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +public final class Compat { + private Compat() {} + + public static ServerLevel level(ServerPlayer player) { return player.serverLevel(); } + public static String dimension(ServerPlayer player) { return level(player).dimension().location().toString(); } + public static void actionbar(ServerPlayer player, Component message) { player.displayClientMessage(message, true); } +} diff --git a/integrations/minecraft/src/1.21.1/java/net/scopenet/minecraft/Compat.java b/integrations/minecraft/src/1.21.1/java/net/scopenet/minecraft/Compat.java new file mode 100644 index 0000000..4462ea9 --- /dev/null +++ b/integrations/minecraft/src/1.21.1/java/net/scopenet/minecraft/Compat.java @@ -0,0 +1,13 @@ +package net.scopenet.minecraft; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +public final class Compat { + private Compat() {} + + public static ServerLevel level(ServerPlayer player) { return player.serverLevel(); } + public static String dimension(ServerPlayer player) { return level(player).dimension().location().toString(); } + public static void actionbar(ServerPlayer player, Component message) { player.displayClientMessage(message, true); } +} diff --git a/integrations/minecraft/src/26/java/net/scopenet/minecraft/Compat.java b/integrations/minecraft/src/26/java/net/scopenet/minecraft/Compat.java new file mode 100644 index 0000000..3adf295 --- /dev/null +++ b/integrations/minecraft/src/26/java/net/scopenet/minecraft/Compat.java @@ -0,0 +1,13 @@ +package net.scopenet.minecraft; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +public final class Compat { + private Compat() {} + + public static ServerLevel level(ServerPlayer player) { return player.level(); } + public static String dimension(ServerPlayer player) { return level(player).dimension().identifier().toString(); } + public static void actionbar(ServerPlayer player, Component message) { player.sendSystemMessage(message, true); } +} diff --git a/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java b/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java index 0263f31..09be90e 100644 --- a/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java +++ b/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java @@ -78,10 +78,10 @@ public final class Bridge { Integration current = integration; if (current == null) return false; if (!current.settings().guildsEnabled() || !current.settings().landClaimingEnabled()) return true; - String dimension = player.serverLevel().dimension().location().toString(); + String dimension = Compat.dimension(player); ChunkCheckResult check = current.checkChunk(dimension, pos.getX() >> 4, pos.getZ() >> 4, player.getUUID()); if (check.allowed()) return true; - player.displayClientMessage(Component.literal("This chunk is protected by a guild or the claim check is unavailable."), true); + Compat.actionbar(player, Component.literal("This chunk is protected by a guild or the claim check is unavailable.")); return false; } diff --git a/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/BlockItemMixin.java b/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/BlockItemMixin.java index 845bccb..21b1f69 100644 --- a/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/BlockItemMixin.java +++ b/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/BlockItemMixin.java @@ -6,6 +6,7 @@ import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.core.registries.BuiltInRegistries; import net.scopenet.minecraft.Bridge; +import net.scopenet.minecraft.Compat; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @@ -25,7 +26,7 @@ public abstract class BlockItemMixin { Bridge.stat(player, "blocks_placed", 1); String material = BuiltInRegistries.BLOCK.getKey(context.getLevel().getBlockState(context.getClickedPos()).getBlock()) .getPath().toUpperCase(java.util.Locale.ROOT); - Bridge.action(player, "block_placed:" + material + "@" + player.serverLevel().dimension().location(), 1); + Bridge.action(player, "block_placed:" + material + "@" + Compat.dimension(player), 1); } } } diff --git a/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/PlayerMixin.java b/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/PlayerMixin.java index a8d61dc..ccd4b7f 100644 --- a/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/PlayerMixin.java +++ b/integrations/minecraft/src/main/java/net/scopenet/minecraft/mixin/PlayerMixin.java @@ -6,6 +6,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.entity.EntityType; import net.minecraft.core.registries.BuiltInRegistries; import net.scopenet.minecraft.Bridge; +import net.scopenet.minecraft.Compat; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -20,7 +21,7 @@ public abstract class PlayerMixin { key = "blocks_broken"; String material = BuiltInRegistries.BLOCK.getKey((Block) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT); Bridge.action((ServerPlayer) (Object) this, "block_broken:" + material + "@" - + ((ServerPlayer) (Object) this).serverLevel().dimension().location(), amount); + + Compat.dimension((ServerPlayer) (Object) this), amount); } else if (stat.getType() == Stats.ENTITY_KILLED) { String entity = BuiltInRegistries.ENTITY_TYPE.getKey((EntityType) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT); Bridge.action((ServerPlayer) (Object) this, "mob_kills:" + entity, amount); diff --git a/launcher/src-tauri/src/secrets.rs b/launcher/src-tauri/src/secrets.rs index 30f1e97..5e703bc 100644 --- a/launcher/src-tauri/src/secrets.rs +++ b/launcher/src-tauri/src/secrets.rs @@ -35,7 +35,9 @@ const SERVICE: &str = "net.scopenet.launcher"; fn load_or_create_key(dir: &std::path::Path) -> [u8; 32] { let b64 = base64::engine::general_purpose::STANDARD; - #[cfg(any(windows, target_os = "macos"))] + // Unit tests use their own temporary key file; the OS keychain can prompt + // for desktop access and block unattended checks. + #[cfg(all(not(test), any(windows, target_os = "macos")))] { if let Ok(entry) = keyring::Entry::new(SERVICE, "secrets-key") { if let Ok(existing) = entry.get_password() { diff --git a/panel/server/src/lib.rs b/panel/server/src/lib.rs index fae29b8..07ab1c1 100644 --- a/panel/server/src/lib.rs +++ b/panel/server/src/lib.rs @@ -1,4 +1,7 @@ //! SCOPENET admin panel. +// SQLx maps several multi-column queries directly to tuples; aliases would +// obscure the selected column order without simplifying the query boundary. +#![allow(clippy::type_complexity)] pub mod auth; pub mod config; diff --git a/release-artifacts/0.4.0/README.md b/release-artifacts/0.4.0/README.md new file mode 100644 index 0000000..5b5b660 --- /dev/null +++ b/release-artifacts/0.4.0/README.md @@ -0,0 +1,20 @@ +# Local 0.4.0 build artifacts + +These files were built from PR #3 source with GitHub Actions disabled. + +- `SCOPENET Launcher_0.4.0_x64-setup.exe`: Windows x64 NSIS installer, + cross-compiled on macOS with Tauri and cargo-xwin. It is unsigned and asks + the player for the panel URL on first launch. +- `scopenet-fabric-{1.20.1,1.21.1,26.3}-0.4.0.jar`: server-side Fabric mods. +- `scopenet-forge-{1.20.1,1.21.1,26.3}-0.4.0.jar`: server-side Forge mods. +- `scopenet-paper-0.4.0.jar`: Paper plugin, built against Spigot API 1.20.1. + +The repository has a Forge server mod but no separate NeoForge server mod, so +these artifacts do not include a NeoForge JAR. + +The build commands and integration tests passed locally. Every JAR passed ZIP +integrity checks and contains its loader descriptor. `SHA256SUMS` records the +artifact hashes; run `shasum -a 256 -c SHA256SUMS` from this folder to verify. + +Minecraft client/server runtime testing and installation of the Windows +installer on Windows still need to be done before a public release. diff --git a/release-artifacts/0.4.0/SCOPENET Launcher_0.4.0_x64-setup.exe b/release-artifacts/0.4.0/SCOPENET Launcher_0.4.0_x64-setup.exe new file mode 100644 index 0000000..1bb9794 Binary files /dev/null and b/release-artifacts/0.4.0/SCOPENET Launcher_0.4.0_x64-setup.exe differ diff --git a/release-artifacts/0.4.0/SHA256SUMS b/release-artifacts/0.4.0/SHA256SUMS new file mode 100644 index 0000000..0906a8f --- /dev/null +++ b/release-artifacts/0.4.0/SHA256SUMS @@ -0,0 +1,8 @@ +e8578861c71edaf4ffe8d6e4edad98c048a0d595e67196041a484eaa2e1bd6fe SCOPENET Launcher_0.4.0_x64-setup.exe +3dd93d31de1bf82d4e5e81a6f964b6f7179b44f4cc47e8ebed7e9751c8022d48 scopenet-fabric-1.20.1-0.4.0.jar +fb7ac37ed2313ec489f0b355b074904809fdec6a271b4c1e0d545f6f88cde9e2 scopenet-fabric-1.21.1-0.4.0.jar +2ef7eef23bfbe6731372cba76a433e3201d3a080d24d0567094b1c59f998740b scopenet-fabric-26.3-0.4.0.jar +1c8256bb3cef5ebc44090b891e1588c04c78158170b81b2a853a11f8b64b4561 scopenet-forge-1.20.1-0.4.0.jar +e94fbd0bc76a8a93a284d6bbf8a52db8fa014dd702c4159278bee0dfe71afdaa scopenet-forge-1.21.1-0.4.0.jar +9a1f4c5796f582a7c636226b72467fc5b734519a0ac845c6c2b33ba0898b738c scopenet-forge-26.3-0.4.0.jar +75560c64d08b512dc53845a0da7de468d01951963af02d10fd0b236f1e71e4f8 scopenet-paper-0.4.0.jar diff --git a/release-artifacts/0.4.0/scopenet-fabric-1.20.1-0.4.0.jar b/release-artifacts/0.4.0/scopenet-fabric-1.20.1-0.4.0.jar new file mode 100644 index 0000000..19c7467 Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-fabric-1.20.1-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-fabric-1.21.1-0.4.0.jar b/release-artifacts/0.4.0/scopenet-fabric-1.21.1-0.4.0.jar new file mode 100644 index 0000000..669aad1 Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-fabric-1.21.1-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-fabric-26.3-0.4.0.jar b/release-artifacts/0.4.0/scopenet-fabric-26.3-0.4.0.jar new file mode 100644 index 0000000..0c9cf9e Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-fabric-26.3-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-forge-1.20.1-0.4.0.jar b/release-artifacts/0.4.0/scopenet-forge-1.20.1-0.4.0.jar new file mode 100644 index 0000000..7d0aa71 Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-forge-1.20.1-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-forge-1.21.1-0.4.0.jar b/release-artifacts/0.4.0/scopenet-forge-1.21.1-0.4.0.jar new file mode 100644 index 0000000..a831927 Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-forge-1.21.1-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-forge-26.3-0.4.0.jar b/release-artifacts/0.4.0/scopenet-forge-26.3-0.4.0.jar new file mode 100644 index 0000000..fe29d37 Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-forge-26.3-0.4.0.jar differ diff --git a/release-artifacts/0.4.0/scopenet-paper-0.4.0.jar b/release-artifacts/0.4.0/scopenet-paper-0.4.0.jar new file mode 100644 index 0000000..638775c Binary files /dev/null and b/release-artifacts/0.4.0/scopenet-paper-0.4.0.jar differ