chore(history): squash 100 commit(s) from 2026-09-24
- fix(backend): case-insensitive unique email, revoke PUBLIC schema access, pin Argon2id params - test(backend): assert password length cap boundary (256 ok, 257 rejected) - docs(backend): plan — typed JWT token kinds so refresh/device tokens cannot pass as access tokens - feat(backend): JWT access/refresh token issue and verify - feat(backend): accounts repository (create/find_by_email/find_by_id) - refactor(gui): LoVisualAddonManagerScreen 989→10 файлов addon/ (8.5.2) - docs(backend): plan — fix sqlx::migrate! path in integration tests - feat(backend): POST /auth/register and /auth/login - refactor(settings): SettingsPanelComponent 715→81 + 6 helpers (8.5.2) - docs(backend): plan — harden device flow (single-use codes, bounded store, 404/429) - feat(backend): OAuth device authorization grant for mod login - fix(backend): first confirm wins for device codes - docs(backend): plan — split Task 8 (refactor) and Task 9 (avatars), harden avatar handling - refactor(mixins): LocalPlayerMixin 691→111 + 4 handlers (8.5.2) - refactor(visuals): Trails 687→130 (8.5.2) - refactor(render): ItemBatchRenderer 677->100 (8.5.2) - refactor(visuals): ReimaginedVisual 674→118 + 5 helpers (8.5.2) - refactor(config): ConfigSerializer 668→91 + 4 helpers (8.5.2) - refactor(hud): DynamicIsland 661→158 + 4 helpers (8.5.2) - refactor(render): GlStencilFramebufferSupport 666→169 (8.5.2) - refactor(gui): MenuScreen 669→128 + 4 helpers (8.5.2) - refactor(render): UiStyle 644→170 + 3 helpers (8.5.2) - refactor(gui): ModuleComponent 613→98 + 4 helpers (8.5.2) - refactor(media): MediaSessionService 616→200 + 4 helpers (8.5.2) - refactor(gui): RelationsComponent 661→59 + 4 helpers (8.5.2) - chore(license): strip GPL file headers from all Java sources - refactor(aiming): PointTracker 583→168 + 2 helpers (8.5.2) - refactor(gui): LoVisualProxyManagerScreen 591→132 + 2 helpers (8.5.2) - refactor(visuals): KillEffect 588→96 + 4 helpers (8.5.2) - refactor(render): MeshBuilder +4 helpers (8.5.2) - refactor(visuals): extract WorldParticlesRender helper (8.5.2) - refactor(world): ExplosionDamageUtil 551→116 + 2 helpers (8.5.2) - refactor(visuals): TazikHat 596->179 + Model + Palette in hats/tazik (8.5.2) - chore(license): strip GPL header from remaining 30 files and make strip script variant-aware - refactor(gui): ThemeComponent 561->166 + CardRenderer + ScrollState (8.5.2) - refactor(hud): CustomHotbar 556→178 + Renderer + Selection + SelectionGradient (8.5.2) - refactor(gui): ThemeCardRenderer perf + readability polish - refactor(clickgui): CooldownRulesSetting 596->198 + Editor + DetailRenderer (8.5.2) - refactor(hud): HudNotifier 561->200 + Painter + runtime/HudNotifierRuntime (8.5.2) - refactor(theme): Themes 555->168 + impl/Transition + impl/Blending + impl/ProfileCodec (8.5.2) - refactor(theme): EditableClickGuiTheme 205->185 + JavaDoc (8.5.2) - refactor(theme): ThemeStore 491->128 + store/ThemeStoreJson + store/ThemeStoreIO (8.5.2) - refactor(clickgui): ClickGuiRenderer 604->200 compacted one-line delegators + JavaDoc (8.5.2) - refactor(mainmenu): LoVisualMainMenuScreen 551->161 + impl/Painter + impl/Renderer + impl/TextUtil (8.5.2) - refactor(clickgui): ClickGuiTextEditorState 531->187 + impl/EditorCaret + impl/EditorPainter (8.5.2) - refactor(tab): TabListModel 525->139 + model/Collector + model/Reader + model/Signature + model/TextSplitter (8.5.2) - refactor(backend): shared bearer helper and test helpers, build_app takes Config, validate JWT secret strength - refactor(module): ModuleManager 521->198 + impl/Registrar + impl/Dispatcher (8.5.2) - feat(backend): avatar upload with decode, square crop, PNG re-encode and S3 storage - refactor(clip): ClipFunction 512->146 + impl/Geometry + impl/Debug (8.5.2) - docs(backend): implementation plans for gateway (auth hardening, gRPC, rate limits) and configs-service - refactor(iris-patch): ShaderPatchEngine 499->146 + impl/Repo (8.5.2) - chore(frontend): add router, react-query, fonts and vitest; dev proxy to gateway - refactor(hud): ScriptedListHudPanel 499->158 + panel/Props + panel/Signature (8.5.2) - refactor(hud): BaseHudElement 499->199 + impl/Registry + impl/Namer + impl/Prewarm (8.5.2) - refactor(clickgui): Setting 498->170 + impl/Localization + impl/I18n (8.5.2) - refact(viewmodel): split swing animations into camera/swing package - refact(kineticlyrics): split module into stage, playback and modes - rename(holeesp): module HoleESP -> CrystalHoles - refact(crystalholes): split module into crystal scanner, renderer and safety - refact(addonmanager): split manager into lifecycle, runtime, descriptors and profiles - refact(accountconfig): split config into store, session and value helpers - refactor(render): CustomTextRenderer 229->195, extract glyph-pass into GradientTexts helper - docs(TODO): mark AddonManager split done; close 9.2 refactor gate - refactor(media): LinuxMediaSession 441->148, split reader + track/seek state - refactor(nametags): split NameTags into facade + impl helpers - refactor(clickgui): split MainSettingsComponent into facade + scroll + model - refactor(hud): split CustomBar into facade, model and BarSettings - docs(frontend): implementation plan with design system from the mod theme - feat(frontend): design tokens from the mod theme, fonts and shared UI kit - fix(accounts): run migrations on startup, offload Argon2, validate register input, JSON error shape - docs(gateway): plan note on splitting auth handlers before refresh endpoints - feat(frontend): API client with silent refresh, error descriptions and test helpers - style(mod): group compact one-line bulk query methods in ModuleManager - feat(frontend): session restore, login and registration with client-side validation - refactor(hud): split CustomHealthBar into facade + painter + script renderer - docs(mod): record the 2026-09-24 HUD/settings split wave in TODO phase 8.5 - refactor(rhi): split GlStencilShapeClipBackend into facade + native-state + pass-lifecycle helpers - refactor(rhi): split VulkanRenderStateBridge into facade + MSAA and stencil state helpers - refactor(backtrack): split BacktrackController into facade + model + impl helpers - refactor(svg): split SvgPathParser into facade + arc geometry + command/curve helpers - refactor(mixin): split ClientPacketListenerMixin into hook-only mixin + handlers - refactor(renderer3d): un-nest batch bindings + culling into sibling impl types - refactor(renderwarp): extract static factories + geometry into impl helpers - feat(backend): add common crate with shared JWT, internal gateway contract and accounts proto - refactor(guimixin): move hook bodies into handlers, keep mixin as hooks + shadows - feat(accounts): accept only gateway traffic, read identity from gateway header - refactor(cacheduiscriptruntime): extract engine, hashing and frame stats into impl - refactor(customskyboxrenderer): extract projection, shader passes and sun into impl - refactor(betterchatstoremanager): extract persistence, key/path and hover helpers into impl - feat(accounts): rotating opaque refresh tokens in httpOnly cookie, /auth/refresh and /auth/logout - refactor(targetesp): extract crystal rendering subsystem into impl/TargetEspCrystalRenderer - refactor(betterchathovercache): extract disk codec and lookup indexing into impl/ChatHoverCacheCodec - refactor(microsoftauth): split HTTP transport, device-code and Xbox flows into impl/ - refactor(pvpcooldowns): extract local item-rule engine and defaults into impl/PvpCooldownRules - refactor(lovisual): extract HUD/world render orchestration into HudRender helper - refactor(statuseffectheuristics): extract palette/inference into ParticlePalette and color utils into ParticleColors - refactor(dropesp): extract overlay/label render subsystem into impl/DropEspOverlayRenderer - refactor(proxy): extract SOCKS handshake message builders into ProxyProtocolMessages - refactor(eagleutil): promote EdgeRecovery controller and RecoveryMode to top-level class
This commit is contained in:
parent
9d08fa910a
commit
72bc4c7148
1897 changed files with 36199 additions and 39289 deletions
2
backend/accounts-service/src/accounts/mod.rs
Normal file
2
backend/accounts-service/src/accounts/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod model;
|
||||
pub mod repo;
|
||||
15
backend/accounts-service/src/accounts/model.rs
Normal file
15
backend/accounts-service/src/accounts/model.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
|
||||
pub struct Account {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub password_hash: String,
|
||||
pub display_nick: String,
|
||||
pub role: String,
|
||||
pub can_publish_addons: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
168
backend/accounts-service/src/accounts/repo.rs
Normal file
168
backend/accounts-service/src/accounts/repo.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
use super::model::Account;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Emails are stored lowercase and compared via the `lower(email)` unique
|
||||
// index (see migrations/0001_init.sql). Every entry point normalizes here.
|
||||
pub fn normalize_email(email: &str) -> String {
|
||||
email.trim().to_lowercase()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
pool: &PgPool,
|
||||
email: &str,
|
||||
password_hash: &str,
|
||||
nick: &str,
|
||||
) -> Result<Account, sqlx::Error> {
|
||||
sqlx::query_as::<_, Account>(
|
||||
"INSERT INTO accounts (email, password_hash, display_nick)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, password_hash, display_nick, role, can_publish_addons, created_at",
|
||||
)
|
||||
.bind(normalize_email(email))
|
||||
.bind(password_hash)
|
||||
.bind(nick)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_email(pool: &PgPool, email: &str) -> Result<Option<Account>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Account>(
|
||||
"SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at
|
||||
FROM accounts WHERE lower(email) = $1",
|
||||
)
|
||||
.bind(normalize_email(email))
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Account>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Account>(
|
||||
"SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at
|
||||
FROM accounts WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_avatar(pool: &PgPool, account_id: Uuid, s3_key: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT INTO avatars (account_id, s3_key) VALUES ($1, $2)
|
||||
ON CONFLICT (account_id) DO UPDATE SET s3_key = EXCLUDED.s3_key, uploaded_at = now()",
|
||||
)
|
||||
.bind(account_id)
|
||||
.bind(s3_key)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn test_pool() -> PgPool {
|
||||
let url = std::env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into());
|
||||
let pool = PgPool::connect(&url).await.expect("connect");
|
||||
sqlx::migrate!("./migrations").run(&pool).await.expect("migrate");
|
||||
pool
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_then_find_by_email_round_trips() {
|
||||
let pool = test_pool().await;
|
||||
let email = format!("test-{}@example.com", Uuid::new_v4());
|
||||
|
||||
let created = create(&pool, &email, "hash123", "TestNick").await.unwrap();
|
||||
assert_eq!(created.email, email);
|
||||
assert_eq!(created.role, "user");
|
||||
assert!(created.can_publish_addons);
|
||||
|
||||
let found = find_by_email(&pool, &email).await.unwrap().expect("must exist");
|
||||
assert_eq!(found.id, created.id);
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE id = $1")
|
||||
.bind(created.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_email_returns_none_for_missing() {
|
||||
let pool = test_pool().await;
|
||||
let result = find_by_email(&pool, "does-not-exist@example.com").await.unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn email_lookup_is_case_insensitive_and_stored_lowercase() {
|
||||
let pool = test_pool().await;
|
||||
let tag = Uuid::new_v4();
|
||||
let mixed = format!("MiXeD-{tag}@Example.COM");
|
||||
|
||||
let created = create(&pool, &mixed, "hash123", "Nick").await.unwrap();
|
||||
assert_eq!(created.email, mixed.to_lowercase());
|
||||
|
||||
let found = find_by_email(&pool, &mixed.to_uppercase())
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("lookup must ignore case");
|
||||
assert_eq!(found.id, created.id);
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE id = $1")
|
||||
.bind(created.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_email_differing_only_by_case_is_rejected() {
|
||||
let pool = test_pool().await;
|
||||
let tag = Uuid::new_v4();
|
||||
let first = create(&pool, &format!("dup-{tag}@example.com"), "h", "A").await.unwrap();
|
||||
|
||||
let second = create(&pool, &format!("DUP-{tag}@EXAMPLE.com"), "h", "B").await;
|
||||
let err = second.expect_err("case-variant duplicate must violate the unique index");
|
||||
match err {
|
||||
sqlx::Error::Database(db) => {
|
||||
assert_eq!(db.constraint(), Some("accounts_email_lower_idx"));
|
||||
}
|
||||
other => panic!("expected a database unique violation, got {other:?}"),
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE id = $1")
|
||||
.bind(first.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_avatar_inserts_then_updates_in_place() {
|
||||
let pool = test_pool().await;
|
||||
let created = create(&pool, &format!("av-{}@example.com", Uuid::new_v4()), "h", "N")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
set_avatar(&pool, created.id, "avatars/one.png").await.unwrap();
|
||||
set_avatar(&pool, created.id, "avatars/two.png").await.unwrap();
|
||||
|
||||
let rows: Vec<(String,)> =
|
||||
sqlx::query_as("SELECT s3_key FROM avatars WHERE account_id = $1")
|
||||
.bind(created.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows, vec![("avatars/two.png".to_string(),)]);
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE id = $1")
|
||||
.bind(created.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue