Skip to main content

max / multithreaded

Internal API, typed enums, DocEngine migration, transaction fix - Internal API: HMAC-SHA256 authenticated endpoints for MNW integration - POST /internal/communities (create with default categories) - POST /internal/threads (create with external_ref, idempotent) - GET /internal/threads/:id/stats (post count + last activity) - Migration 021: threads.external_ref column + unique index - 10 integration tests + 2 unit tests for auth - Typed enums: CommunityRole, BanType, ModAction, SortColumn, SortOrder - create_post wrapped in transaction (atomicity fix) - markdown.rs removed, replaced with docengine crate - Config: mnw_base_url as Arc<str>, internal_shared_secret field
Co-Authored-By
Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-22 05:30 UTC
Commit: 738be80d870163847583ba7ee36bbc29435a00b3
Parent: 504f3ea
27 files changed, +1421 insertions, -591 deletions
M Cargo.lock +19 -1
@@ -1007,6 +1007,17 @@
1007 1007 "syn",
1008 1008 ]
1009 1009
1010 + [[package]]
1011 + name = "docengine"
1012 + version = "0.3.0"
1013 + dependencies = [
1014 + "ammonia",
1015 + "pulldown-cmark",
1016 + "regex-lite",
1017 + "serde",
1018 + "uuid",
1019 + ]
1020 +
1010 1021 [[package]]
1011 1022 name = "dotenvy"
1012 1023 version = "0.15.7"
@@ -2070,6 +2081,7 @@
2070 2081 version = "0.3.1"
2071 2082 dependencies = [
2072 2083 "chrono",
2084 + "mt-core",
2073 2085 "serde",
2074 2086 "sqlx",
2075 2087 "tracing",
@@ -2097,16 +2109,17 @@
2097 2109 name = "multithreaded"
2098 2110 version = "0.3.1"
2099 2111 dependencies = [
2100 - "ammonia",
2101 2112 "askama",
2102 2113 "aws-config",
2103 2114 "aws-sdk-s3",
2104 2115 "axum",
2105 2116 "base64",
2106 2117 "chrono",
2118 + "docengine",
2107 2119 "dotenvy",
2108 2120 "governor",
2109 2121 "hex",
2122 + "hmac",
2110 2123 "http-body-util",
2111 2124 "mt-core",
2112 2125 "mt-db",
@@ -2118,6 +2131,7 @@
2118 2131 "serde_json",
2119 2132 "sha2",
2120 2133 "sqlx",
2134 + "tagtree",
2121 2135 "time",
2122 2136 "tokio",
2123 2137 "tower",
@@ -3461,6 +3475,10 @@
3461 3475 "syn",
3462 3476 ]
3463 3477
3478 + [[package]]
3479 + name = "tagtree"
3480 + version = "0.3.0"
3481 +
3464 3482 [[package]]
3465 3483 name = "tendril"
3466 3484 version = "0.4.3"
M Cargo.toml +5 -2
@@ -30,6 +30,7 @@
30 30 # HTTP client / crypto
31 31 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
32 32 sha2 = "0.10"
33 + hmac = "0.12"
33 34 base64 = "0.22"
34 35 rand = "0.8"
35 36
@@ -48,12 +49,12 @@
48 49 chrono = { version = "0.4", features = ["serde"] }
49 50 uuid = { version = "1", features = ["v4", "serde"] }
50 51 pulldown-cmark = "0.12"
51 - ammonia = "4"
52 52 askama = "0.13"
53 53
54 54 # Internal crates
55 55 mt-core = { path = "crates/mt-core" }
56 56 mt-db = { path = "crates/mt-db" }
57 + tagtree = { path = "../tagtree" }
57 58
58 59 [package]
59 60 name = "multithreaded"
@@ -82,13 +83,15 @@
82 83 base64 = { workspace = true }
83 84 rand = { workspace = true }
84 85 pulldown-cmark = { workspace = true }
85 - ammonia = { workspace = true }
86 + docengine = { path = "../docengine", features = ["mentions", "quotes"] }
87 + tagtree = { workspace = true }
86 88 tower_governor = { workspace = true }
87 89 governor = { workspace = true }
88 90 aws-sdk-s3 = { workspace = true }
89 91 aws-config = { workspace = true }
90 92 dotenvy = "0.15"
91 93 hex = "0.4"
94 + hmac = { workspace = true }
92 95 regex-lite = "0.1"
93 96 urlencoding = "2"
94 97 time = "0.3"
M todo.md +61 -5
@@ -1,6 +1,6 @@
1 1 # Multithreaded — Todo
2 2
3 - Done: All pre-beta phases (0-11, 13-24). 222 tests (150 integration + 56 unit lib + 16 unit mt-core). v0.3.0. Audit grade: A (Run 8). Deployed to hetzner+astra (forums.makenot.work). All 20 migrations applied. S3 image uploads configured. MNW Forums tab integration live (MT_BASE_URL set).
3 + Done: All pre-beta phases (0-11, 13-24). 239 tests (167 integration + 56 unit lib + 16 unit mt-core). v0.3.0. Audit grade: A (Run 8). Deployed to hetzner+astra (forums.makenot.work). All 20 migrations applied. S3 image uploads configured. MNW Forums tab integration live (MT_BASE_URL set).
4 4
5 5 Completed work archived in [todo_done.md](todo_done.md).
6 6
@@ -12,12 +12,68 @@
12 12
13 13 ---
14 14
15 + ## Rust Patterns Audit (2026-03-21)
16 +
17 + ### Done
18 + - [x] Create `CommunityRole` enum (Owner/Moderator/Member) replacing string checks (`mt-core/types.rs`, `routes/mod.rs`)
19 + - [x] Create `BanType` enum (Ban/Mute) replacing `&str` parameter (`mt-core/types.rs`, `mt-db/mutations.rs`)
20 + - [x] Create `ModAction` enum replacing raw string mod log actions (`mt-core/types.rs`, `mt-db/mutations.rs`)
21 + - [x] Create `SortColumn`/`SortOrder` enums replacing raw strings (`mt-core/types.rs`, `routes/forum/views.rs`)
22 + - [x] Wrap `create_post` + `last_activity_at` update in a transaction (`mt-db/mutations.rs`)
23 + - [x] Optimize config cloning — clone once at startup, reuse reference
24 +
25 + ---
26 +
27 + ## TagTree Integration (2026-03-21)
28 +
29 + ### Done
30 + - [x] Add `tagtree` workspace dependency
31 + - [x] Replace inline tag slug validation with `tagtree::validate_with()` (TagConfig: max_depth 3, max_length 64)
32 + - [x] Tag slugs now support dot-separated hierarchy (e.g. `rust.async`)
33 + - [x] All 218 tests pass
34 +
35 + ---
36 +
37 + ## Platform Integration (Post-Beta)
38 +
39 + MT becomes the social backbone for MNW. Design doc: `docs/internal/strategy/platform-integration.md`.
40 +
41 + ### Internal API
42 + - [ ] `POST /internal/communities` — create community for an MNW project (HMAC-SHA256 auth)
43 + - [ ] `POST /internal/threads` — create thread linked to MNW item/blog post
44 + - [ ] `POST /internal/posts` — create system post (e.g., "this thread discusses [item]")
45 + - [ ] `GET /internal/threads/{id}/stats` — comment count for embedding in MNW UI
46 + - [ ] Auth middleware: `X-Internal-Signature` header with HMAC-SHA256(timestamp + body, shared_secret)
47 + - [ ] `communities.project_id` nullable FK (links back to MNW project) — migration 021
48 + - [ ] `threads.external_ref` nullable (stores MNW item/blog ID for linking) — migration 021
49 +
50 + ### Default Categories
51 + Auto-provisioned when MNW creates a community:
52 + - [ ] Items (comments on items)
53 + - [ ] Blog (comments on blog posts)
54 + - [ ] Devlog (developer updates)
55 + - [ ] Discussion (general)
56 + - [ ] Issues (git issue tracker replacement — see MNW G8-issues)
57 + - [ ] Patches (inbound email patches — see MNW G7B-patches)
58 + - [ ] Crashes (crash reports from DS2)
59 + - [ ] Feedback (user feedback from DS3)
60 +
61 + ### Private Communities (Fan+)
62 + - [ ] Community visibility flag (public/private)
63 + - [ ] Membership gating: restrict join to Fan+ subscribers or item buyers
64 + - [ ] Hidden from public listing, accessible only via direct link or MNW project page
65 +
66 + ### Notification Integration
67 + - [ ] Push mentions, replies, endorsements, flags to MNW notifications API
68 + - [ ] Read state synced with MNW notification center
69 +
70 + ---
71 +
15 72 ## Deferred (Post-Beta)
16 73
17 74 - [ ] E2E encrypted live chat (OpenMLS integration, WebSocket gateway)
18 - - [ ] Real-time thread updates (WebSocket or SSE for new posts)
19 - - [ ] Community creation by users (currently admin-seeded only)
20 - - [ ] Private communities (invite-only, hidden from listing)
75 + - [ ] Real-time thread updates via shared WebSocket gateway (shared with SyncKit realtime sync — single service)
76 + - [ ] Community creation by users (currently admin-seeded only; MNW auto-provisioning handles project communities)
21 77 - [ ] Federation (ActivityPub or custom protocol)
22 78 - [ ] Subcategories / nested categories
23 79 - [ ] Similar thread detection on new thread creation
@@ -40,7 +96,7 @@
40 96 | Routes | `src/routes/` (mod.rs, forum/{mod,views,actions}.rs, moderation.rs, settings.rs, admin.rs, flagging.rs, tracking.rs, search.rs, uploads.rs) |
41 97 | Auth (OAuth) | `src/auth.rs` |
42 98 | CSRF | `src/csrf.rs` |
43 - | Markdown | `src/markdown.rs` |
99 + | Markdown | `docengine` crate (`active/docengine/`) — features: mentions, quotes |
44 100 | Config | `src/config.rs` |
45 101 | Seed data | `src/seed.rs` |
46 102 | Entry point | `src/main.rs` |
M src/config.rs +7 -2
@@ -1,10 +1,11 @@
1 1 //! Application configuration read from environment variables.
2 2
3 + use std::sync::Arc;
3 4 use uuid::Uuid;
4 5
5 6 #[derive(Clone)]
6 7 pub struct Config {
7 - pub mnw_base_url: String,
8 + pub mnw_base_url: Arc<str>,
8 9 pub oauth_client_id: String,
9 10 pub oauth_redirect_uri: String,
10 11 pub platform_admin_id: Option<Uuid>,
@@ -13,6 +14,8 @@
13 14 pub cookie_secure: bool,
14 15 /// S3 storage configuration. None if S3 env vars are missing.
15 16 pub s3: Option<S3Config>,
17 + /// Shared secret for HMAC-signed internal API requests from MNW.
18 + pub internal_shared_secret: Option<String>,
16 19 }
17 20
18 21 #[derive(Clone)]
@@ -39,7 +42,8 @@
39 42 pub fn from_env() -> Self {
40 43 Self {
41 44 mnw_base_url: std::env::var("MNW_BASE_URL")
42 - .unwrap_or_else(|_| "http://127.0.0.1:3000".to_string()),
45 + .unwrap_or_else(|_| "http://127.0.0.1:3000".to_string())
46 + .into(),
43 47 oauth_client_id: std::env::var("OAUTH_CLIENT_ID")
44 48 .expect("OAUTH_CLIENT_ID must be set"),
45 49 oauth_redirect_uri: std::env::var("OAUTH_REDIRECT_URI")
@@ -51,6 +55,7 @@
51 55 .map(|v| v != "false")
52 56 .unwrap_or(true),
53 57 s3: S3Config::from_env(),
58 + internal_shared_secret: std::env::var("INTERNAL_SHARED_SECRET").ok(),
54 59 }
55 60 }
56 61 }
M src/lib.rs +1 -1
@@ -3,8 +3,8 @@
3 3 pub mod auth;
4 4 pub mod config;
5 5 pub mod csrf;
6 + pub mod internal_auth;
6 7 pub mod link_preview;
7 - pub mod markdown;
8 8 pub mod routes;
9 9 pub mod seed;
10 10 pub mod storage;
M src/main.rs +3 -1
@@ -86,7 +86,7 @@
86 86 ))
87 87 .with_secure(state.config.cookie_secure);
88 88
89 - let app = multithreaded::routes::forum_routes(state)
89 + let app = multithreaded::routes::forum_routes(state.clone())
90 90 .layer(axum::middleware::from_fn(csrf::csrf_middleware))
91 91 .layer(session_layer)
92 92 .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
@@ -103,6 +103,8 @@
103 103 axum::http::header::X_FRAME_OPTIONS,
104 104 axum::http::HeaderValue::from_static("DENY"),
105 105 ))
106 + // Internal API routes — HMAC auth only, no CSRF/session middleware
107 + .merge(multithreaded::routes::internal::internal_routes(state))
106 108 .nest_service("/static", ServeDir::new("static"));
107 109
108 110 let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
@@ -4,6 +4,7 @@
4 4 edition.workspace = true
5 5
6 6 [dependencies]
7 + mt-core = { workspace = true }
7 8 sqlx = { workspace = true }
8 9 chrono = { workspace = true }
9 10 uuid = { workspace = true }
@@ -13,6 +13,8 @@
13 13 use crate::templates::*;
14 14 use crate::AppState;
15 15
16 + use mt_core::types::ModAction;
17 +
16 18 use super::{log_mod_action, parse_uuid, AdminSearchQuery, SuspendForm};
17 19
18 20 #[tracing::instrument(skip_all)]
@@ -93,7 +95,7 @@
93 95
94 96 log_mod_action(
95 97 &state.db, None, admin.user_id,
96 - "suspend_community", None, Some(community_id), reason,
98 + ModAction::SuspendCommunity, None, Some(community_id), reason,
97 99 ).await;
98 100
99 101 Ok(Redirect::to("/_admin?toast=Community+suspended"))
@@ -116,7 +118,7 @@
116 118
117 119 log_mod_action(
118 120 &state.db, None, admin.user_id,
119 - "unsuspend_community", None, Some(community_id), None,
121 + ModAction::UnsuspendCommunity, None, Some(community_id), None,
120 122 ).await;
121 123
122 124 Ok(Redirect::to("/_admin?toast=Community+unsuspended"))
@@ -141,7 +143,7 @@
141 143
142 144 log_mod_action(
143 145 &state.db, None, admin.user_id,
144 - "suspend_user", Some(user_id), None, reason,
146 + ModAction::SuspendUser, Some(user_id), None, reason,
145 147 ).await;
146 148
147 149 Ok(Redirect::to("/_admin?toast=User+suspended"))
@@ -164,7 +166,7 @@
164 166
165 167 log_mod_action(
166 168 &state.db, None, admin.user_id,
167 - "unsuspend_user", Some(user_id), None, None,
169 + ModAction::UnsuspendUser, Some(user_id), None, None,
168 170 ).await;
169 171
170 172 Ok(Redirect::to("/_admin?toast=User+unsuspended"))
M src/routes/mod.rs +17 -12