Skip to main content

max / makenotwork

8.2 KB · 218 lines History Blame Raw
1 # Contributing to Multithreaded
2
3 Patterns, conventions, and rules for working on the Multithreaded forum codebase.
4
5 ## Project Structure
6
7 ```
8 multithreaded/ (workspace root)
9 Cargo.toml # Workspace + root crate deps
10 src/
11 lib.rs # AppState, module declarations
12 main.rs # Entry point, migrations, server bind
13 auth.rs # MNW OAuth (PKCE), session extractors
14 csrf.rs # CSRF middleware (synchronizer token)
15 config.rs # Config from environment
16 internal_auth.rs # HMAC-SHA256 auth for MNW→MT internal API
17 link_preview.rs # OG metadata extraction
18 storage.rs # S3 wrapper around s3-storage crate
19 seed.rs # Seed data for development
20 routes/
21 mod.rs # Route tree, rate limiting
22 helpers.rs # Reusable route helpers (get_community, render_markdown)
23 forum/ # Forum views, thread, posts, actions
24 moderation.rs # Mod tools
25 flagging.rs # Content flagging
26 settings.rs # Community settings
27 admin.rs # Platform admin
28 search.rs # Full-text search
29 tracking.rs # Read state tracking
30 uploads.rs # Image uploads (S3)
31 templates/
32 mod.rs # Template structs + IntoResponse macro
33 public.rs # Public page templates
34 admin.rs # Admin templates
35 crates/
36 mt-core/ # Domain types (enums, time formatting)
37 mt-db/ # Database queries and mutations (sqlx)
38 templates/ # Askama HTML templates
39 static/ # CSS, JS, fonts
40 migrations/ # PostgreSQL migrations (auto-applied on boot)
41 tests/ # Integration tests
42 deploy/ # Deploy scripts, systemd unit
43 ```
44
45 ### Crate Boundaries
46
47 | Crate | Role | May depend on |
48 |-------|------|---------------|
49 | `mt-core` | Domain enums, time formatting | Nothing internal |
50 | `mt-db` | SQL queries and mutations | `mt-core` |
51 | root crate | Routes, templates, auth, config | `mt-core`, `mt-db`, shared libs |
52
53 Library crates (`mt-core`, `mt-db`) contain no web framework types. Routes and templates live in the root crate only.
54
55 ## MNW OAuth Integration
56
57 Multithreaded delegates all authentication to MNW via OAuth 2.0 with PKCE. There are no local passwords or signup forms. See [architecture.md § Authentication]docs/architecture.md#4-authentication for the full flow (PKCE parameters, state nonce validation, retry behavior, session cycling).
58
59 **Extractors:**
60 - `MaybeUser(Option<SessionUser>)` — optional auth, infallible (never rejects)
61 - `PlatformAdmin(SessionUser)` — admin-only, returns 404 to hide admin routes from non-admins
62
63 ## Route Handlers
64
65 ### Signature Pattern
66
67 ```rust
68 #[tracing::instrument(skip_all)]
69 pub(in crate::routes) async fn handler_name(
70 axum::extract::State(state): axum::extract::State<AppState>,
71 session: Session,
72 MaybeUser(session_user): MaybeUser,
73 Path((slug, category)): Path<(String, String)>,
74 Query(page_query): Query<PageQuery>,
75 ) -> Result<impl IntoResponse, Response> {
76 let csrf_token = Some(csrf::get_or_create_token(&session).await);
77 let community = get_community(&state.db, &slug).await?;
78 // ... build template struct ...
79 Ok(MyTemplate { csrf_token, session_user, /* ... */ })
80 }
81 ```
82
83 ### Error Handling
84
85 Multithreaded uses `Result<impl IntoResponse, Response>` directly — no centralized `AppError` type. Errors are converted to `Response` inline via helper functions in `routes/helpers.rs`:
86
87 ```rust
88 pub(crate) async fn get_community(db: &PgPool, slug: &str) -> Result<CommunityRow, Response> {
89 mt_db::queries::get_community_by_slug(db, slug)
90 .await
91 .map_err(|e| {
92 tracing::error!(error = ?e, "db error fetching community");
93 StatusCode::INTERNAL_SERVER_ERROR.into_response()
94 })?
95 .ok_or_else(|| StatusCode::NOT_FOUND.into_response())
96 }
97 ```
98
99 Use these helpers for consistent error responses. All DB errors log with `tracing::error!` and return 500. Missing resources return 404.
100
101 ### Rate Limiting
102
103 Write routes (POST) use `tower_governor` rate limiting. Read routes (GET) have no rate limit. The rate limiter is applied via `.route_layer()` on the write routes group.
104
105 ## Templates
106
107 Askama templates with Jinja2-like syntax. Template structs live in `src/templates/`, HTML files in `templates/`.
108
109 ### IntoResponse Macro
110
111 Template structs get `IntoResponse` via a bulk macro:
112
113 ```rust
114 impl_into_response!(
115 ForumDirectoryTemplate,
116 CommunityTemplate,
117 ThreadTemplate,
118 // ...
119 );
120 ```
121
122 This handles rendering and returns 500 if template rendering fails.
123
124 ### Layout Inheritance
125
126 All full pages extend `templates/base.html`:
127 ```html
128 {% extends "base.html" %}
129 {% block title %}Page Title{% endblock %}
130 {% block header %}{% include "partials/site_header.html" %}{% endblock %}
131 {% block content %}
132 <!-- page content -->
133 {% endblock %}
134 ```
135
136 Every full-page template struct needs:
137 - `csrf_token: Option<String>` — for the CSRF meta tag
138 - `session_user: Option<TemplateSessionUser>` — for header login state
139 - `mnw_base_url: Arc<str>` — for links back to MNW
140
141 ## Database Layer
142
143 Queries and mutations live in `crates/mt-db/`. Same sqlx patterns as MNW server:
144
145 ```rust
146 pub async fn list_communities(pool: &PgPool, limit: i64, offset: i64) -> Result<Vec<CommunityListRow>, sqlx::Error> {
147 sqlx::query_as::<_, CommunityListRow>(
148 "SELECT co.name, co.slug, co.description, ... FROM communities co ...
149 LIMIT $1 OFFSET $2",
150 )
151 .bind(limit).bind(offset)
152 .fetch_all(pool).await
153 }
154 ```
155
156 **Rules:**
157 - Always use positional parameters (`$1`, `$2`). Never interpolate.
158 - Projection structs derive `sqlx::FromRow` and are shaped for templates, not domain models.
159 - Queries in `queries.rs`, mutations in `mutations.rs`.
160 - Use `ON CONFLICT ... DO UPDATE` for upserts (user sync from MNW).
161
162 Migrations are in `migrations/` and auto-apply on boot via `sqlx::migrate!()`.
163
164 ## Shared Dependencies
165
166 MT uses three shared crates from `MNW/shared/`:
167
168 | Crate | Usage |
169 |-------|-------|
170 | `docengine` | Markdown rendering with `render_strict()`, @mention resolution, quote post-processing |
171 | `tagtree` | Tag name validation (`validate_with(&tag, &MT_TAG_CONFIG)`) |
172 | `s3-storage` | Image uploads via `S3Storage` wrapper |
173
174 **Deploy note:** `deploy.sh` syncs shared deps to `~/src/shared/` on Astra because Cargo.toml references `../shared/X`. If you add a new shared dep, update the rsync loop in `deploy.sh`.
175
176 ## CSRF Protection
177
178 Synchronizer token pattern, same as MNW server. CSRF middleware validates `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. Exempt paths: `/auth/`, `/api/health`, `/_test/`.
179
180 Client-side: HTMX sends the token automatically via a `htmx:configRequest` listener that reads from `<meta name="csrf-token">`.
181
182 ## Internal API (MNW → MT)
183
184 MNW server can call MT's internal API (e.g., to auto-create communities for new projects). These requests use HMAC-SHA256 authentication:
185
186 - `X-Internal-Timestamp` — Unix timestamp (must be within 60 seconds of server time)
187 - `X-Internal-Signature` — HMAC-SHA256 of `"timestamp\nbody"` using shared secret
188
189 The `InternalAuth` extractor validates both before allowing access.
190
191 ## Testing
192
193 Integration tests use the same pattern as MNW: each test creates and drops its own PostgreSQL database.
194
195 ```rust
196 let harness = TestHarness::new().await; // Creates mnw_test_<uuid> DB
197 let user_id = harness.login_as("alice").await; // Inserts user + sets session
198 // ... test routes via harness.client ...
199 // Database dropped on harness drop
200 ```
201
202 A `/_test/login` route (only available in tests) bypasses OAuth for session setup. Tests are organized by domain in `tests/workflows/`.
203
204 ## Deployment
205
206 MT builds natively on Astra (aarch64). Deploy from `multithreaded/`:
207
208 ```bash
209 ./deploy/deploy.sh # rsync source + shared deps, build on Astra, deploy binary + assets
210 ```
211
212 The deploy script:
213 1. Rsyncs source to `~/src/multithreaded/` on Astra
214 2. Rsyncs shared deps (docengine, tagtree, s3-storage) to `~/src/shared/`
215 3. Builds release binary on Astra
216 4. Copies binary + static + migrations to `/opt/multithreaded/`
217 5. Restarts the systemd service
218