chore(history): squash 59 commit(s) from 2026-09-23

- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2)
- refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2)
- refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2)
- refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2)
- refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2)
- refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2)
- refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2)
- refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2)
- refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2)
- refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2)
- refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2)
- refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2)
- refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2)
- refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2)
- refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2)
- refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2)
- refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2)
- refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2)
- refactor(hud): Potions 617→176 + 4×≤166 (8.5.2)
- refactor(hud): Admins 702→118 + 4×≤138 (8.5.2)
- refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2)
- refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2)
- refactor(hud): Radar 392→161 + 2×≤124 (8.5.2)
- refactor(hud): Fps 309→168 + 3×≤107 (8.5.2)
- refactor(hud): Memory 304→151 + 3×≤144 (8.5.2)
- refactor(hud): Tps 339→154 + 3×≤143 (8.5.2)
- refactor(hud): Ping 309→145 + 3×≤125 (8.5.2)
- refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2)
- refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2)
- refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2)
- refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2)
- refactor(hud): Armor 324→162 + 2×≤148 (8.5.2)
- refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2)
- docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост)
- docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе)
- refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2)
- refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2)
- refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2)
- docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО
- refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2)
- docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата
- docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка
- docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3
- chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/
- docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1)
- chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md
- docs(backend): edition 2024 в плане вместо 2021
- chore: обновить версии до реально актуальных (проверено компиляцией)
- docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md
- docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend)
- docs: commit messages in English from now on
- chore: gitignore .superpowers/ scratch directory
- docs: commit messages in English from now on (mod)
- feat(backend): scaffold accounts-service with health check
- chore(backend): remove target/ build artifacts from git, add gitignore
- chore: track docs/ in git (was accidentally gitignored, never committed)
- chore(backend): convert to Cargo workspace, plan full microservice layout
- feat(backend): accounts/avatars/device_links schema + migration
- feat(backend): argon2id password hashing
This commit is contained in:
loki5512344 2026-09-23 22:38:08 +02:00
parent ec10395511
commit 9d08fa910a
2239 changed files with 35265 additions and 28110 deletions

7
backend/.env.example Normal file
View file

@ -0,0 +1,7 @@
DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db
JWT_SECRET=change-me-to-a-long-random-string
PORT=8081
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=lovisual-avatars
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin

2
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.env

3931
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
backend/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[workspace]
resolver = "2"
members = [
"accounts-service",
]
# Planned members, added when their own implementation plan starts
# (see backend/STRUCTURE.md and TODO.md Фаза 10 / Подсистемы 1-3):
# "gateway" — API gateway: routing + single JWT check point + rate limits
# "configs-service" — 4 config slots, share codes, showcase (Подсистема 1)
# "chat-service" — RPC chat, friends, presence, telemetry widgets (Подсистема 2)
# "addons-registry" — addon marketplace backend (Подсистема 3)

1744
backend/PLAN.md Normal file

File diff suppressed because it is too large Load diff

44
backend/STRUCTURE.md Normal file
View file

