From f4e15b45c9f645ddd11837fc4431491d7fa2bfa1 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 9 Oct 2026 19:21:17 +0200 Subject: [PATCH] fix: CI workflow, password reset flow, health/limits in services, session refactor, dead mixin stub cleanup --- .github/workflows/ci.yml | 132 +++++++++++ backend/.env.example | 4 + backend/Cargo.lock | 15 ++ backend/accounts-service/Cargo.toml | 5 +- .../migrations/0005_password_reset_tokens.sql | 13 ++ .../accounts-service/src/accounts/model.rs | 21 +- backend/accounts-service/src/auth/handlers.rs | 15 +- backend/accounts-service/src/auth/mod.rs | 1 + .../src/auth/reset/handlers.rs | 86 +++++++ .../accounts-service/src/auth/reset/mod.rs | 129 +++++++++++ backend/accounts-service/src/lib.rs | 23 +- backend/accounts-service/src/main.rs | 7 +- .../accounts-service/tests/avatar_upload.rs | 2 + backend/accounts-service/tests/common/mod.rs | 66 ++++++ .../accounts-service/tests/password_reset.rs | 215 ++++++++++++++++++ backend/chat-service/Cargo.toml | 3 +- backend/chat-service/src/lib.rs | 10 +- backend/chat-service/src/main.rs | 7 +- backend/chat-service/src/presence/handlers.rs | 14 +- backend/chat-service/src/presence/mod.rs | 5 +- backend/chat-service/src/presence/payload.rs | 8 +- backend/chat-service/src/presence/store.rs | 26 ++- backend/chat-service/tests/presence.rs | 33 ++- backend/configs-service/Cargo.toml | 3 +- backend/configs-service/src/lib.rs | 2 + backend/configs-service/src/main.rs | 7 +- backend/docker-compose.prod.yml | 6 + backend/gateway/Cargo.toml | 2 +- backend/gateway/src/lib.rs | 5 +- backend/gateway/src/main.rs | 7 +- backend/gateway/src/rate_limit/rules.rs | 21 +- frontend/src/app/App.tsx | 2 +- .../account/components/ProfileCard.tsx | 2 +- .../components/avatar/AvatarUpload.tsx | 2 +- frontend/src/features/auth/RequireAuth.tsx | 2 +- .../src/features/auth/forms/LoginForm.tsx | 2 +- .../src/features/auth/forms/RegisterForm.tsx | 2 +- .../features/auth/{ => session}/session.tsx | 22 +- .../features/auth/session/sessionContext.ts | 12 + .../auth/{ => session}/sessionHint.ts | 0 .../src/features/auth/session/useSession.ts | 8 + .../src/features/auth/tests/session.test.tsx | 2 +- frontend/src/features/landing/CommandHero.tsx | 2 +- .../features/landing/tests/landing.test.tsx | 2 +- .../showcase/components/CopyToSlot.tsx | 2 +- frontend/src/shared/layout/topbar/parts.tsx | 2 +- frontend/src/test/render.tsx | 2 +- .../gui/hud/draggable/impl/BetterChat.java | 1 - .../draggable/impl/metrics/Coordinates.java | 1 - .../hud/draggable/impl/metrics/SpeedBps.java | 1 - .../impl/panels/keybinds/KeybindsData.java | 1 - .../gui/hud/draggable/impl/stats/Ping.java | 1 - .../gui/hud/draggable/impl/status/Armor.java | 1 - .../features/module/core/ModuleCategory.java | 1 - .../features/module/core/ModuleManager.java | 4 +- .../modules/combat/damage/DamageTint.java | 1 - .../modules/combat/prediction/TargetESP.java | 1 - .../modules/combat/pvp/PvpCooldowns.java | 1 - .../modules/combat/pvp/SpearAssist.java | 1 - .../module/modules/combat/pvp/TPSSync.java | 1 - .../module/modules/misc/DefineTarget.java | 1 - .../module/modules/misc/FakePlayer.java | 1 - .../modules/misc/protect/MessageFilter.java | 1 - .../module/modules/misc/sound/HitSounds.java | 1 - .../module/modules/misc/sound/NoSound.java | 1 - .../modules/misc/vanilla/BetterMinecraft.java | 1 - .../modules/player/interact/AutoTool.java | 1 - .../modules/player/interact/ClickPearl.java | 1 - .../modules/player/interact/NoInteract.java | 1 - .../module/modules/visuals/FovControl.java | 1 - .../modules/visuals/camera/AspectRatio.java | 1 - .../modules/visuals/camera/CameraClip.java | 1 - .../modules/visuals/camera/Freecam.java | 1 - .../modules/visuals/camera/ViewModel.java | 1 - .../effects/ambient/WorldParticles.java | 1 - .../visuals/effects/hits/KillEffect.java | 1 - .../module/modules/visuals/esp/ESP.java | 1 - .../modules/visuals/esp/audio/SoundESP.java | 1 - .../modules/visuals/esp/overlay/DropESP.java | 1 - .../module/modules/visuals/fx/MotionBlur.java | 1 - .../module/modules/visuals/fx/PostFX.java | 1 - .../module/modules/visuals/fx/TotemFX.java | 1 - .../modules/visuals/items/BackSword.java | 1 - .../modules/visuals/nametags/NameTags.java | 2 - .../modules/visuals/player/FullBright.java | 1 - .../modules/visuals/scene/BlockHighlight.java | 1 - .../modules/visuals/visibility/NoRender.java | 1 - .../modules/visuals/weather/WorldTweaks.java | 2 - .../visuals/weather/sky/ShaderSky.java | 1 - .../entity/living/LivingEntityMixin.java | 31 --- .../input/keyboard/KeyboardInputMixin.java | 3 +- .../mixins/input/mouse/InputMixin.java | 16 -- .../game/MultiPlayerGameModeMixin.java | 10 - .../mixins/render/main/MinecraftMixin.java | 17 -- mod/src/main/resources/lovisual.mixins.json | 1 - 95 files changed, 899 insertions(+), 185 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 backend/accounts-service/migrations/0005_password_reset_tokens.sql create mode 100644 backend/accounts-service/src/auth/reset/handlers.rs create mode 100644 backend/accounts-service/src/auth/reset/mod.rs create mode 100644 backend/accounts-service/tests/password_reset.rs rename frontend/src/features/auth/{ => session}/session.tsx (73%) create mode 100644 frontend/src/features/auth/session/sessionContext.ts rename frontend/src/features/auth/{ => session}/sessionHint.ts (100%) create mode 100644 frontend/src/features/auth/session/useSession.ts delete mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..68098918 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,132 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: backend (cargo test + clippy) + runs-on: ubuntu-latest + timeout-minutes: 30 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: lovisual + POSTGRES_PASSWORD: lovisual + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U lovisual" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + s3: + image: minio/minio:latest + env: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - 9000:9000 + options: >- + --health-cmd "mc ready local || curl -sf http://localhost:9000/minio/health/live" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + env: + DATABASE_URL: postgres://lovisual:lovisual@localhost:5432/accounts_db + CONFIGS_DATABASE_URL: postgres://lovisual:lovisual@localhost:5432/configs_db + steps: + - uses: actions/checkout@v4 + + - name: Install protoc (prost-build requirement) + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: backend + + - name: Create service databases + run: | + psql -h localhost -U lovisual -d postgres -c "CREATE DATABASE accounts_db" + psql -h localhost -U lovisual -d postgres -c "CREATE DATABASE configs_db" + env: + PGPASSWORD: lovisual + + - name: cargo fmt --check + run: cargo fmt --check + working-directory: backend + + - name: cargo clippy + run: cargo clippy --workspace --all-targets -- -D warnings + working-directory: backend + + - name: cargo test + run: cargo test --workspace --no-fail-fast + working-directory: backend + + frontend: + name: frontend (lint + test + build) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Install bun + uses: oven-sh/setup-bun@v2 + + - name: Cache bun install + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('frontend/bun.lock') }} + restore-keys: bun-${{ runner.os }}- + + - name: Install dependencies + run: bun install --frozen-lockfile + working-directory: frontend + + - name: lint + run: bun run lint + working-directory: frontend + + - name: test + run: bun run test + working-directory: frontend + + - name: build + run: bun run build + working-directory: frontend + + mod: + name: mod (gradle test + checkFolderLimit) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 25 + + - name: Setup Gradle (dependency + loom caching) + uses: gradle/actions/setup-gradle@v4 + + - name: test + checkFolderLimit + run: ./gradlew test checkFolderLimit --console=plain + working-directory: mod diff --git a/backend/.env.example b/backend/.env.example index 83818c28..d2d4925b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,6 +21,10 @@ ACCOUNTS_GRPC_URL=http://127.0.0.1:50051 CONFIGS_HTTP_URL=http://127.0.0.1:8082 CHAT_HTTP_URL=http://127.0.0.1:8083 SITE_ORIGIN=http://localhost:5173 +# true ONLY when the gateway is behind a reverse proxy that sets +# X-Forwarded-For (nginx in deploy/): per-IP rate limits then key on the +# real client. docker-compose.prod.yml overrides this to "true" itself; +# direct exposure must keep it false, or clients could spoof the header. TRUST_PROXY=false # Directory the gateway serves under GET /downloads/* (lovisual.jar lives here; # docker-compose.prod.yml mounts it as /srv/downloads and sets the variable itself) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 14ba96d9..9844329e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -872,6 +872,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tower-http 0.7.1", "tracing", "tracing-subscriber", "uuid", @@ -941,6 +942,7 @@ dependencies = [ "sqlx", "tokio", "tonic", + "tower-http 0.7.1", "tracing", "tracing-subscriber", "uuid", @@ -2330,6 +2332,15 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.8.4" @@ -4174,10 +4185,14 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/backend/accounts-service/Cargo.toml b/backend/accounts-service/Cargo.toml index f0d67077..50b72fd8 100644 --- a/backend/accounts-service/Cargo.toml +++ b/backend/accounts-service/Cargo.toml @@ -9,10 +9,10 @@ path = "src/lib.rs" [dependencies] axum = { version = "0.8", features = ["multipart", "macros"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] } tower-http = { version = "0.7", features = ["trace", "cors"] } tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros", "migrate"] } @@ -36,4 +36,5 @@ tonic = "0.14" [dev-dependencies] axum-test = "21" +aws-sdk-s3 = "1" tokio-stream = { version = "0.1", features = ["net"] } diff --git a/backend/accounts-service/migrations/0005_password_reset_tokens.sql b/backend/accounts-service/migrations/0005_password_reset_tokens.sql new file mode 100644 index 00000000..e9beb518 --- /dev/null +++ b/backend/accounts-service/migrations/0005_password_reset_tokens.sql @@ -0,0 +1,13 @@ +-- Single-use password reset tokens (stored hashed, like refresh tokens). +-- A token is valid for 30 minutes; issuing a new request for the same +-- account supersedes any token still pending from an earlier request. +CREATE TABLE password_reset_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_password_reset_tokens_account_id ON password_reset_tokens(account_id); diff --git a/backend/accounts-service/src/accounts/model.rs b/backend/accounts-service/src/accounts/model.rs index 3b559152..bd038c1b 100644 --- a/backend/accounts-service/src/accounts/model.rs +++ b/backend/accounts-service/src/accounts/model.rs @@ -24,6 +24,17 @@ pub fn validate_register( password: &str, nick: &str, ) -> Result<(String, String), AppError> { + let email = normalize_email(email)?; + let nick = validate_nick(nick)?; + validate_password(password)?; + Ok((email, nick)) +} + +/// Trims and shape-checks an email address the same way for every auth flow +/// that accepts one (register, password reset request). The lookup itself is +/// case-insensitive: `accounts_email_lower_idx` is a unique index on +/// `lower(email)`, and `repo::find_by_email` lowercases the query value. +pub fn normalize_email(email: &str) -> Result { let email = email.trim(); if email.is_empty() { return Err(AppError::Validation("email must not be empty".into())); @@ -42,7 +53,10 @@ pub fn validate_register( "email must have exactly one '@' with non-empty parts".into(), )); } + Ok(email.to_string()) +} +fn validate_nick(nick: &str) -> Result { let nick = nick.trim(); let nick_len = nick.chars().count(); if nick_len == 0 || nick_len > 32 { @@ -55,7 +69,11 @@ pub fn validate_register( "nick must not contain control characters".into(), )); } + Ok(nick.to_string()) +} +/// Password policy shared by registration and password reset. +pub fn validate_password(password: &str) -> Result<(), AppError> { if password.chars().count() < 8 { return Err(AppError::Validation( "password must be at least 8 characters".into(), @@ -66,8 +84,7 @@ pub fn validate_register( "password must be at most {MAX_PASSWORD_BYTES} bytes" ))); } - - Ok((email.to_string(), nick.to_string())) + Ok(()) } #[cfg(test)] diff --git a/backend/accounts-service/src/auth/handlers.rs b/backend/accounts-service/src/auth/handlers.rs index 33fd82a3..f2d09bed 100644 --- a/backend/accounts-service/src/auth/handlers.rs +++ b/backend/accounts-service/src/auth/handlers.rs @@ -1,6 +1,6 @@ use crate::accounts::model::validate_register; use crate::accounts::repo; -use crate::auth::{password, tokens}; +use crate::auth::{password, reset, tokens}; use crate::error::{AppError, AppJson}; use axum::{Json, extract::State, http::StatusCode}; use axum_extra::extract::cookie::CookieJar; @@ -13,6 +13,9 @@ pub struct AuthState { pub jwt_secret: String, pub cookie_secure: bool, pub hasher: password::PasswordHasher, + /// Reset-email delivery back-end: Log in production (until a real + /// provider lands), Queue in tests. + pub mail: reset::MailBox, // A real Argon2id hash of a throwaway string. `login` verifies against // it when the email is unknown so that "no such account" costs the same // ~100ms as "wrong password" — otherwise response time leaks which @@ -22,6 +25,15 @@ pub struct AuthState { impl AuthState { pub fn new(pool: sqlx::PgPool, jwt_secret: String, cookie_secure: bool) -> Self { + Self::with_mail(pool, jwt_secret, cookie_secure, reset::MailBox::Log) + } + + pub fn with_mail( + pool: sqlx::PgPool, + jwt_secret: String, + cookie_secure: bool, + mail: reset::MailBox, + ) -> Self { // Hashing a fixed, short constant with fixed valid params cannot // fail; this is not user input, so the expect is a startup invariant. let dummy_hash = password::hash_password("timing-equalizer-not-a-real-password") @@ -31,6 +43,7 @@ impl AuthState { jwt_secret, cookie_secure, hasher: password::PasswordHasher::new(), + mail, dummy_hash, } } diff --git a/backend/accounts-service/src/auth/mod.rs b/backend/accounts-service/src/auth/mod.rs index 95d327cc..d34e9676 100644 --- a/backend/accounts-service/src/auth/mod.rs +++ b/backend/accounts-service/src/auth/mod.rs @@ -1,3 +1,4 @@ pub mod handlers; pub mod password; +pub mod reset; pub mod tokens; diff --git a/backend/accounts-service/src/auth/reset/handlers.rs b/backend/accounts-service/src/auth/reset/handlers.rs new file mode 100644 index 00000000..d4c62ed0 --- /dev/null +++ b/backend/accounts-service/src/auth/reset/handlers.rs @@ -0,0 +1,86 @@ +use crate::accounts::{model, repo}; +use crate::auth::reset; +use crate::error::{AppError, AppJson}; +use axum::{Json, extract::State, http::StatusCode}; +use serde::{Deserialize, Serialize}; + +use super::super::handlers::AuthState; +#[derive(Deserialize)] +pub struct ForgotPasswordRequest { + pub email: String, +} + +#[derive(Serialize)] +pub struct AcceptedResponse { + pub status: &'static str, +} + +/// POST /auth/forgot-password { email } +/// +/// The response is identical whether or not the email is registered: no +/// oracle for account enumeration. Delivery happens out of band; the gateway +/// rate-limits this route per IP (3/hour) to keep the mailer from being +/// weaponised. +pub async fn forgot_password( + State(state): State, + AppJson(req): AppJson, +) -> Result<(StatusCode, Json), AppError> { + let email = model::normalize_email(&req.email)?; + if let Some(account) = repo::find_by_email(&state.pool, &email).await? { + let token = reset::issue_reset_token(&state.pool, account.id).await?; + state.mail.send(&email, &token); + } + Ok(( + StatusCode::ACCEPTED, + Json(AcceptedResponse { + status: "reset email sent if the account exists", + }), + )) +} + +#[derive(Deserialize)] +pub struct ResetPasswordRequest { + pub token: String, + pub new_password: String, +} + +/// POST /auth/reset-password { token, new_password } +/// +/// Consumes the token atomically, rewrites the password hash and revokes +/// every refresh session of the account. A bad token is a plain 400 with no +/// distinction between unknown, expired and already-used — all three are the +/// same "try again" situation from an attacker's point of view. +pub async fn reset_password( + State(state): State, + AppJson(req): AppJson, +) -> Result, AppError> { + if !reset::is_well_formed_token(&req.token) { + return Err(AppError::Validation("malformed reset token".into())); + } + model::validate_password(&req.new_password)?; + + let account_id = reset::consume_reset_token(&state.pool, &req.token) + .await? + .ok_or_else(|| AppError::Validation("reset token is invalid or expired".into()))?; + + let hash = state + .hasher + .hash(req.new_password) + .await + .map_err(AppError::Internal)?; + let updated = sqlx::query("UPDATE accounts SET password_hash = $2 WHERE id = $1") + .bind(account_id) + .bind(&hash) + .execute(&state.pool) + .await?; + if updated.rows_affected() != 1 { + // The token row referenced a cascade-deleted account. + return Err(AppError::Validation( + "reset token is invalid or expired".into(), + )); + } + reset::revoke_all_sessions(&state.pool, account_id).await?; + Ok(Json(AcceptedResponse { + status: "password updated", + })) +} diff --git a/backend/accounts-service/src/auth/reset/mod.rs b/backend/accounts-service/src/auth/reset/mod.rs new file mode 100644 index 00000000..c241b88f --- /dev/null +++ b/backend/accounts-service/src/auth/reset/mod.rs @@ -0,0 +1,129 @@ +pub mod handlers; + +use sqlx::PgPool; +use tokio::sync::mpsc::UnboundedSender; +use uuid::Uuid; + +use crate::auth::tokens::{hash_token, new_opaque_token}; + +/// One outgoing reset email. `token` is the plaintext token: the only place +/// it ever exists outside the response of `issue_reset_token`. +#[derive(Debug, Clone)] +pub struct Mail { + pub to: String, + pub token: String, +} + +/// Delivery back-end for reset emails. +#[derive(Clone)] +pub enum MailBox { + /// Development delivery: a structured log line carrying the token, which + /// local/dev stacks pick up from the container logs. When a real provider + /// is wired in (lettre/SES/anything), this variant is the single switch + /// point - the endpoint contract does not change. + Log, + /// In-process queue: tests (and a future in-process mail worker) receive + /// every mail exactly as the handler produced it. + Queue(UnboundedSender), +} + +impl MailBox { + pub fn send(&self, to: &str, token: &str) { + match self { + MailBox::Log => { + tracing::info!( + account = %to, + reset_token = %token, + "password reset requested; deliver the reset link to the account owner" + ); + } + MailBox::Queue(tx) => { + let _ = tx.send(Mail { + to: to.to_string(), + token: token.to_string(), + }); + } + } + } +} + +/// Reset links must be used quickly: long windows turn a leaked email into +/// an account takeover. 30 minutes is the common industry compromise. +const TTL_MINUTES: i64 = 30; + +pub const TOKEN_PREFIX: &str = "lvpr_"; + +/// A reset token is base64url like every other opaque token in this service +/// ("lvpr_" prefix + 43 chars), so the length check alone filters out most +/// junk before the database is ever touched. +pub fn is_well_formed_token(token: &str) -> bool { + token.len() == TOKEN_PREFIX.len() + 43 && token.starts_with(TOKEN_PREFIX) +} + +/// Invalidates any token still pending for the account, then stores the hash +/// of a fresh one. Returns the plaintext token for the mailer only — it is +/// never persisted in clear form. +pub async fn issue_reset_token(pool: &PgPool, account_id: Uuid) -> Result { + sqlx::query( + "UPDATE password_reset_tokens SET used_at = now() + WHERE account_id = $1 AND used_at IS NULL", + ) + .bind(account_id) + .execute(pool) + .await?; + let token = new_opaque_token(TOKEN_PREFIX); + sqlx::query( + "INSERT INTO password_reset_tokens (account_id, token_hash, expires_at) + VALUES ($1, $2, now() + make_interval(mins => $3::int))", + ) + .bind(account_id) + .bind(hash_token(&token)) + .bind(TTL_MINUTES) + .execute(pool) + .await?; + Ok(token) +} + +/// Atomically marks the token as used and returns its account. The single +/// conditional UPDATE makes double-spend impossible even for concurrent +/// callers: exactly one of them gets the row back. +pub async fn consume_reset_token(pool: &PgPool, token: &str) -> Result, sqlx::Error> { + let row: Option<(Uuid,)> = sqlx::query_as( + "UPDATE password_reset_tokens SET used_at = now() + WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now() + RETURNING account_id", + ) + .bind(hash_token(token)) + .fetch_optional(pool) + .await?; + Ok(row.map(|(id,)| id)) +} + +/// Kill every refresh session of the account: whoever holds a stolen session +/// cookie must not survive a password change. +pub async fn revoke_all_sessions(pool: &PgPool, account_id: Uuid) -> Result<(), sqlx::Error> { + sqlx::query( + "UPDATE refresh_tokens SET revoked_at = now() + WHERE account_id = $1 AND revoked_at IS NULL", + ) + .bind(account_id) + .execute(pool) + .await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn well_formed_token_shape_matches_opaque_generator() { + let token = new_opaque_token(TOKEN_PREFIX); + assert!(is_well_formed_token(&token)); + assert!(!is_well_formed_token(&format!("{token}x"))); + assert!(!is_well_formed_token("lvpr_short")); + assert!(!is_well_formed_token( + "XWpr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + )); + } +} diff --git a/backend/accounts-service/src/lib.rs b/backend/accounts-service/src/lib.rs index fada9c1f..c95bd475 100644 --- a/backend/accounts-service/src/lib.rs +++ b/backend/accounts-service/src/lib.rs @@ -16,9 +16,21 @@ use axum::{ }; use config::Config; use device::{handlers::DeviceState, store::DeviceStore}; +use tower_http::trace::TraceLayer; pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { - let auth_state = AuthState::new(pool.clone(), cfg.jwt_secret.clone(), cfg.cookie_secure); + build_app_with_mail(pool, cfg, auth::reset::MailBox::Log) +} + +/// Same router with a custom reset-email back-end: production uses the log +/// mailer, tests capture tokens through the queue variant. +pub fn build_app_with_mail(pool: sqlx::PgPool, cfg: &Config, mail: auth::reset::MailBox) -> Router { + let auth_state = AuthState::with_mail( + pool.clone(), + cfg.jwt_secret.clone(), + cfg.cookie_secure, + mail, + ); let device_state = DeviceState { store: DeviceStore::default(), pool: pool.clone(), @@ -43,6 +55,14 @@ pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { .route("/auth/login", post(auth::handlers::login)) .route("/auth/refresh", post(auth::handlers::refresh)) .route("/auth/logout", post(auth::handlers::logout)) + .route( + "/auth/forgot-password", + post(auth::reset::handlers::forgot_password), + ) + .route( + "/auth/reset-password", + post(auth::reset::handlers::reset_password), + ) .with_state(auth_state); let device_routes = Router::new() @@ -87,4 +107,5 @@ pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { Router::new() .route("/health", get(|| async { "ok" })) .merge(api) + .layer(TraceLayer::new_for_http()) } diff --git a/backend/accounts-service/src/main.rs b/backend/accounts-service/src/main.rs index 63b8d23a..6613442d 100644 --- a/backend/accounts-service/src/main.rs +++ b/backend/accounts-service/src/main.rs @@ -2,7 +2,12 @@ use accounts_service::{build_app, config::Config}; #[tokio::main] async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); dotenvy::dotenv().ok(); let cfg = Config::from_env()?; cfg.validate()?; diff --git a/backend/accounts-service/tests/avatar_upload.rs b/backend/accounts-service/tests/avatar_upload.rs index cd93c94b..85291d70 100644 --- a/backend/accounts-service/tests/avatar_upload.rs +++ b/backend/accounts-service/tests/avatar_upload.rs @@ -20,6 +20,8 @@ fn form(bytes: Vec) -> MultipartForm { #[tokio::test] async fn valid_upload_stores_avatar_and_returns_url() { + common::init_test_logging(); + common::ensure_avatar_bucket().await; let pool = common::test_pool().await; let server = common::test_server(accounts_service::build_app( pool.clone(), diff --git a/backend/accounts-service/tests/common/mod.rs b/backend/accounts-service/tests/common/mod.rs index 580306ee..30c4aa3b 100644 --- a/backend/accounts-service/tests/common/mod.rs +++ b/backend/accounts-service/tests/common/mod.rs @@ -8,6 +8,10 @@ use uuid::Uuid; // mounts as `mod common;`). pub use ::common::internal::{ACCOUNT_ID_HEADER, INTERNAL_KEY_HEADER}; +pub fn init_test_logging() { + let _ = tracing_subscriber::fmt::try_init(); +} + pub async fn test_pool() -> sqlx::PgPool { let url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); @@ -60,3 +64,65 @@ pub async fn register_account(server: &TestServer) -> (Uuid, String) { email, ) } + +/// Creates the test avatar bucket if it does not exist yet, so the avatar +/// tests are self-contained: any S3-compatible backend (MinIO, SeaweedFS) +/// works without external `mc mb` bootstrap. Mirrors `S3Storage::from_config`. +/// Retries briefly so a just-started container does not race the test. +pub async fn ensure_avatar_bucket() { + let creds = + aws_sdk_s3::config::Credentials::new("minioadmin", "minioadmin", None, None, "static"); + let config = aws_sdk_s3::config::Builder::new() + .endpoint_url("http://localhost:9000") + .credentials_provider(creds) + .region(aws_sdk_s3::config::Region::new("us-east-1")) + .force_path_style(true) + .behavior_version(aws_sdk_s3::config::BehaviorVersion::latest()) + .build(); + let client = aws_sdk_s3::Client::from_conf(config); + let mut last_err = String::new(); + for _ in 0..30 { + // Probe writability, not just bucket existence: a freshly started + // S3 backend may still be electing volumes ("Not enough data nodes" + // on SeaweedFS) right after create_bucket succeeds. + match async { + // Already-exists is success (MinIO: BucketAlreadyOwnedByYou, + // SeaweedFS: BucketAlreadyExists); anything else aborts. + if let Err(e) = client + .create_bucket() + .bucket("lovisual-avatars-test") + .send() + .await + { + let msg = format!("{e:?}"); + if !msg.contains("BucketAlready") { + return Err(msg); + } + } + client + .put_object() + .bucket("lovisual-avatars-test") + .key(".probe") + .body(aws_sdk_s3::primitives::ByteStream::from_static(b"probe")) + .send() + .await + .map_err(|e| format!("{e:?}"))?; + let _ = client + .delete_object() + .bucket("lovisual-avatars-test") + .key(".probe") + .send() + .await; + Ok::<(), String>(()) + } + .await + { + Ok(_) => return, + Err(msg) => { + last_err = msg; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + } + panic!("S3 backend is not writable: {last_err}"); +} diff --git a/backend/accounts-service/tests/password_reset.rs b/backend/accounts-service/tests/password_reset.rs new file mode 100644 index 00000000..76bdf76d --- /dev/null +++ b/backend/accounts-service/tests/password_reset.rs @@ -0,0 +1,215 @@ +mod common; + +use accounts_service::auth::reset::{Mail, MailBox}; +use axum::http::StatusCode; +use axum_test::TestServer; +use sqlx::PgPool; +use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel}; + +type App = (TestServer, PgPool, UnboundedReceiver); + +async fn app() -> App { + let (tx, rx) = unbounded_channel(); + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app_with_mail( + pool.clone(), + &common::test_config(), + MailBox::Queue(tx), + )); + (server, pool, rx) +} + +async fn request_reset(server: &TestServer, email: &str) { + server + .post("/auth/forgot-password") + .json(&serde_json::json!({ "email": email })) + .await + .assert_status(StatusCode::ACCEPTED); +} + +async fn reset_with(server: &TestServer, token: &str, password: &str) -> axum_test::TestResponse { + server + .post("/auth/reset-password") + .json(&serde_json::json!({ "token": token, "new_password": password })) + .await +} + +#[tokio::test] +async fn forgot_password_never_reveals_account_existence() { + let (server, _pool, _mail) = app().await; + + // Unknown email and known email must be indistinguishable: same status, + // same body. Enumeration is the first step of account takeover. + let unknown = server + .post("/auth/forgot-password") + .json(&serde_json::json!({ "email": "nobody-here@example.com" })) + .await; + unknown.assert_status(StatusCode::ACCEPTED); + let unknown_body: serde_json::Value = unknown.json(); + assert_eq!( + unknown_body["status"], + "reset email sent if the account exists" + ); + + let (_, email) = common::register_account(&server).await; + let known = server + .post("/auth/forgot-password") + .json(&serde_json::json!({ "email": email })) + .await; + known.assert_status(StatusCode::ACCEPTED); + let known_body: serde_json::Value = known.json(); + assert_eq!(unknown_body, known_body); +} + +#[tokio::test] +async fn full_reset_flow_changes_password_and_kills_sessions() { + let (server, _pool, mut mail) = app().await; + let (_, email) = common::register_account(&server).await; + let old_password = "correct-horse-battery-staple"; + + // Log in to create a live refresh session the reset must kill. + let login = server + .post("/auth/login") + .json(&serde_json::json!({ "email": email, "password": old_password })) + .await; + login.assert_status_ok(); + let old_refresh = refresh_cookie(&login).expect("login must set the refresh cookie"); + + request_reset(&server, &email).await; + let token = mail.recv().await.expect("queue mailer must deliver").token; + + reset_with(&server, &token, "brand-new-password-1") + .await + .assert_status_ok(); + + // Old password is dead, new password works. + server + .post("/auth/login") + .json(&serde_json::json!({ "email": email, "password": old_password })) + .await + .assert_status(StatusCode::UNAUTHORIZED); + server + .post("/auth/login") + .json(&serde_json::json!({ "email": email, "password": "brand-new-password-1" })) + .await + .assert_status_ok(); + + // Every refresh session issued before the reset is revoked: replaying + // the pre-reset cookie must not survive (a stolen session cannot + // outlive a password change). + let replay = server + .post("/auth/refresh") + .add_cookie(old_refresh.as_str().into()) + .await; + replay.assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn reset_token_is_single_use() { + let (server, _pool, mut mail) = app().await; + let (_, email) = common::register_account(&server).await; + + request_reset(&server, &email).await; + let token = mail.recv().await.expect("mail").token; + + reset_with(&server, &token, "brand-new-password-1") + .await + .assert_status_ok(); + reset_with(&server, &token, "another-password-2") + .await + .assert_status(StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn new_request_supersedes_pending_token() { + let (server, _pool, mut mail) = app().await; + let (_, email) = common::register_account(&server).await; + + request_reset(&server, &email).await; + let first = mail.recv().await.expect("mail").token; + request_reset(&server, &email).await; + let second = mail.recv().await.expect("mail").token; + assert_ne!(first, second); + + // The superseded token no longer works, the fresh one does. + reset_with(&server, &first, "brand-new-password-1") + .await + .assert_status(StatusCode::BAD_REQUEST); + reset_with(&server, &second, "brand-new-password-1") + .await + .assert_status_ok(); +} + +#[tokio::test] +async fn bad_tokens_are_rejected_without_oracle() { + let (server, _pool, _mail) = app().await; + + server + .post("/auth/reset-password") + .json(&serde_json::json!({ "token": "short", "new_password": "brand-new-password-1" })) + .await + .assert_status(StatusCode::BAD_REQUEST); + + // Unknown but well-formed token: same 400 family, and unlike the + // shape-rejection above it must not leak which of unknown/expired/used + // it is. + let unknown = server + .post("/auth/reset-password") + .json(&serde_json::json!({ + "token": "lvpr_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "new_password": "brand-new-password-1" + })) + .await; + unknown.assert_status(StatusCode::BAD_REQUEST); + let body: serde_json::Value = unknown.json(); + assert_eq!(body["error"], "reset token is invalid or expired"); +} + +#[tokio::test] +async fn weak_password_is_rejected_before_token_consumption() { + let (server, _pool, mut mail) = app().await; + let (_, email) = common::register_account(&server).await; + + request_reset(&server, &email).await; + let token = mail.recv().await.expect("mail").token; + + reset_with(&server, &token, "short12") + .await + .assert_status(StatusCode::BAD_REQUEST); + + // The token must still be usable afterwards: rejecting a weak password + // must not burn the user's one link. + reset_with(&server, &token, "brand-new-password-1") + .await + .assert_status_ok(); +} + +#[tokio::test] +async fn reset_mail_only_goes_to_known_accounts() { + let (server, _pool, mut mail) = app().await; + + request_reset(&server, "ghost@example.com").await; + assert!( + mail.try_recv().is_err(), + "unknown account must not enqueue a reset email" + ); + + let (_, email) = common::register_account(&server).await; + request_reset(&server, &email).await; + let sent = mail.recv().await.expect("mail"); + assert_eq!(sent.to, email); + assert!(sent.token.starts_with("lvpr_")); +} + +/// The refresh cookie is httpOnly and scoped to /auth; axum-test exposes +/// response headers, so parse Set-Cookie directly. +fn refresh_cookie(res: &axum_test::TestResponse) -> Option { + res.headers() + .get_all(axum::http::header::SET_COOKIE) + .iter() + .find_map(|v| { + let s = v.to_str().ok()?; + s.strip_prefix("lv_refresh=") + .map(|rest| format!("lv_refresh={}", rest.split(';').next().unwrap_or(""))) + }) +} diff --git a/backend/chat-service/Cargo.toml b/backend/chat-service/Cargo.toml index a4eeeeaa..618dbdef 100644 --- a/backend/chat-service/Cargo.toml +++ b/backend/chat-service/Cargo.toml @@ -10,9 +10,10 @@ path = "src/lib.rs" [dependencies] common = { path = "../common" } axum = { version = "0.8", features = ["macros"] } +tower-http = { version = "0.7", features = ["trace"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } serde_json = "1" uuid = { version = "1", features = ["v4", "serde"] } diff --git a/backend/chat-service/src/lib.rs b/backend/chat-service/src/lib.rs index c500627b..b20ada71 100644 --- a/backend/chat-service/src/lib.rs +++ b/backend/chat-service/src/lib.rs @@ -1,11 +1,16 @@ pub mod config; pub mod presence; -use axum::{Router, extract::DefaultBodyLimit, routing::{get, post}}; +use axum::{ + Router, + extract::DefaultBodyLimit, + routing::{get, post}, +}; use common::internal::{InternalKey, require_internal_key}; use config::Config; use presence::store::Store; use std::sync::Arc; +use tower_http::trace::TraceLayer; /// Presence requests are tiny JSON (a ≤64-char payload and ≤40 uuids). const MAX_BODY_BYTES: usize = 8 * 1024; @@ -22,6 +27,7 @@ pub fn build_app(cfg: &Config) -> (Router, Arc) { )); let app = Router::new() .route("/health", get(|| async { "ok" })) - .merge(api); + .merge(api) + .layer(TraceLayer::new_for_http()); (app, store) } diff --git a/backend/chat-service/src/main.rs b/backend/chat-service/src/main.rs index 47e3ab2b..d5d82d6f 100644 --- a/backend/chat-service/src/main.rs +++ b/backend/chat-service/src/main.rs @@ -1,7 +1,12 @@ #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); let cfg = chat_service::config::Config::from_env()?; cfg.validate()?; let (app, state) = chat_service::build_app(&cfg); diff --git a/backend/chat-service/src/presence/handlers.rs b/backend/chat-service/src/presence/handlers.rs index bc962add..60522882 100644 --- a/backend/chat-service/src/presence/handlers.rs +++ b/backend/chat-service/src/presence/handlers.rs @@ -63,15 +63,23 @@ pub async fn sync( } let now = Instant::now(); if req.publish - && let Err(PublishError::Claimed) = store.publish(id.account_id, &server, req.mc, req.gui, now) + && let Err(PublishError::Claimed) = + store.publish(id.account_id, &server, req.mc, req.gui, now) { - return error(StatusCode::CONFLICT, "player uuid is bound to another account"); + return error( + StatusCode::CONFLICT, + "player uuid is bound to another account", + ); } let players: Vec = store .lookup(&server, &req.want, now) .into_iter() .filter(|s| s.mc != req.mc) - .map(|s| PlayerState { mc: s.mc, gui: s.gui, age: s.age_ms }) + .map(|s| PlayerState { + mc: s.mc, + gui: s.gui, + age: s.age_ms, + }) .collect(); Json(json!({ "players": players })).into_response() } diff --git a/backend/chat-service/src/presence/mod.rs b/backend/chat-service/src/presence/mod.rs index ebc1a222..a46dde6c 100644 --- a/backend/chat-service/src/presence/mod.rs +++ b/backend/chat-service/src/presence/mod.rs @@ -6,7 +6,10 @@ pub mod handlers; pub mod payload; pub mod store; -use std::{sync::Arc, time::{Duration, Instant}}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; use store::Store; pub fn spawn_purger(store: Arc) { diff --git a/backend/chat-service/src/presence/payload.rs b/backend/chat-service/src/presence/payload.rs index 20541872..c9a96cf6 100644 --- a/backend/chat-service/src/presence/payload.rs +++ b/backend/chat-service/src/presence/payload.rs @@ -10,7 +10,8 @@ pub const MAX_WANT: usize = 40; pub fn valid_payload(s: &str) -> bool { !s.is_empty() && s.len() <= MAX_PAYLOAD_CHARS - && s.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) + && s.bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) } /// Normalised server key: trimmed, lower-case, no control characters, bounded. @@ -38,7 +39,10 @@ mod tests { #[test] fn server_is_normalised() { - assert_eq!(normalize_server(" Play.Example.COM ").as_deref(), Some("play.example.com")); + assert_eq!( + normalize_server(" Play.Example.COM ").as_deref(), + Some("play.example.com") + ); assert_eq!(normalize_server(""), None); assert_eq!(normalize_server("a\nb"), None); assert_eq!(normalize_server(&"x".repeat(65)), None); diff --git a/backend/chat-service/src/presence/store.rs b/backend/chat-service/src/presence/store.rs index 2f69cc8b..17c0f97a 100644 --- a/backend/chat-service/src/presence/store.rs +++ b/backend/chat-service/src/presence/store.rs @@ -97,7 +97,9 @@ impl Store { pub fn purge(&self, now: Instant) { let mut inner = self.inner.lock().expect("presence lock"); - inner.entries.retain(|_, e| now.duration_since(e.updated) < ENTRY_TTL); + inner + .entries + .retain(|_, e| now.duration_since(e.updated) < ENTRY_TTL); let expired: Vec = inner .claims .iter() @@ -117,7 +119,12 @@ mod tests { use super::*; fn ids() -> (Uuid, Uuid, Uuid, Uuid) { - (Uuid::from_u128(1), Uuid::from_u128(2), Uuid::from_u128(10), Uuid::from_u128(20)) + ( + Uuid::from_u128(1), + Uuid::from_u128(2), + Uuid::from_u128(10), + Uuid::from_u128(20), + ) } #[test] @@ -125,7 +132,9 @@ mod tests { let (acc, _, mc, _) = ids(); let store = Store::default(); let t0 = Instant::now(); - store.publish(acc, "srv", mc, Some("AAAA".into()), t0).unwrap(); + store + .publish(acc, "srv", mc, Some("AAAA".into()), t0) + .unwrap(); let seen = store.lookup("srv", &[mc], t0 + Duration::from_millis(500)); assert_eq!(seen.len(), 1); assert_eq!(seen[0].gui.as_deref(), Some("AAAA")); @@ -149,7 +158,10 @@ mod tests { let store = Store::default(); let t0 = Instant::now(); store.publish(acc, "s", mc, None, t0).unwrap(); - assert_eq!(store.publish(other, "s", mc, None, t0 + Duration::from_secs(1)), Err(PublishError::Claimed)); + assert_eq!( + store.publish(other, "s", mc, None, t0 + Duration::from_secs(1)), + Err(PublishError::Claimed) + ); // after the claim goes idle it can be taken over assert!(store.publish(other, "s", mc, None, t0 + CLAIM_TTL).is_ok()); } @@ -183,6 +195,10 @@ mod tests { let t0 = Instant::now(); store.publish(acc, "s", mc, None, t0).unwrap(); store.purge(t0 + CLAIM_TTL); - assert!(store.publish(Uuid::from_u128(2), "s", mc, None, t0 + CLAIM_TTL).is_ok()); + assert!( + store + .publish(Uuid::from_u128(2), "s", mc, None, t0 + CLAIM_TTL) + .is_ok() + ); } } diff --git a/backend/chat-service/tests/presence.rs b/backend/chat-service/tests/presence.rs index 85faa61c..54265642 100644 --- a/backend/chat-service/tests/presence.rs +++ b/backend/chat-service/tests/presence.rs @@ -7,7 +7,10 @@ use uuid::Uuid; const KEY: &str = "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"; fn server() -> TestServer { - let cfg = Config { port: 0, internal_key: KEY.into() }; + let cfg = Config { + port: 0, + internal_key: KEY.into(), + }; let (app, _) = chat_service::build_app(&cfg); TestServer::new(app) } @@ -37,10 +40,20 @@ async fn requires_internal_key_and_identity() { async fn two_players_see_each_other() { let s = server(); let (a, b) = (Uuid::from_u128(10), Uuid::from_u128(20)); - let r = sync(&s, 1, json!({"server": "Play.X", "mc": a, "gui": "AQEAAAA=", "want": [b]})).await; + let r = sync( + &s, + 1, + json!({"server": "Play.X", "mc": a, "gui": "AQEAAAA=", "want": [b]}), + ) + .await; assert_eq!(r.status_code(), 200); assert_eq!(r.json::()["players"].as_array().unwrap().len(), 0); - let r = sync(&s, 2, json!({"server": "play.x", "mc": b, "gui": null, "want": [a]})).await; + let r = sync( + &s, + 2, + json!({"server": "play.x", "mc": b, "gui": null, "want": [a]}), + ) + .await; let body = r.json::(); let players = body["players"].as_array().unwrap(); assert_eq!(players.len(), 1); @@ -56,7 +69,12 @@ async fn rejects_bad_input_and_uuid_theft() { assert_eq!(r.status_code(), 400); let r = sync(&s, 1, json!({"server": "", "mc": mc})).await; assert_eq!(r.status_code(), 400); - assert_eq!(sync(&s, 1, json!({"server": "x", "mc": mc})).await.status_code(), 200); + assert_eq!( + sync(&s, 1, json!({"server": "x", "mc": mc})) + .await + .status_code(), + 200 + ); let r = sync(&s, 2, json!({"server": "x", "mc": mc})).await; assert_eq!(r.status_code(), 409); } @@ -65,7 +83,12 @@ async fn rejects_bad_input_and_uuid_theft() { async fn read_only_poll_does_not_publish() { let s = server(); let (a, b) = (Uuid::from_u128(10), Uuid::from_u128(20)); - sync(&s, 1, json!({"server": "x", "mc": a, "publish": false, "want": [b]})).await; + sync( + &s, + 1, + json!({"server": "x", "mc": a, "publish": false, "want": [b]}), + ) + .await; let r = sync(&s, 2, json!({"server": "x", "mc": b, "want": [a]})).await; assert_eq!(r.json::()["players"].as_array().unwrap().len(), 0); } diff --git a/backend/configs-service/Cargo.toml b/backend/configs-service/Cargo.toml index 6e51e10a..c7f21bae 100644 --- a/backend/configs-service/Cargo.toml +++ b/backend/configs-service/Cargo.toml @@ -10,9 +10,10 @@ path = "src/lib.rs" [dependencies] common = { path = "../common" } axum = { version = "0.8", features = ["macros"] } +tower-http = { version = "0.7", features = ["trace"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json", "macros", "migrate"] } diff --git a/backend/configs-service/src/lib.rs b/backend/configs-service/src/lib.rs index 7fb7df45..0a5f43b0 100644 --- a/backend/configs-service/src/lib.rs +++ b/backend/configs-service/src/lib.rs @@ -13,6 +13,7 @@ use common::internal::{InternalKey, require_internal_key}; use config::Config; use showcase::profiles::ProfileSource; use std::sync::Arc; +use tower_http::trace::TraceLayer; pub fn build_app(pool: sqlx::PgPool, cfg: &Config, profiles: Arc) -> Router { let slots_state = slots::handlers::SlotsState { pool: pool.clone() }; @@ -52,4 +53,5 @@ pub fn build_app(pool: sqlx::PgPool, cfg: &Config, profiles: Arc anyhow::Result<()> { dotenvy::dotenv().ok(); - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); let cfg = configs_service::config::Config::from_env()?; cfg.validate()?; let pool = sqlx::PgPool::connect(&cfg.database_url).await?; diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml index c70ffcbd..f817c61f 100644 --- a/backend/docker-compose.prod.yml +++ b/backend/docker-compose.prod.yml @@ -126,6 +126,12 @@ services: ACCOUNTS_GRPC_URL: http://accounts-service:50051 CONFIGS_HTTP_URL: http://configs-service:8082 CHAT_HTTP_URL: http://chat-service:8083 + # This stack is only ever exposed through nginx (see deploy/), so the + # rate limiter must read the client IP from X-Forwarded-For. Without + # this every client shares the proxy's socket address and one bucket: + # the global 300/min and login 5/min would be site-wide self-DoS. + # Keep TRUST_PROXY=false in .env for direct-exposure dev runs. + TRUST_PROXY: "true" # Read-only static files served under GET /downloads/* (lovisual.jar). DOWNLOADS_DIR: /srv/downloads volumes: diff --git a/backend/gateway/Cargo.toml b/backend/gateway/Cargo.toml index f6f0d09a..9b1570a5 100644 --- a/backend/gateway/Cargo.toml +++ b/backend/gateway/Cargo.toml @@ -14,7 +14,7 @@ http-body-util = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time"] } tower-http = { version = "0.7", features = ["cors", "trace", "fs"] } tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde_json = "1" uuid = { version = "1", features = ["v4"] } reqwest = { version = "0.13", default-features = false, features = ["stream"] } diff --git a/backend/gateway/src/lib.rs b/backend/gateway/src/lib.rs index d9800ebc..dc7b3cd3 100644 --- a/backend/gateway/src/lib.rs +++ b/backend/gateway/src/lib.rs @@ -15,7 +15,7 @@ use axum::{ use config::Config; use identity::device::DeviceAuthenticator; use std::sync::Arc; -use tower_http::{cors::CorsLayer, services::ServeDir}; +use tower_http::{cors::CorsLayer, services::ServeDir, trace::TraceLayer}; pub fn build_app(cfg: &Config, devices: Arc) -> Router { let upstreams = Arc::new(proxy::forward::Upstreams::new(cfg).expect("valid upstream config")); @@ -69,6 +69,9 @@ pub fn build_app(cfg: &Config, devices: Arc) -> Router guard::reject_ambiguous_paths, )) .layer(cors) + // Outermost so every route (health, downloads, proxied API) is + // traced; spans carry method/path/status for the fmt subscriber. + .layer(TraceLayer::new_for_http()) } fn spawn_purger(limits: Arc) { diff --git a/backend/gateway/src/main.rs b/backend/gateway/src/main.rs index a8c08c2d..088cd501 100644 --- a/backend/gateway/src/main.rs +++ b/backend/gateway/src/main.rs @@ -4,7 +4,12 @@ use std::sync::Arc; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); let cfg = gateway::config::Config::from_env()?; cfg.validate()?; let devices = Arc::new(GrpcDevices::connect_lazy( diff --git a/backend/gateway/src/rate_limit/rules.rs b/backend/gateway/src/rate_limit/rules.rs index ed18b365..a4e86aac 100644 --- a/backend/gateway/src/rate_limit/rules.rs +++ b/backend/gateway/src/rate_limit/rules.rs @@ -46,6 +46,20 @@ pub fn rules() -> Vec { vec![ rule(Method::POST, "/auth/login", Quota::per_minute(n(5)), Ip), rule(Method::POST, "/auth/register", Quota::per_hour(n(3)), Ip), + // Same threat as register: each accepted request sends an email, so + // the mailer must not be usable as a spam cannon. + rule( + Method::POST, + "/auth/forgot-password", + Quota::per_hour(n(3)), + Ip, + ), + rule( + Method::POST, + "/auth/reset-password", + Quota::per_minute(n(10)), + Ip, + ), rule(Method::POST, "/auth/refresh", Quota::per_minute(n(30)), Ip), rule(Method::POST, "/device/code", Quota::per_minute(n(10)), Ip), // The mod polls every 2–3 s for up to 10 min: 10/min would break linking. @@ -61,7 +75,12 @@ pub fn rules() -> Vec { rule(Method::POST, "/avatars", Quota::per_hour(n(5)), Account), rule(Method::GET, "/showcase*", Quota::per_minute(n(60)), Ip), // The mod syncs menu presence about 3 times a second while a menu is open. - rule(Method::POST, "/presence/*", Quota::per_second(n(6)), Account), + rule( + Method::POST, + "/presence/*", + Quota::per_second(n(6)), + Account, + ), ] } diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 1c33c05d..63a9ce49 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createBrowserRouter, RouterProvider } from 'react-router' -import { SessionProvider } from '@features/auth/session' +import { SessionProvider } from '@features/auth/session/session' import { ActiveThemeProvider } from '@features/themes/useActiveTheme' import { routes } from './routes' diff --git a/frontend/src/features/account/components/ProfileCard.tsx b/frontend/src/features/account/components/ProfileCard.tsx index d7c2c9ff..8d42657d 100644 --- a/frontend/src/features/account/components/ProfileCard.tsx +++ b/frontend/src/features/account/components/ProfileCard.tsx @@ -2,7 +2,7 @@ import { useCallback, useRef, useState, type DragEvent } from 'react' import { useTranslation } from '@shared/i18n/react' import { useNavigate } from 'react-router' import { Button } from '@shared/ui/Button' -import { useSession } from '@features/auth/session' +import { useSession } from '@features/auth/session/useSession' import { AvatarUpload } from './avatar/AvatarUpload' /** The mod's profile panel: big avatar with an accent ring, nick, email and join date. The avatar circle doubles as a file drop target. */ diff --git a/frontend/src/features/account/components/avatar/AvatarUpload.tsx b/frontend/src/features/account/components/avatar/AvatarUpload.tsx index 49cab7c0..2549e789 100644 --- a/frontend/src/features/account/components/avatar/AvatarUpload.tsx +++ b/frontend/src/features/account/components/avatar/AvatarUpload.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useId, useState, type ChangeEvent } from 'react' import { useTranslation } from '@shared/i18n/react' import { Notice } from '@shared/ui/Notice' -import { useSession } from '@features/auth/session' +import { useSession } from '@features/auth/session/useSession' import { addAvatarHistory, checkAvatarFile, diff --git a/frontend/src/features/auth/RequireAuth.tsx b/frontend/src/features/auth/RequireAuth.tsx index 71b91cfc..d65e75c1 100644 --- a/frontend/src/features/auth/RequireAuth.tsx +++ b/frontend/src/features/auth/RequireAuth.tsx @@ -1,6 +1,6 @@ import { useTranslation } from '@shared/i18n/react' import { Navigate, Outlet, useLocation } from 'react-router' -import { useSession } from './session' +import { useSession } from './session/useSession' export function RequireAuth() { const { t } = useTranslation('auth') diff --git a/frontend/src/features/auth/forms/LoginForm.tsx b/frontend/src/features/auth/forms/LoginForm.tsx index 3c0a3915..f5a03864 100644 --- a/frontend/src/features/auth/forms/LoginForm.tsx +++ b/frontend/src/features/auth/forms/LoginForm.tsx @@ -5,7 +5,7 @@ import { describeError } from '@shared/api/errors' import { Button } from '@shared/ui/Button' import { Notice } from '@shared/ui/Notice' import { TextField } from '@shared/ui/text-field/TextField' -import { useSession } from '../session' +import { useSession } from '../session/useSession' export function LoginForm() { const { t } = useTranslation('auth') diff --git a/frontend/src/features/auth/forms/RegisterForm.tsx b/frontend/src/features/auth/forms/RegisterForm.tsx index 172f818e..9226945e 100644 --- a/frontend/src/features/auth/forms/RegisterForm.tsx +++ b/frontend/src/features/auth/forms/RegisterForm.tsx @@ -5,7 +5,7 @@ import { describeError } from '@shared/api/errors' import { Button } from '@shared/ui/Button' import { Notice } from '@shared/ui/Notice' import { TextField } from '@shared/ui/text-field/TextField' -import { useSession } from '../session' +import { useSession } from '../session/useSession' import { validateEmail, validateNick, validatePassword } from '../validation' export function RegisterForm() { diff --git a/frontend/src/features/auth/session.tsx b/frontend/src/features/auth/session/session.tsx similarity index 73% rename from frontend/src/features/auth/session.tsx rename to frontend/src/features/auth/session/session.tsx index 2d15ccfb..1228fd9b 100644 --- a/frontend/src/features/auth/session.tsx +++ b/frontend/src/features/auth/session/session.tsx @@ -1,20 +1,10 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react' +import { useCallback, useMemo, type ReactNode } from 'react' import { api } from '@shared/api/client' -import type { Me } from '@shared/types' -import { fetchMe, login, logout, register } from './api' +import { fetchMe, login, logout, register } from '../api' +import { SessionContext, type Session } from './sessionContext' import { hasSessionHint, setSessionHint } from './sessionHint' -type Session = { - status: 'loading' | 'anonymous' | 'authenticated' - me: Me | null - signIn(email: string, password: string): Promise - signUp(email: string, password: string, nick: string): Promise - signOut(): Promise -} - -const SessionContext = createContext(null) - export function SessionProvider({ children }: { children: ReactNode }) { const client = useQueryClient() // Restores the session after a reload: the refresh cookie survives, the @@ -64,9 +54,3 @@ export function SessionProvider({ children }: { children: ReactNode }) { return {children} } - -export function useSession(): Session { - const session = useContext(SessionContext) - if (!session) throw new Error('useSession outside SessionProvider') - return session -} diff --git a/frontend/src/features/auth/session/sessionContext.ts b/frontend/src/features/auth/session/sessionContext.ts new file mode 100644 index 00000000..32eb2f43 --- /dev/null +++ b/frontend/src/features/auth/session/sessionContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react' +import type { Me } from '@shared/types' + +export type Session = { + status: 'loading' | 'anonymous' | 'authenticated' + me: Me | null + signIn(email: string, password: string): Promise + signUp(email: string, password: string, nick: string): Promise + signOut(): Promise +} + +export const SessionContext = createContext(null) diff --git a/frontend/src/features/auth/sessionHint.ts b/frontend/src/features/auth/session/sessionHint.ts similarity index 100% rename from frontend/src/features/auth/sessionHint.ts rename to frontend/src/features/auth/session/sessionHint.ts diff --git a/frontend/src/features/auth/session/useSession.ts b/frontend/src/features/auth/session/useSession.ts new file mode 100644 index 00000000..e10fe9d9 --- /dev/null +++ b/frontend/src/features/auth/session/useSession.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react' +import { SessionContext, type Session } from './sessionContext' + +export function useSession(): Session { + const session = useContext(SessionContext) + if (!session) throw new Error('useSession outside SessionProvider') + return session +} diff --git a/frontend/src/features/auth/tests/session.test.tsx b/frontend/src/features/auth/tests/session.test.tsx index 981d1a68..1a8ce6c9 100644 --- a/frontend/src/features/auth/tests/session.test.tsx +++ b/frontend/src/features/auth/tests/session.test.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react' import { expect, test } from 'vitest' import { RequireAuth } from '../RequireAuth' -import { useSession } from '../session' +import { useSession } from '../session/useSession' import { renderApp } from '@test/render' import { json, mockFetch } from '@test/fetch' diff --git a/frontend/src/features/landing/CommandHero.tsx b/frontend/src/features/landing/CommandHero.tsx index 42d330e4..df1df494 100644 --- a/frontend/src/features/landing/CommandHero.tsx +++ b/frontend/src/features/landing/CommandHero.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from '@shared/i18n/react' import { prefersReducedMotion } from '@shared/motion/reducedMotion' import { ButtonLink } from '@shared/ui/Button' -import { useSession } from '../auth/session' +import { useSession } from '../auth/session/useSession' import { useInView } from './effects/useInView' import '@features/landing/styles/sections.css' diff --git a/frontend/src/features/landing/tests/landing.test.tsx b/frontend/src/features/landing/tests/landing.test.tsx index fad3600b..21466064 100644 --- a/frontend/src/features/landing/tests/landing.test.tsx +++ b/frontend/src/features/landing/tests/landing.test.tsx @@ -6,7 +6,7 @@ import { Hero } from '../sections/hero/Hero' import { createMemoryRouter, RouterProvider } from 'react-router' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { initI18n } from '@app/i18n' -import { SessionProvider } from '@features/auth/session' +import { SessionProvider } from '@features/auth/session/session' import { json, mockFetch } from '@test/fetch' test('reduced motion shows the full command at once', async () => { diff --git a/frontend/src/features/showcase/components/CopyToSlot.tsx b/frontend/src/features/showcase/components/CopyToSlot.tsx index 4a06707a..f4b2a6ce 100644 --- a/frontend/src/features/showcase/components/CopyToSlot.tsx +++ b/frontend/src/features/showcase/components/CopyToSlot.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@shared/i18n/react' import { Link, useLocation } from 'react-router' import { Notice } from '@shared/ui/Notice' import { ApiError } from '@shared/api/client' -import { useSession } from '@features/auth/session' +import { useSession } from '@features/auth/session/useSession' import { listSlots } from '@features/configs/api' import { copyToSlot } from '../api' diff --git a/frontend/src/shared/layout/topbar/parts.tsx b/frontend/src/shared/layout/topbar/parts.tsx index 38d3a21e..9a8673c2 100644 --- a/frontend/src/shared/layout/topbar/parts.tsx +++ b/frontend/src/shared/layout/topbar/parts.tsx @@ -1,6 +1,6 @@ import { useTranslation } from '@shared/i18n/react' import { Link } from 'react-router' -import { useSession } from '@features/auth/session' +import { useSession } from '@features/auth/session/useSession' import type { Me } from '../../types' /** The pixel-heart mark, standalone (no badge background) — same shape as public/favicon.svg. */ diff --git a/frontend/src/test/render.tsx b/frontend/src/test/render.tsx index 0eef031c..b8b10f7b 100644 --- a/frontend/src/test/render.tsx +++ b/frontend/src/test/render.tsx @@ -1,7 +1,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render } from '@testing-library/react' import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router' -import { SessionProvider } from '@features/auth/session' +import { SessionProvider } from '@features/auth/session/session' export function renderApp(routes: RouteObject[], initialPath = '/') { const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java index 12a3567e..b66231e8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java @@ -19,7 +19,6 @@ import dev.loki.lovisual.Lifecycle; import java.util.Map; -//todo Description @HudElementInfo( id = "better_chat", displayName = "Better Chat", diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java index 04a43132..d8e441d8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java @@ -17,7 +17,6 @@ import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -//todo Description @HudElementInfo( id = "xyz", displayName = "XYZ", diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java index c8035ce9..fe6099b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java @@ -18,7 +18,6 @@ import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -//todo Description @HudElementInfo( id = "speed_bps", displayName = "Speed BPS", diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java index 464da3c4..017c326e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java @@ -20,7 +20,6 @@ final class KeybindsData { if (category == null) return "ellipsis"; return switch (category) { case COMBAT -> "swords"; - case MOVEMENT -> "accessibility"; case PLAYER -> "user"; case VISUALS -> "tree-pine"; case ACCESSORIES -> "shirt"; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java index a56d13b6..2a239204 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java @@ -17,7 +17,6 @@ import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -//todo Description @HudElementInfo( id = "ping", displayName = "Ping", diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java index 0a492fe1..4332e2a9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java @@ -11,7 +11,6 @@ import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -//todo Description @HudElementInfo( id = "armor", displayName = "Armor", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java index 130d8ce5..48e249c4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java @@ -2,7 +2,6 @@ package dev.loki.lovisual.features.module.core; public enum ModuleCategory { COMBAT, - MOVEMENT, PLAYER, VISUALS, ACCESSORIES, diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java index bd2b6e26..b4e9bf28 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java @@ -190,5 +190,7 @@ public enum ModuleManager { catch (Throwable ignored) { DebugLog.error("Failed to load config for module %s", ignored, m.name()); } } - public static void saveAllModuleConfigs() { for (Module m : modulesSnapshot) { try { m.saveConfig(); } catch (Throwable ignored) { } } } + /** Save config for every registered module; logs per-module failures without aborting. */ + public static void saveAllModuleConfigs() { for (Module m : modulesSnapshot) try { m.saveConfig(); } + catch (Throwable ignored) { DebugLog.error("Failed to save config for module %s", ignored, m.name()); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java index 9209046d..46e01742 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java @@ -16,7 +16,6 @@ import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.uniform.impl.post.PostProcessUniforms; -//todo Description @ModuleInfo(id = "damagetint", displayName = "DamageTint", category = ModuleCategory.COMBAT) public class DamageTint extends Module implements PostProcessPass { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java index 4b9158f6..3d2d2be1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java @@ -32,7 +32,6 @@ import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetE import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.setCaptureSpeedScale; import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.tickTargetEsp; -//todo Description @ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT) public class TargetESP extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java index d07648e0..657ce1f2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java @@ -24,7 +24,6 @@ import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.CooldownRulesSe /** * Handles PvP cooldown rendering and optional local rule synthesis. */ -//todo Description @ModuleInfo( id = "pvpcooldowns", displayName = "PvpCooldowns", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java index 0706c625..cb0a397c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java @@ -7,7 +7,6 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -//todo Description @ModuleInfo( id = "spearassist", displayName = "SpearAssist", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java index d9166171..9c6c3832 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java @@ -8,7 +8,6 @@ import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; -//todo Description @ModuleInfo( id = "tpssync", displayName = "TPSSync", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java index 1d7bed5c..7b6a3e99 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java @@ -30,7 +30,6 @@ import dev.loki.lovisual.util.text.mask.ChatNameUtil; * - Players: friend/enemy/staff * - Non-players: toggle ignored entities (EntityFilters) */ -//todo Description @ModuleInfo(id = "definetarget", displayName = "DefineTarget", aliases = {"clickfriend"}, category = ModuleCategory.MISC) public class DefineTarget extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java index ce9e04f3..c73d89be 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java @@ -36,7 +36,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import java.util.UUID; -//todo Description @ModuleInfo( id = "fakeplayer", displayName = "FakePlayer", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java index f789afcc..518b262e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java @@ -11,7 +11,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import java.util.Locale; import java.util.Set; -//todo Description @ModuleInfo( id = "message_filter", displayName = "MessageFilter", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java index 24bd0333..c77589d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java @@ -18,7 +18,6 @@ import dev.loki.lovisual.util.wav.engine.CustomSoundEngine; import java.util.Map; -//todo Description @ModuleInfo( id = "hitsounds", displayName = "HitSounds", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java index 3b2d6980..4403a969 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java @@ -11,7 +11,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import java.util.Locale; import java.util.Set; -//todo Description @ModuleInfo( id = "nosound", displayName = "NoSound", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java index e7e76ffa..104c918a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java @@ -5,7 +5,6 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -//todo Description @ModuleInfo( id = "betterminecraft", displayName = "BetterMinecraft", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java index 7417ef9c..703acb17 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java @@ -15,7 +15,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; -//todo Description @ModuleInfo( id = "autotool", displayName = "AutoTool", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java index c64a4373..973f25a3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java @@ -11,7 +11,6 @@ import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; -//todo Description @ModuleInfo( id = "clickpearl", displayName = "ClickPearl", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java index 0449269f..70ead04a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java @@ -4,7 +4,6 @@ import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -//todo Description @ModuleInfo( id = "nointeract", displayName = "NoInteract", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java index 11d35fe0..e4e1b2a7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java @@ -9,7 +9,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import java.util.LinkedHashMap; -//todo Description @ModuleInfo( id = "fovcontrol", displayName = "FovControl", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java index 4d57de75..1794b4d5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java @@ -8,7 +8,6 @@ import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.lifecycle.Modules; -//todo Description @ModuleInfo( id = "aspectratio", displayName = "AspectRatio", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java index da1e77f6..f6dbe32b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java @@ -5,7 +5,6 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -//todo Description @ModuleInfo( id = "cameraclip", displayName = "CameraClip", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java index 27069f03..715f6935 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java @@ -19,7 +19,6 @@ import dev.loki.lovisual.features.module.modules.visuals.player.FullBright; import java.util.Map; -//todo Description @ModuleInfo(id = "freecam", displayName = "Freecam", aliases = {"camera", "spectator"}, category = ModuleCategory.VISUALS) public class Freecam extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java index cd1297c3..7730221d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java @@ -20,7 +20,6 @@ import dev.loki.lovisual.features.module.modules.visuals.camera.swing.SwingAnima /** * ViewModel: mini items + custom swing animations */ -//todo Description @ModuleInfo(id = "viewmodel", displayName = "ViewModel", category = ModuleCategory.VISUALS) public class ViewModel extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java index 3f034722..1bff3016 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java @@ -28,7 +28,6 @@ import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometr import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesSphere; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -//todo Description @ModuleInfo( id = "worldparticles", displayName = "WorldParticles", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java index 44651faf..a79487b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java @@ -23,7 +23,6 @@ import net.minecraft.world.phys.Vec3; import java.util.HashMap; import java.util.Map; -//todo Description @ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS) public class KillEffect extends DeathEffectSettings { private static final Identifier ORTHODOX_SOUND = diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java index 0cee103f..8fea75b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java @@ -42,7 +42,6 @@ import net.minecraft.world.phys.Vec3; import java.util.*; -//todo Description @ModuleInfo(id = "esp", displayName = "ESP", aliases = {"wallhack", "wh", "outline"}, category = ModuleCategory.VISUALS) public class ESP extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java index a17279d3..6d5d99dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java @@ -23,7 +23,6 @@ import dev.loki.lovisual.render.helpers.clip.ScreenProjection; import java.util.ArrayList; import java.util.List; -//todo Description @ModuleInfo(id = "soundesp", displayName = "SoundESP", category = ModuleCategory.VISUALS) public class SoundESP extends Module { private static final String SETTING_NAME_MODE = "name_mode"; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java index ab014e26..c92760da 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java @@ -32,7 +32,6 @@ import dev.loki.lovisual.util.item.food.IllegalItemUtil; import dev.loki.lovisual.util.item.rarity.RarityColorUtil; import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; -//todo Description @ModuleInfo(id = "dropesp", displayName = "DropESP", category = ModuleCategory.VISUALS) public class DropESP extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java index a69ed069..11777127 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java @@ -21,7 +21,6 @@ import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.uniform.impl.post.MotionBlurUniforms; import dev.loki.lovisual.util.logging.DebugLog; -//todo Description @ModuleInfo( id = "motionblur", displayName = "MotionBlur", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java index d996de77..c393e54d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java @@ -16,7 +16,6 @@ import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.uniform.impl.post.PostFXUniforms; -//todo Description @ModuleInfo(id = "postfx", displayName = "PostFX", category = ModuleCategory.VISUALS) public class PostFX extends Module implements PostProcessPass { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java index 41b98c61..1a536bd6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java @@ -15,7 +15,6 @@ import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.uniform.impl.world.HeatUniforms; -//todo Description @ModuleInfo(id = "totemfx", displayName = "TotemFX", category = ModuleCategory.VISUALS) public class TotemFX extends Module implements PostProcessPass { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java index 84c9d837..6d77acf5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java @@ -18,7 +18,6 @@ import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; -//todo Description @ModuleInfo(id = "backsword", displayName = "BackSword", category = ModuleCategory.VISUALS) public class BackSword extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java index b8cec6ed..f08615ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java @@ -28,8 +28,6 @@ import net.minecraft.world.item.ItemStack; import java.util.*; - -//todo Description @ModuleInfo(id = "nametags", displayName = "NameTags", aliases = {"nametag", "tags", "names"}, category = ModuleCategory.VISUALS) public class NameTags extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java index 7d57997c..24d0a9af 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java @@ -8,7 +8,6 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -//todo Description @ModuleInfo( id = "fullbright", displayName = "FullBright", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java index 10a1be7b..3c9d75cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java @@ -19,7 +19,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.render.engine.renderer.Renderer3D; -//todo Description @ModuleInfo( id = "blockhighlight", displayName = "BlockHighlight", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java index c727b542..56297151 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java @@ -18,7 +18,6 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import java.util.Optional; -//todo Description @ModuleInfo( id = "norender", displayName = "NoRender", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java index 69601ff6..6304259b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java @@ -30,7 +30,6 @@ import dev.loki.lovisual.render.engine.RenderState; import java.time.LocalTime; import java.util.LinkedHashMap; -//todo Description @ModuleInfo(id = "worldtweaks", displayName = "WorldTweaks", category = ModuleCategory.VISUALS) public class WorldTweaks extends Module { @@ -217,7 +216,6 @@ public class WorldTweaks extends Module { return isEnabled() && changeTime.get(); } - public int getTimeOfDayTicks() { if (syncSystemTime.get()) { return getSystemTimeTicks(); diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java index 7e089c38..08d1e648 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java @@ -13,7 +13,6 @@ import dev.loki.lovisual.render.engine.postprocess.render.SkyShaderStarryRendere import dev.loki.lovisual.render.engine.renderer.Renderer3D; import net.minecraft.client.Minecraft; -//todo Description @ModuleInfo(id = "shadersky", displayName = "ShaderSky", category = ModuleCategory.VISUALS) public class ShaderSky extends Module { diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java index ef362033..f0cde130 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java @@ -11,8 +11,6 @@ import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.ai.attributes.Attribute; -import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -60,13 +58,6 @@ public abstract class LivingEntityMixin { @Shadow protected abstract float getJumpPower(); - @Inject(method = "getAttributeValue", at = @At("RETURN")) - private void lovisual$noStunSlownessMovementSpeed(Holder attribute, CallbackInfoReturnable cir) { - if (!attribute.equals(Attributes.MOVEMENT_SPEED)) return; - return; - - } - /** * Optional local cooldown synthesis for servers that lock consumables without sending vanilla cooldown packets. * This never cancels item use; it only starts a visible local timer when explicitly configured. @@ -165,14 +156,6 @@ public abstract class LivingEntityMixin { return Math.max(serverDelta, 0.0f); } - /** - * Local totem cooldowns are handled from the confirmed entity-status packet when explicitly configured. - */ - @Inject(method = "checkTotemDeathProtection", at = @At("RETURN")) - private void lovisual$onTotemUse(DamageSource source, CallbackInfoReturnable cir) { - // no local use blocking or prediction here - } - /** * Сброс кулдаунов при смерти. */ @@ -208,11 +191,6 @@ public abstract class LivingEntityMixin { } } - @Inject(method = "aiStep", at = @At("HEAD")) - private void preventCrash(CallbackInfo ci) { - // ElytraFly module removed; no crash-prevention override remains. - } - @ModifyExpressionValue( method = "updateFallFlyingMovement", at = @At( @@ -367,13 +345,4 @@ public abstract class LivingEntityMixin { || eff == MobEffects.NAUSEA.value(); } - @Inject( - method = "getBlockSpeedFactor", - at = @At("RETURN"), - cancellable = true - ) - private void nostun$soulSandVelocity(CallbackInfoReturnable cir) { - // NoStun module removed; no velocity override remains. - } - } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java index 9d596c47..75e5001d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java @@ -10,10 +10,9 @@ import dev.loki.lovisual.util.aiming.rotation.RotationManager; import dev.loki.lovisual.util.aiming.rotation.RotationUtil; import dev.loki.lovisual.util.aiming.data.Rotation; import dev.loki.lovisual.util.aiming.features.MovementCorrection; -import dev.loki.lovisual.mixins.input.mouse.InputMixin; @Mixin(KeyboardInput.class) -public abstract class KeyboardInputMixin extends InputMixin { +public abstract class KeyboardInputMixin { @ModifyExpressionValue( method = "tick", diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java deleted file mode 100644 index d21dc845..00000000 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.loki.lovisual.mixins.input.mouse; - -import net.minecraft.client.player.ClientInput; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(ClientInput.class) -public abstract class InputMixin { - - @Inject(method = "hasForwardImpulse", at = @At("HEAD"), cancellable = true) - private void allowAllDirections(CallbackInfoReturnable cir) { - // Sprint module removed; keep vanilla forward impulse check. - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java index 814e5ed9..d9625a08 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java @@ -84,16 +84,6 @@ public class MultiPlayerGameModeMixin { cooldowns.tryStartLocalCooldown(stack.getItem(), ItemCooldownRulesValue.Trigger.INTERACT_ACCEPT); } - @Inject(method = "useItemOn", at = @At("HEAD")) - private void onInteractBlockHead(LocalPlayer player, InteractionHand hand, BlockHitResult hit, CallbackInfoReturnable cir) { - // no PvP cooldown block on interactBlock - } - - @Inject(method = "useItemOn", at = @At("RETURN")) - private void onInteractBlockReturn(LocalPlayer player, InteractionHand hand, BlockHitResult hit, CallbackInfoReturnable cir) { - // no PvP cooldown prediction on interactBlock - } - @Inject(method = "useItemOn", at = @At("HEAD"), cancellable = true) private void freecam$block(LocalPlayer player, InteractionHand hand, BlockHitResult hit, CallbackInfoReturnable cir) { diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java index bf9d1955..951617e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java @@ -39,8 +39,6 @@ import dev.loki.lovisual.render.helpers.util.TickDelta; import dev.loki.lovisual.util.combat.tracking.AntiBotTracker; import dev.loki.lovisual.util.session.SessionChanger; import dev.loki.lovisual.util.session.MinecraftGameConfigHolder; -import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns; -import dev.loki.lovisual.features.module.modules.combat.pvp.SpearAssist; @Mixin(Minecraft.class) public class MinecraftMixin implements MinecraftGameConfigHolder { @@ -118,16 +116,6 @@ public class MinecraftMixin implements MinecraftGameConfigHolder { // === КУРСОР ДЛЯ CLICKGUI === - @Inject(method = "continueAttack", at = @At("HEAD"), cancellable = true) - private void onHandleBlockBreaking(boolean breaking, CallbackInfo ci) { - // AutoAttack / Hitbox modules removed; no block-breaking override remains. - } - - @Inject(method = "startAttack", at = @At("HEAD"), cancellable = true) - private void onDoAttack(CallbackInfoReturnable cir) { - // AttributeSwap / AutoAttack modules removed; no attack override remains. - } - @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;ZZ)V", at = @At("HEAD")) private void lovisual$betterChat$saveOnDisconnect(net.minecraft.client.gui.screens.Screen screen, boolean transferring, boolean bl, CallbackInfo ci) { lovisual$guardSingleplayerSaveWithAltUsername(); @@ -319,11 +307,6 @@ public class MinecraftMixin implements MinecraftGameConfigHolder { return Mth.clamp(base * 2, 120, 360); } - @Inject(method = "getTickTargetMillis", at = @At("RETURN"), cancellable = true) - private void lovisual$applyTimer(float millis, CallbackInfoReturnable cir) { - // Timer module removed; tick speed remains unchanged. - } - @Inject(method = "isLevelRunningNormally", at = @At("HEAD"), cancellable = true) private void lovisual$skipPlayerDependentTicksWithoutPlayer(CallbackInfoReturnable cir) { Minecraft mc = Minecraft.getInstance(); diff --git a/mod/src/main/resources/lovisual.mixins.json b/mod/src/main/resources/lovisual.mixins.json index 750d7f21..3265e980 100644 --- a/mod/src/main/resources/lovisual.mixins.json +++ b/mod/src/main/resources/lovisual.mixins.json @@ -124,7 +124,6 @@ "input.keyboard.KeyMappingMixin", "input.keyboard.KeyboardInputMixin", "input.keyboard.KeyboardMixin", - "input.mouse.InputMixin", "input.mouse.MouseMixin", "input.spatialgui.MouseHandlerSpatialGuiMixin", "iris.hand.IrisHandRendererAccessor",