@ -0,0 +1,44 @@
# Backend Structure (продумано заранее, 2026-09-23)
Rust workspace (`backend/Cargo.toml`), один сервис — один крейт. Планировка
на все три подсистемы платформы (см. `TODO.md`, Фаза 10) сразу, чтобы не
переизобретать layout при переходе от Подсистемы 1 к 2/3.
```
backend/
Cargo.toml # workspace root — members растёт по мере реализации
STRUCTURE.md # этот файл
accounts-service/ # РЕАЛИЗУЕТСЯ — backend/PLAN.md (Подсистема 1, часть 1)
gateway/ # ПЛАН НЕ НАПИСАН — routing + единая точка JWT-проверки
# + рейт-лимиты (TODO.md §6). Начинается после
# accounts-service, т.к. фронтит уже готовый сервис.
configs-service/ # ПЛАН НЕ НАПИСАН — 4 слота конфигов, share-коды,
# витрина (Подсистема 1, часть 2)
chat-service/ # ПЛАН НЕ НАПИСАН — RPC-чат, друзья, presence,
# виджеты-телеметрия (Подсистема 2). Единственный
# сервис с WebSocket, а не только REST.
addons-registry/ # ПЛАН НЕ НАПИСАН — витрина аддонов, версии,
# публикация/модерация (Подсистема 3)
```
## Почему не добавлены в `[workspace] members` сразу
Пустой крейт без реализации — то же самое "без пустышек", что запрещено
правилами мода (см. `TODO.md`) — просто в Rust-обёртке: `cargo build
--workspace` не должен спотыкаться о нереализованные заглушки. Каждый
сервис входит в `members` в своём implementation-плане первым шагом (как
`accounts-service` в Task 1 `backend/PLAN.md`), не раньше.
## Общий код между сервисами (`common`, пока не создан)
Кандидаты на вынос в `backend/common/` **когда появится второй реальный
потребитель** (не раньше — YAGNI): JWT `Claims`/`verify_token` (сейчас
только в `accounts-service`, но `gateway` тоже будет его проверять — при
старте `gateway` вынести в `common`), типовой `AppError`. До этого момента
дублирование двух сервисов — не проблема, преждевременная общая библиотека
между одним реальным потребителем — проблема.
## Модульная структура внутри сервиса (эталон — `accounts-service`)
Домен, не технический слой: `auth/`, `accounts/`, `device/`, `avatars/` —
каждый со своими `mod.rs`/`handlers.rs`/`repo.rs` и т.п. (см. File Structure
в `backend/PLAN.md`). Тот же принцип — для `configs-service` (`slots/`,
`sharing/`, `showcase/`) и `chat-service` (`chat/`, `friends/`,
`telemetry/`) при их реализации.

2
backend/accounts-service/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.env

3931
backend/accounts-service/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
[package]
name = "accounts-service"
version = "0.1.0"
edition = "2024"
[lib]
name = "accounts_service"
path = "src/lib.rs"
[dependencies]
axum = { version = "0.8", features = ["multipart"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.7", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = "0.3"
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"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
argon2 = "0.6"
jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] }
rand = "0.10"
dashmap = "6"
aws-sdk-s3 = "1"
aws-config = "1"
anyhow = "1"
dotenvy = "0.15"
[dev-dependencies]
axum-test = "21"

View file

@ -0,0 +1,27 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_nick TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
can_publish_addons BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE avatars (
account_id UUID PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
s3_key TEXT NOT NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE device_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
device_token_hash TEXT NOT NULL,
linked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen TIMESTAMPTZ
);
CREATE INDEX idx_device_links_account_id ON device_links(account_id);

View file

@ -0,0 +1 @@
pub mod password;

View file

@ -0,0 +1,37 @@
use argon2::{Argon2, PasswordHasher, PasswordVerifier};
use argon2::password_hash::phc::PasswordHash;
pub fn hash_password(plain: &str) -> Result<String, argon2::password_hash::Error> {
let argon2 = Argon2::default();
Ok(argon2.hash_password(plain.as_bytes())?.to_string())
}
pub fn verify_password(plain: &str, hash: &str) -> bool {
let Ok(parsed) = PasswordHash::new(hash) else { return false };
Argon2::default()
.verify_password(plain.as_bytes(), &parsed)
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_then_verify_succeeds() {
let hash = hash_password("correct-horse-battery-staple").unwrap();
assert!(verify_password("correct-horse-battery-staple", &hash));
}
#[test]
fn wrong_password_fails_verify() {
let hash = hash_password("correct-horse-battery-staple").unwrap();
assert!(!verify_password("wrong-password", &hash));
}
#[test]
fn hash_is_not_the_plain_password() {
let hash = hash_password("secret123").unwrap();
assert_ne!(hash, "secret123");
}
}

View file

@ -0,0 +1,35 @@
use anyhow::{Context, Result};
#[derive(Clone)]
pub struct Config {
pub database_url: String,
pub jwt_secret: String,
pub port: u16,
pub s3_endpoint: String,
pub s3_bucket: String,
pub s3_access_key: String,
pub s3_secret_key: String,
}
impl Config {
pub fn from_env() -> Result<Config> {
Ok(Config {
database_url: std::env::var("DATABASE_URL")
.context("DATABASE_URL not set")?,
jwt_secret: std::env::var("JWT_SECRET")
.context("JWT_SECRET not set")?,
port: std::env::var("PORT")
.unwrap_or_else(|_| "8081".into())
.parse()
.context("PORT must be a number")?,
s3_endpoint: std::env::var("S3_ENDPOINT")
.context("S3_ENDPOINT not set")?,
s3_bucket: std::env::var("S3_BUCKET")
.context("S3_BUCKET not set")?,
s3_access_key: std::env::var("S3_ACCESS_KEY")
.context("S3_ACCESS_KEY not set")?,
s3_secret_key: std::env::var("S3_SECRET_KEY")
.context("S3_SECRET_KEY not set")?,
})
}
}

View file

@ -0,0 +1,8 @@
pub mod auth;
pub mod config;
use axum::{routing::get, Router};
pub fn build_app(_pool: sqlx::PgPool) -> Router {
Router::new().route("/health", get(|| async { "ok" }))
}

View file

@ -0,0 +1,19 @@
use accounts_service::{build_app, config::Config};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
dotenvy::dotenv().ok();
let cfg = Config::from_env()?;
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(10)
.connect(&cfg.database_url)
.await?;
let app = build_app(pool);
let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?;
tracing::info!("accounts-service listening on {}", cfg.port);
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -0,0 +1,14 @@
#[tokio::test]
async fn migrations_create_accounts_table() {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into());
let pool = sqlx::PgPool::connect(&database_url).await.expect("connect");
sqlx::migrate!("./migrations").run(&pool).await.expect("migrate");
let row: (i64,) = sqlx::query_as("SELECT count(*) FROM accounts")
.fetch_one(&pool)
.await
.expect("accounts table must exist");
assert_eq!(row.0, 0);
}

View file

@ -0,0 +1,16 @@
use axum_test::TestServer;
#[tokio::test]
async fn health_returns_ok() {
// NOTE: this test does not touch the DB — pass a pool that is never
// queried. sqlx::PgPool::connect_lazy never opens a connection until
// a query runs, so this is safe without a running Postgres.
let pool = sqlx::PgPool::connect_lazy("postgres://user:pass@localhost/db")
.expect("lazy pool");
let app = accounts_service::build_app(pool);
let server = TestServer::new(app);
let response = server.get("/health").await;
response.assert_status_ok();
response.assert_text("ok");
}

View file

@ -0,0 +1,5 @@
# addons-registry (planned)
Addon marketplace backend: versions, publishing, moderation (Подсистема 3).
See `TODO.md` (Фаза 10, "Подсистема 3") and `backend/STRUCTURE.md`. No
implementation plan yet.

View file

@ -0,0 +1,5 @@
# chat-service (planned)
RPC chat, friends, presence, telemetry widgets (Подсистема 2). WebSocket, not
just REST — see `TODO.md` (Фаза 10, "Подсистема 2") and
`backend/STRUCTURE.md`. No implementation plan yet.

View file

@ -0,0 +1,5 @@
# configs-service (planned)
4 cloud config slots per account, share codes, public showcase (Подсистема 1,
часть 2). See `TODO.md` (Фаза 10) and `backend/STRUCTURE.md`. No
implementation plan yet.

View file

@ -0,0 +1,6 @@
# gateway (planned)
Routing + single JWT check point + rate limits in front of `accounts-service`,
`configs-service`, `chat-service`. See `TODO.md` (Фаза 10) and
`backend/STRUCTURE.md`. No implementation plan yet — starts after
`accounts-service` (`backend/PLAN.md`) is done.