Skip to main content

max / makenotwork

Restructure into monorepo Move server files to server/ subdirectory. Absorb multithreaded, pom, and mnw-cli (previously separate repos with .gitignore'd directories). Move shared libraries from ../Shared/ into shared/ subdirectory. Update all path dependencies accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-15 19:17 UTC
Commit: 5c12c14e097ad3a406f64f658b65b41c31251184
Parent: 1500bef
2026 files changed, +238199 insertions, -148016 deletions
D .env.example -35
@@ -1,35 +0,0 @@
1 - # Server Configuration
2 - HOST=127.0.0.1
3 - PORT=3000
4 -
5 - # Database (on macOS with Homebrew, use your username without password)
6 - DATABASE_URL=postgres://your_user@localhost:5432/makenotwork
7 -
8 - # Authentication
9 - JWT_SECRET=your-super-secret-jwt-key-change-in-production
10 -
11 - # Session
12 - SESSION_SECRET=your-session-secret-change-in-production
13 -
14 - # Optional: Stripe Connect (payments)
15 - # Get these from https://dashboard.stripe.com/apikeys
16 - # STRIPE_SECRET_KEY=sk_test_...
17 - # STRIPE_WEBHOOK_SECRET=whsec_...
18 - # HOST_URL=http://localhost:3000
19 -
20 - # Optional: S3 Storage (for file uploads)
21 - # S3_ENDPOINT=https://fsn1.your-objectstorage.com
22 - # S3_BUCKET=makenotwork-files
23 - # S3_REGION=fsn1
24 - # S3_ACCESS_KEY=
25 - # S3_SECRET_KEY=
26 -
27 - # Optional: CDN for free content downloads (Cloudflare-proxied)
28 - # CDN_BASE_URL=https://cdn.makenot.work
29 -
30 - # Optional: Email (Phase 8)
31 - # SMTP_HOST=
32 - # SMTP_PORT=587
33 - # SMTP_USER=
34 - # SMTP_PASS=
35 - # FROM_EMAIL=noreply@makenot.work
M .gitignore +5 -10
@@ -3,9 +3,9 @@
3 3 **/target
4 4
5 5 # Environment files (contain secrets)
6 - .env
7 - .env.local
8 - .env.*.local
6 + server/.env
7 + server/.env.local
8 + server/.env.*.local
9 9 **/env.production
10 10
11 11 # IDE
@@ -23,12 +23,7 @@ Thumbs.db
23 23 .sqlx/
24 24
25 25 # Generated template partial (build.rs output)
26 - templates/_head_assets.html
26 + server/templates/_head_assets.html
27 27
28 28 # Generated rustdoc output
29 - rustdoc-out/
30 -
31 - # Nested repos (separate git projects colocated in MNW/)
32 - /multithreaded/
33 - /pom/
34 - /mnw-cli/
29 + server/rustdoc-out/
D Cargo.lock -500
@@ -1,7556 +0,0 @@
1 - # This file is automatically @generated by Cargo.
2 - # It is not intended for manual editing.
3 - version = 4
4 -
5 - [[package]]
6 - name = "addr2line"
7 - version = "0.25.1"
8 - source = "registry+https://github.com/rust-lang/crates.io-index"
9 - checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
10 - dependencies = [
11 - "gimli",
12 - ]
13 -
14 - [[package]]
15 - name = "adler2"
16 - version = "2.0.1"
17 - source = "registry+https://github.com/rust-lang/crates.io-index"
18 - checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
19 -
20 - [[package]]
21 - name = "aes"
22 - version = "0.8.4"
23 - source = "registry+https://github.com/rust-lang/crates.io-index"
24 - checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
25 - dependencies = [
26 - "cfg-if",
27 - "cipher",
28 - "cpufeatures",
29 - ]
30 -
31 - [[package]]
32 - name = "aho-corasick"
33 - version = "1.1.4"
34 - source = "registry+https://github.com/rust-lang/crates.io-index"
35 - checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
36 - dependencies = [
37 - "log",
38 - "memchr",
39 - ]
40 -
41 - [[package]]
42 - name = "allocator-api2"
43 - version = "0.2.21"
44 - source = "registry+https://github.com/rust-lang/crates.io-index"
45 - checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
46 -
47 - [[package]]
48 - name = "ammonia"
49 - version = "4.1.2"
50 - source = "registry+https://github.com/rust-lang/crates.io-index"
51 - checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
52 - dependencies = [
53 - "cssparser",
54 - "html5ever",
55 - "maplit",
56 - "tendril",
57 - "url",
58 - ]
59 -
60 - [[package]]
61 - name = "android_system_properties"
62 - version = "0.1.5"
63 - source = "registry+https://github.com/rust-lang/crates.io-index"
64 - checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
65 - dependencies = [
66 - "libc",
67 - ]
68 -
69 - [[package]]
70 - name = "annotate-snippets"
71 - version = "0.12.13"
72 - source = "registry+https://github.com/rust-lang/crates.io-index"
73 - checksum = "74fc7650eedcb2fee505aad48491529e408f0e854c2d9f63eb86c1361b9b3f93"
74 - dependencies = [
75 - "anstyle",
76 - "memchr",
77 - "unicode-width",
78 - ]
79 -
80 - [[package]]
81 - name = "anstream"
82 - version = "1.0.0"
83 - source = "registry+https://github.com/rust-lang/crates.io-index"
84 - checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
85 - dependencies = [
86 - "anstyle",
87 - "anstyle-parse",
88 - "anstyle-query",
89 - "anstyle-wincon",
90 - "colorchoice",
91 - "is_terminal_polyfill",
92 - "utf8parse",
93 - ]
94 -
95 - [[package]]
96 - name = "anstyle"
97 - version = "1.0.14"
98 - source = "registry+https://github.com/rust-lang/crates.io-index"
99 - checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
100 -
101 - [[package]]
102 - name = "anstyle-parse"
103 - version = "1.0.0"
104 - source = "registry+https://github.com/rust-lang/crates.io-index"
105 - checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
106 - dependencies = [
107 - "utf8parse",
108 - ]
109 -
110 - [[package]]
111 - name = "anstyle-query"
112 - version = "1.1.5"
113 - source = "registry+https://github.com/rust-lang/crates.io-index"
114 - checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
115 - dependencies = [
116 - "windows-sys 0.61.2",
117 - ]
118 -
119 - [[package]]
120 - name = "anstyle-wincon"
121 - version = "3.0.11"
122 - source = "registry+https://github.com/rust-lang/crates.io-index"
123 - checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
124 - dependencies = [
125 - "anstyle",
126 - "once_cell_polyfill",
127 - "windows-sys 0.61.2",
128 - ]
129 -
130 - [[package]]
131 - name = "anyhow"
132 - version = "1.0.102"
133 - source = "registry+https://github.com/rust-lang/crates.io-index"
134 - checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
135 -
136 - [[package]]
137 - name = "arbitrary"
138 - version = "1.4.2"
139 - source = "registry+https://github.com/rust-lang/crates.io-index"
140 - checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
141 -
142 - [[package]]
143 - name = "argon2"
144 - version = "0.5.3"
145 - source = "registry+https://github.com/rust-lang/crates.io-index"
146 - checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
147 - dependencies = [
148 - "base64ct",
149 - "blake2",
150 - "cpufeatures",
151 - "password-hash",
152 - ]
153 -
154 - [[package]]
155 - name = "ascii_tree"
156 - version = "0.1.1"
157 - source = "registry+https://github.com/rust-lang/crates.io-index"
158 - checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643"
159 -
160 - [[package]]
161 - name = "askama"
162 - version = "0.13.1"
163 - source = "registry+https://github.com/rust-lang/crates.io-index"
164 - checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
165 - dependencies = [
166 - "askama_derive",
167 - "itoa",
168 - "percent-encoding",
169 - "serde",
170 - "serde_json",
171 - ]
172 -
173 - [[package]]
174 - name = "askama_derive"
175 - version = "0.13.1"
176 - source = "registry+https://github.com/rust-lang/crates.io-index"
177 - checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
178 - dependencies = [
179 - "askama_parser",
180 - "basic-toml",
181 - "memchr",
182 - "proc-macro2",
183 - "quote",
184 - "rustc-hash 2.1.1",
185 - "serde",
186 - "serde_derive",
187 - "syn 2.0.117",
188 - ]
189 -
190 - [[package]]
191 - name = "askama_parser"
192 - version = "0.13.0"
193 - source = "registry+https://github.com/rust-lang/crates.io-index"
194 - checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
195 - dependencies = [
196 - "memchr",
197 - "serde",
198 - "serde_derive",
199 - "winnow",
200 - ]
201 -
202 - [[package]]
203 - name = "asn1-rs"
204 - version = "0.6.2"
205 - source = "registry+https://github.com/rust-lang/crates.io-index"
206 - checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
207 - dependencies = [
208 - "asn1-rs-derive 0.5.1",
209 - "asn1-rs-impl",
210 - "displaydoc",
211 - "nom 7.1.3",
212 - "num-traits",
213 - "rusticata-macros",
214 - "thiserror 1.0.69",
215 - "time",
216 - ]
217 -
218 - [[package]]
219 - name = "asn1-rs"
220 - version = "0.7.1"
221 - source = "registry+https://github.com/rust-lang/crates.io-index"
222 - checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
223 - dependencies = [
224 - "asn1-rs-derive 0.6.0",
225 - "asn1-rs-impl",
226 - "displaydoc",
227 - "nom 7.1.3",
228 - "num-traits",
229 - "rusticata-macros",
230 - "thiserror 2.0.18",
231 - "time",
232 - ]
233 -
234 - [[package]]
235 - name = "asn1-rs-derive"
236 - version = "0.5.1"
237 - source = "registry+https://github.com/rust-lang/crates.io-index"
238 - checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
239 - dependencies = [
240 - "proc-macro2",
241 - "quote",
242 - "syn 2.0.117",
243 - "synstructure",
244 - ]
245 -
246 - [[package]]
247 - name = "asn1-rs-derive"
248 - version = "0.6.0"
249 - source = "registry+https://github.com/rust-lang/crates.io-index"
250 - checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
251 - dependencies = [
252 - "proc-macro2",
253 - "quote",
254 - "syn 2.0.117",
255 - "synstructure",
256 - ]
257 -
258 - [[package]]
259 - name = "asn1-rs-impl"
260 - version = "0.2.0"
261 - source = "registry+https://github.com/rust-lang/crates.io-index"
262 - checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
263 - dependencies = [
264 - "proc-macro2",
265 - "quote",
266 - "syn 2.0.117",
267 - ]
268 -
269 - [[package]]
270 - name = "async-channel"
271 - version = "1.9.0"
272 - source = "registry+https://github.com/rust-lang/crates.io-index"
273 - checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
274 - dependencies = [
275 - "concurrent-queue",
276 - "event-listener 2.5.3",
277 - "futures-core",
278 - ]
279 -
280 - [[package]]
281 - name = "async-stream"
282 - version = "0.3.6"
283 - source = "registry+https://github.com/rust-lang/crates.io-index"
284 - checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
285 - dependencies = [
286 - "async-stream-impl",
287 - "futures-core",
288 - "pin-project-lite",
289 - ]
290 -
291 - [[package]]
292 - name = "async-stream-impl"
293 - version = "0.3.6"
294 - source = "registry+https://github.com/rust-lang/crates.io-index"
295 - checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
296 - dependencies = [
297 - "proc-macro2",
298 - "quote",
299 - "syn 2.0.117",
300 - ]
301 -
302 - [[package]]
303 - name = "async-stripe"
304 - version = "0.37.3"
305 - source = "registry+https://github.com/rust-lang/crates.io-index"
306 - checksum = "e2f14b5943a52cf051bbbbb68538e93a69d1e291934174121e769f4b181113f5"
307 - dependencies = [
308 - "chrono",
309 - "futures-util",
310 - "hex",
311 - "hmac",
312 - "http-types",
313 - "hyper 0.14.32",
314 - "hyper-tls 0.5.0",
315 - "serde",
316 - "serde_json",
317 - "serde_path_to_error",
318 - "serde_qs 0.10.1",
319 - "sha2",
320 - "smart-default",
321 - "smol_str",
322 - "thiserror 1.0.69",
323 - "tokio",
324 - "uuid 0.8.2",
325 - ]
326 -
327 - [[package]]
328 - name = "async-trait"
329 - version = "0.1.89"
330 - source = "registry+https://github.com/rust-lang/crates.io-index"
331 - checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
332 - dependencies = [
333 - "proc-macro2",
334 - "quote",
335 - "syn 2.0.117",
336 - ]
337 -
338 - [[package]]
339 - name = "atoi"
340 - version = "2.0.0"
341 - source = "registry+https://github.com/rust-lang/crates.io-index"
342 - checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
343 - dependencies = [
344 - "num-traits",
345 - ]
346 -
347 - [[package]]
348 - name = "atomic-waker"
349 - version = "1.1.2"
350 - source = "registry+https://github.com/rust-lang/crates.io-index"
351 - checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
352 -
353 - [[package]]
354 - name = "autocfg"
355 - version = "1.5.0"
356 - source = "registry+https://github.com/rust-lang/crates.io-index"
357 - checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
358 -
359 - [[package]]
360 - name = "aws-config"
361 - version = "1.8.15"
362 - source = "registry+https://github.com/rust-lang/crates.io-index"
363 - checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc"
364 - dependencies = [
365 - "aws-credential-types",
366 - "aws-runtime",
367 - "aws-sdk-sso",
368 - "aws-sdk-ssooidc",
369 - "aws-sdk-sts",
370 - "aws-smithy-async",
371 - "aws-smithy-http 0.63.6",
372 - "aws-smithy-json 0.62.5",
373 - "aws-smithy-runtime",
374 - "aws-smithy-runtime-api",
375 - "aws-smithy-types",
376 - "aws-types",
377 - "bytes",
378 - "fastrand 2.3.0",
379 - "hex",
380 - "http 1.4.0",
381 - "sha1",
382 - "time",
383 - "tokio",
384 - "tracing",
385 - "url",
386 - "zeroize",
387 - ]
388 -
389 - [[package]]
390 - name = "aws-credential-types"
391 - version = "1.2.14"
392 - source = "registry+https://github.com/rust-lang/crates.io-index"
393 - checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7"
394 - dependencies = [
395 - "aws-smithy-async",
396 - "aws-smithy-runtime-api",
397 - "aws-smithy-types",
398 - "zeroize",
399 - ]
400 -
401 - [[package]]
402 - name = "aws-lc-rs"
403 - version = "1.16.2"
404 - source = "registry+https://github.com/rust-lang/crates.io-index"
405 - checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
406 - dependencies = [
407 - "aws-lc-sys",
408 - "zeroize",
409 - ]
410 -
411 - [[package]]
412 - name = "aws-lc-sys"
413 - version = "0.39.0"
414 - source = "registry+https://github.com/rust-lang/crates.io-index"
415 - checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
416 - dependencies = [
417 - "cc",
418 - "cmake",
419 - "dunce",
420 - "fs_extra",
421 - ]
422 -
423 - [[package]]
424 - name = "aws-runtime"
425 - version = "1.7.2"
426 - source = "registry+https://github.com/rust-lang/crates.io-index"
427 - checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17"
428 - dependencies = [
429 - "aws-credential-types",
430 - "aws-sigv4",
431 - "aws-smithy-async",
432 - "aws-smithy-eventstream",
433 - "aws-smithy-http 0.63.6",
434 - "aws-smithy-runtime",
435 - "aws-smithy-runtime-api",
436 - "aws-smithy-types",
437 - "aws-types",
438 - "bytes",
439 - "bytes-utils",
440 - "fastrand 2.3.0",
441 - "http 0.2.12",
442 - "http 1.4.0",
443 - "http-body 0.4.6",
444 - "http-body 1.0.1",
445 - "percent-encoding",
446 - "pin-project-lite",
447 - "tracing",
448 - "uuid 1.22.0",
449 - ]
450 -
451 - [[package]]
452 - name = "aws-sdk-s3"
453 - version = "1.119.0"
454 - source = "registry+https://github.com/rust-lang/crates.io-index"
455 - checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c"
456 - dependencies = [
457 - "aws-credential-types",
458 - "aws-runtime",
459 - "aws-sigv4",
460 - "aws-smithy-async",
461 - "aws-smithy-checksums",
462 - "aws-smithy-eventstream",
463 - "aws-smithy-http 0.62.6",
464 - "aws-smithy-json 0.61.9",
465 - "aws-smithy-runtime",
466 - "aws-smithy-runtime-api",
467 - "aws-smithy-types",
468 - "aws-smithy-xml",
469 - "aws-types",
470 - "bytes",
471 - "fastrand 2.3.0",
472 - "hex",
473 - "hmac",
474 - "http 0.2.12",
475 - "http 1.4.0",
476 - "http-body 0.4.6",
477 - "lru",
478 - "percent-encoding",
479 - "regex-lite",
480 - "sha2",
481 - "tracing",
482 - "url",
483 - ]
484 -
485 - [[package]]
486 - name = "aws-sdk-sso"
487 - version = "1.97.0"
488 - source = "registry+https://github.com/rust-lang/crates.io-index"
489 - checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567"
490 - dependencies = [
491 - "aws-credential-types",
492 - "aws-runtime",
493 - "aws-smithy-async",
494 - "aws-smithy-http 0.63.6",
495 - "aws-smithy-json 0.62.5",
496 - "aws-smithy-observability",
497 - "aws-smithy-runtime",
498 - "aws-smithy-runtime-api",
499 - "aws-smithy-types",
500 - "aws-types",
Lines truncated
D Cargo.toml -114
@@ -1,114 +0,0 @@
1 - [package]
2 - name = "makenotwork"
3 - version = "0.3.24"
4 - edition = "2024"
5 - license-file = "LICENSE"
6 -
7 - [dependencies]
8 - # Async trait (for StorageBackend trait object)
9 - async-trait = "0.1"
10 -
11 - # Web framework
12 - axum = { version = "0.8.8", features = ["macros"] }
13 - axum-extra = { version = "0.10.3", features = ["cookie", "form", "typed-header"] }
14 - serde = { version = "1.0.228", features = ["derive"] }
15 - serde_json = "1.0.149"
16 - tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "net", "signal"] }
17 - tokio-stream = { version = "0.1", features = ["sync"] }
18 - tower = "0.5.3"
19 - tower-http = { version = "0.6.8", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] }
20 - tracing = "0.1.44"
21 - tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
22 -
23 - # Templates
24 - askama = "0.13.1"
25 -
26 - # Environment & Configuration
27 - dotenvy = "0.15.7"
28 -
29 - # Database
30 - sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] }
31 - uuid = { version = "1.20.0", features = ["v4", "serde"] }
32 - chrono = { version = "0.4.43", features = ["serde"] }
33 -
34 - # Authentication
35 - argon2 = "0.5.3"
36 - tower-sessions = { version = "0.14.0", features = ["axum-core"] }
37 - tower-sessions-sqlx-store = { version = "0.15.0", features = ["postgres"] }
38 -
39 - # Concurrent hash map (session touch cache)
40 - dashmap = "6"
41 -
42 - # Rate Limiting
43 - tower_governor = "0.6.0"
44 - governor = "0.8.1"
45 -
46 - # JWT (SyncKit)
47 - jsonwebtoken = "9.3.1"
48 -
49 - # TOTP / 2FA
50 - totp-rs = { version = "5.7", features = ["qr"] }
51 -
52 - # WebAuthn / Passkeys
53 - webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "conditional-ui"] }
54 - webauthn-rs-proto = "0.5"
55 -
56 - # OpenSSL (transitive dep from git2, webauthn-rs — vendored for cross-compilation)
57 - openssl = { version = "0.10", features = ["vendored"] }
58 -
59 - # Security
60 - rand = "0.8.5"
61 - hmac = "0.12.1"
62 - sha1 = "0.10.6"
63 - sha2 = "0.10.9"
64 - hex = "0.4.3"
65 - base64 = "0.22.1"
66 -
67 - # File scanning
68 - infer = "0.19"
69 - goblin = "0.10"
70 - zip = "8.2"
71 - yara-x = "1.13"
72 -
73 - # CSV parsing (import system)
74 - csv = "1.3"
75 -
76 - # CLI
77 - clap = { version = "4", features = ["derive"] }
78 -
79 - # Error handling
80 - thiserror = "2.0.18"
81 - anyhow = "1.0.101"
82 -
83 - # Markdown rendering + documentation engine
84 - docengine = { path = "../Shared/docengine", features = ["doc-loader", "directives", "frontmatter", "media-urls"] }
85 -
86 - # Tag standard
87 - tagtree = { path = "../Shared/tagtree" }
88 -
89 - # Git source browser
90 - git2 = { version = "0.20", features = ["vendored-libgit2"] }
91 - syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "html", "regex-fancy"] }
92 - regex = "1"
93 - semver = "1"
94 -
95 - # S3 Storage
96 - s3-storage = { path = "../Shared/s3-storage" }
97 -
98 - # Stripe Payments
99 - async-stripe = { version = "0.37.3", features = ["runtime-tokio-hyper", "checkout", "connect", "billing"] }
100 - reqwest = { version = "0.12", features = ["json", "cookies"] }
101 - urlencoding = "2.1.3"
102 -
103 - # URL parsing
104 - url = "2.5.8"
105 -
106 - [[bin]]
107 - name = "mnw-admin"
108 - path = "src/bin/mnw-admin.rs"
109 -
110 - [dev-dependencies]
111 - tower = { version = "0.5.3", features = ["util"] }
112 - http-body-util = "0.1"
113 - webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] }
114 - tempfile = "3"
M README.md +31 -37
@@ -2,17 +2,37 @@
2 2
3 3 Creator platform with 0% platform fee. Only Stripe's ~3% processing fee applies. Live at [makenot.work](https://makenot.work).
4 4
5 + ## Monorepo Structure
6 +
7 + ```
8 + MNW/
9 + server/ MNW server (Rust/Axum, PostgreSQL, HTMX)
10 + multithreaded/ Forum software (Rust/Axum, MNW OAuth integration)
11 + pom/ Production operations monitor (health checks, alerts)
12 + mnw-cli/ CLI tool for MNW platform
13 + shared/ Shared libraries
14 + docengine/ Markdown rendering + documentation engine
15 + tagtree/ Hierarchical tag standard
16 + synckit-client/ SyncKit cloud sync client SDK
17 + theme-common/ Theme loading + parsing (TOML themes)
18 + s3-storage/ S3-compatible storage abstraction
19 + themes/ TOML theme definitions (24 themes)
20 + tauri-updater-ui/ OTA update UI components
21 + ```
22 +
23 + ## MNW Server
24 +
5 25 Built with Rust (2024 edition), Axum, PostgreSQL, Askama templates, and HTMX.
6 26
7 - ## Prerequisites
27 + ### Prerequisites
8 28
9 29 - **Rust** (stable toolchain, 1.85+, 2024 edition)
10 30 - **PostgreSQL** (16+)
11 - - **Environment variables** via `.env` file: database URL, Stripe keys, Postmark token, S3 credentials, Sentry DSN, session secret, JWT secret. See `.env.example` or the systemd unit for the full list.
31 + - **Environment variables** via `.env` file: database URL, Stripe keys, Postmark token, S3 credentials, Sentry DSN, session secret, JWT secret. See `server/.env.example` for the full list.
12 32
13 - ## Build and Run
33 + ### Build and Run
14 34
15 - All commands run from the `MNW/` directory:
35 + All commands run from the `MNW/server/` directory:
16 36
17 37 ```sh
18 38 # Development
@@ -28,36 +48,9 @@ TEST_DATABASE_URL="postgres://user:pass@host:5432/postgres" cargo test --test in
28 48 cargo run --bin mnw-admin
29 49 ```
30 50
31 - Production deployment uses `cargo zigbuild` for cross-compilation to x86_64 Linux. See `deploy/deploy.sh`.
51 + Production deployment uses `cargo zigbuild` for cross-compilation to x86_64 Linux. See `server/deploy/deploy.sh`.
32 52
33 - ## Project Structure
34 -
35 - ```
36 - MNW/
37 - src/
38 - main.rs Entry point
39 - lib.rs Library root
40 - config.rs Configuration
41 - auth.rs Session authentication + 2FA (TOTP, WebAuthn)
42 - synckit_auth.rs SyncKit JWT authentication
43 - db/ PostgreSQL queries (sqlx, compile-time checked)
44 - routes/ Axum route handlers
45 - pages/ HTML page routes (public/, dashboard/)
46 - api/ JSON API endpoints
47 - stripe/ Stripe webhooks + Connect
48 - synckit.rs SyncKit cloud sync endpoints
49 - storage.rs File upload/download
50 - templates/ Askama HTML templates
51 - scanning/ File scanning (ClamAV, YARA, hash lookup)
52 - types/ Shared types
53 - email/ Transactional email (Postmark)
54 - migrations/ SQLx migrations (auto-applied on boot)
55 - static/ CSS, JS, fonts, images
56 - tests/ Integration tests
57 - deploy/ Deployment scripts, systemd unit, Caddyfile
58 - ```
59 -
60 - ## Key Integrations
53 + ### Key Integrations
61 54
62 55 - **Stripe Connect** -- creator payouts, subscriptions, checkout
63 56 - **Postmark** -- transactional email (verification, password reset, purchase receipts)
@@ -66,11 +59,13 @@ MNW/
66 59 - **Sentry** -- error tracking
67 60 - **git2** -- built-in git source browser
68 61
69 - ## Deployment
62 + ### Deployment
70 63
71 64 Runs on a Hetzner VPS with systemd and Caddy (reverse proxy + TLS). No Docker.
72 65
73 66 ```sh
67 + cd server/
68 +
74 69 # Full deploy (cross-compile + upload + restart)
75 70 ./deploy/deploy.sh
76 71
@@ -81,13 +76,12 @@ Runs on a Hetzner VPS with systemd and Caddy (reverse proxy + TLS). No Docker.
81 76 ./deploy/deploy.sh --config
82 77 ```
83 78
84 - SQLx migrations run automatically on application startup.
85 -
86 - ## Testing
79 + ### Testing
87 80
88 81 Each integration test creates and drops its own PostgreSQL database, so tests run in full isolation. Unit tests have no external dependencies.
89 82
90 83 ```sh
84 + cd server/
91 85 cargo test # Unit tests
92 86 cargo test --test integration # Integration tests (needs TEST_DATABASE_URL)
93 87 ```
D build.rs -63
@@ -1,63 +0,0 @@
1 - use std::collections::hash_map::DefaultHasher;
2 - use std::hash::{Hash, Hasher};
3 - use std::process::Command;
4 - use std::{fs, path::Path};
5 -
6 - fn main() {
7 - // Set GIT_HASH env var for compile-time inclusion via option_env!()
8 - let hash = Command::new("git")
9 - .args(["rev-parse", "--short", "HEAD"])
10 - .output()
11 - .ok()
12 - .filter(|o| o.status.success())
13 - .and_then(|o| String::from_utf8(o.stdout).ok())
14 - .map(|s| s.trim().to_string())
15 - .unwrap_or_default();
16 -
17 - println!("cargo::rustc-env=GIT_HASH={}", hash);
18 - // Only re-run when HEAD changes
19 - println!("cargo::rerun-if-changed=.git/HEAD");
20 -
21 - // --- Static asset fingerprinting ---
22 - // Hash the content of key static files to produce a version suffix.
23 - // When any watched file changes, URLs in templates get a new ?v= param,
24 - // busting browser caches automatically.
25 - let static_files = [
26 - "static/style.css",
27 - "static/htmx.min.js",
28 - "static/upload.js",
29 - "static/passkey.js",
30 - "static/insertions.js",
31 - ];
32 -
33 - let mut hasher = DefaultHasher::new();
34 - for path in &static_files {
35 - println!("cargo::rerun-if-changed={}", path);
36 - if let Ok(content) = fs::read(path) {
37 - content.hash(&mut hasher);
38 - }
39 - }
40 - let static_hash = format!("{:016x}", hasher.finish());
41 - let version = &static_hash[..8];
42 -
43 - // Generate a template partial with versioned asset URLs.
44 - // base.html includes this via {% include "_head_assets.html" %}
45 - let partial = format!(
46 - r#" <link rel="preload" href="/static/fonts/Lato-Regular.woff2" as="font" type="font/woff2" crossorigin>
47 - <link rel="preload" href="/static/fonts/ysrf.woff2" as="font" type="font/woff2" crossorigin>
48 - <link rel="stylesheet" href="/static/style.css?v={v}">
49 - <link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
50 - <script src="/static/htmx.min.js"></script>
51 - <script src="/static/upload.js?v={v}"></script>"#,
52 - v = version,
53 - );
54 -
55 - let out_path = Path::new("templates/_head_assets.html");
56 - // Only write if content changed (avoids unnecessary recompilation)
57 - let needs_write = fs::read_to_string(out_path)
58 - .map(|existing| existing != partial)
59 - .unwrap_or(true);
60 - if needs_write {
61 - fs::write(out_path, &partial).expect("failed to write _head_assets.html");
62 - }
63 - }
@@ -1,209 +0,0 @@
1 - # Makenotwork Caddy Configuration
2 - # Place in /etc/caddy/Caddyfile on the server
3 - #
4 - # TLS: Cloudflare Origin CA cert (wildcard *.makenot.work + makenot.work)
5 - # All HTTPS traffic routed through Cloudflare proxy (origin IP hidden).
6 - # Authenticated Origin Pulls: only Cloudflare can reach the origin.
7 - # git.makenot.work redirects browser visits to the web UI.
8 - # SSH clone uses ssh.makenot.work (proxy OFF in Cloudflare).
9 - #
10 - # Custom domains: on-demand TLS via Let's Encrypt (ACME HTTP-01).
11 - # The ask endpoint validates that the domain is verified before issuing a cert.
12 - # makenot.work subdomains remain protected by Cloudflare mTLS even with ports open.
13 -
14 - {
15 - on_demand_tls {
16 - ask http://localhost:3000/api/domains/caddy-ask
17 - }
18 - }
19 -
20 - # Shared TLS config: Origin CA cert + Authenticated Origin Pulls (mTLS)
21 - (cloudflare_tls) {
22 - tls /etc/caddy/cloudflare-origin.pem /etc/caddy/cloudflare-origin-key.pem {
23 - client_auth {
24 - mode require_and_verify
25 - trusted_ca_cert_file /etc/caddy/cloudflare-authenticated-origin-pull-ca.pem
26 - }
27 - }
28 - }
29 -
30 - makenot.work {
31 - import cloudflare_tls
32 -
33 - # Block internal API from external access (CLI uses localhost directly)
34 - @internal path /api/internal/*
35 - respond @internal 404
36 -
37 - # Reverse proxy to application (includes /docs routes)
38 - reverse_proxy localhost:3000
39 -
40 - # Security headers
41 - header {
42 - X-Frame-Options "SAMEORIGIN"
43 - X-Content-Type-Options "nosniff"
44 - X-XSS-Protection "1; mode=block"
45 - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
46 - Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(self)"
47 - Referrer-Policy "strict-origin-when-cross-origin"
48 - Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline' https://unpkg.com https://js.stripe.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https://fsn1.your-objectstorage.com https://cdn.makenot.work; connect-src 'self' https://api.stripe.com https://fsn1.your-objectstorage.com https://cdn.makenot.work; media-src 'self' https://fsn1.your-objectstorage.com https://cdn.makenot.work; frame-src https://js.stripe.com; base-uri 'self'; form-action 'self'"
49 - }
50 -
51 - # Static error pages when app is down
52 - handle_errors {
53 - @404 expression {err.status_code} == 404
54 - handle @404 {
55 - root * /opt/makenotwork/error-pages
56 - rewrite * /404.html
57 - file_server
58 - }
59 - @500 expression {err.status_code} == 500
60 - handle @500 {
61 - root * /opt/makenotwork/error-pages
62 - rewrite * /500.html
63 - file_server
64 - }
65 - handle {
66 - root * /opt/makenotwork/error-pages
67 - rewrite * /502.html
68 - file_server
69 - }
70 - }
71 -
72 - encode gzip zstd
73 -
74 - log {
75 - output file /var/log/caddy/makenotwork.log
76 - format json
77 - }
78 - }
79 -
80 - # Multithreaded forum
81 - forums.makenot.work {
82 - import cloudflare_tls
83 -
84 - reverse_proxy localhost:3400
85 -
86 - header {
87 - X-Frame-Options "SAMEORIGIN"
88 - X-Content-Type-Options "nosniff"
89 - X-XSS-Protection "1; mode=block"
90 - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
91 - Permissions-Policy "camera=(), microphone=(), geolocation=()"
92 - Referrer-Policy "strict-origin-when-cross-origin"
93 - Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self' https://makenot.work"
94 - }
95 -
96 - encode gzip zstd
97 -
98 - log {
99 - output file /var/log/caddy/forums.log
100 - format json
101 - }
102 - }
103 -
104 - # CDN for free content downloads — reverse-proxies to Hetzner Object Storage.
105 - # Cloudflare caches responses at the edge (free egress). Origin only hit on cache miss.
106 - # Requires: S3 bucket policy allowing public s3:GetObject, Cloudflare DNS A record (proxy ON).
107 - cdn.makenot.work {
108 - import cloudflare_tls
109 -
110 - # Only allow GET (downloads). Block mutations.
111 - @not_get not method GET HEAD
112 - respond @not_get 405
113 -
114 - # Prepend bucket name to URI path and proxy to Hetzner Object Storage.
115 - # Replace BUCKET_NAME with the actual S3 bucket name.
116 - rewrite * /BUCKET_NAME{uri}
117 - reverse_proxy https://fsn1.your-objectstorage.com {
118 - header_up Host fsn1.your-objectstorage.com
119 - }
120 -
121 - header {
122 - X-Content-Type-Options "nosniff"
123 - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
124 - Access-Control-Allow-Origin "https://makenot.work"
125 - Access-Control-Allow-Methods "GET, HEAD"
126 - # Cache-Control is set on the S3 objects themselves (immutable).
127 - # Cloudflare respects the origin's Cache-Control header.
128 - }
129 -
130 - log {
131 - output file /var/log/caddy/cdn.log
132 - format json
133 - }
134 - }
135 -
136 - # maxj.phd TLS config: separate Origin CA cert + Authenticated Origin Pulls (mTLS)
137 - (maxjphd_tls) {
138 - tls /etc/caddy/maxj-phd-origin.pem /etc/caddy/maxj-phd-origin-key.pem {
139 - client_auth {
140 - mode require_and_verify
141 - trusted_ca_cert_file /etc/caddy/cloudflare-authenticated-origin-pull-ca.pem
142 - }
143 - }
144 - }
145 -
146 - # Static file downloads (audiofiles binaries, etc.)
147 - dl.maxj.phd {
148 - import maxjphd_tls
149 -
150 - root * /opt/downloads
151 - file_server browse
152 -
153 - header {
154 - X-Content-Type-Options "nosniff"
155 - Strict-Transport-Security "max-age=31536000; includeSubDomains"
156 - }
157 -
158 - encode gzip zstd
159 -
160 - log {
161 - output file /var/log/caddy/dl-maxjphd.log
162 - format json
163 - }
164 - }
165 -
166 - # Redirect www to canonical domain
167 - # Note: makenotwork.com and www.makenotwork.com redirects are handled by
168 - # Cloudflare Redirect Rules (edge-level, no origin hit needed).
169 - # Those domains are not covered by the *.makenot.work Origin CA cert.
170 - # Redirect git subdomain browser visits to web UI
171 - git.makenot.work {
172 - import cloudflare_tls
173 - redir https://makenot.work/git permanent
174 - }
175 -
176 - www.makenot.work {
177 - import cloudflare_tls
178 - redir https://makenot.work{uri} permanent
179 - }
180 -
181 - # Custom domains — on-demand TLS via Let's Encrypt.
182 - # Caddy calls /api/domains/caddy-ask before issuing a cert for any domain.
183 - # makenot.work subdomains are unaffected (matched by explicit blocks above
184 - # which use Cloudflare Origin CA + mTLS).
185 - :443 {
186 - tls {
187 - on_demand
188 - }
189 -
190 - reverse_proxy localhost:3000
191 -
192 - header {
193 - X-Content-Type-Options "nosniff"
194 - Strict-Transport-Security "max-age=31536000; includeSubDomains"
195 - Referrer-Policy "strict-origin-when-cross-origin"
196 - }
197 -
198 - encode gzip zstd
199 -
200 - log {
201 - output file /var/log/caddy/custom-domains.log
202 - format json
203 - }
204 - }
205 -
206 - # HTTP catch-all — redirect to HTTPS (also needed for ACME HTTP-01 challenges)
207 - :80 {
208 - redir https://{host}{uri} permanent
209 - }
@@ -1,156 +0,0 @@
1 - # MNW Deployment
2 -
3 - Scripts and configuration for deploying MNW to production.
4 -
5 - ## Quick Reference
6 -
7 - ```sh
8 - ./deploy/deploy.sh # Full: build + config + binary + restart
9 - ./deploy/deploy.sh --quick # Build + binary + restart (skip config)
10 - ./deploy/deploy.sh --config # Config files only (Caddyfile, systemd, static, docs)
11 - ```
12 -
13 - Run all commands from the `MNW/` directory.
14 -
15 - ## Prerequisites (one-time)
16 -
17 - ```sh
18 - brew install zig
19 - cargo install cargo-zigbuild
20 - rustup target add x86_64-unknown-linux-gnu
21 - ```
22 -
23 - ## What Each Mode Does
24 -
25 - ### Full deploy (default)
26 -
27 - 1. Cross-compiles the binary with `cargo zigbuild --release --target x86_64-unknown-linux-gnu`
28 - 2. Uploads config files (Caddyfile, systemd unit, error pages, security configs)
29 - 3. Minifies CSS via `clean-css-cli`, uploads static assets via rsync
30 - 4. Uploads public site-docs and generated rustdoc
31 - 5. Sends a 30-second restart warning to connected users via internal API
32 - 6. Stops the service, uploads the binary (+ `mnw-admin` if present), restarts
33 - 7. Verifies the app responds on `http://127.0.0.1:3000`
34 -
35 - ### Quick deploy (`--quick`)
36 -
37 - Skips config/static/docs upload. Builds, warns users, uploads binary, restarts.
38 -
39 - ### Config deploy (`--config`)
40 -
41 - Uploads Caddyfile, systemd unit, error pages, security configs, minified CSS, static assets, site-docs, and rustdoc. Reloads systemd and restarts Caddy. Does not touch the application binary.
42 -
43 - ## Production Server
44 -
45 - - **Host:** Hetzner VPS (CCX13 x86, US-West)
46 - - **Public IP:** `5.78.144.244`
47 - - **Tailscale IP:** `100.120.174.96` (hostname: `alpha-west-1`)
48 - - **SSH:** `root@100.120.174.96` (via Tailscale only)
49 - - **OS:** Ubuntu, x86_64
50 -
51 - ### Filesystem layout
52 -
53 - ```
54 - /opt/makenotwork/
55 - makenotwork Application binary
56 - mnw-admin Admin CLI binary
57 - .env Environment variables (secrets)
58 - static/ CSS, JS, fonts, images
59 - error-pages/ Custom 404/500/502 pages
60 - backup-db.sh Database backup script
61 - docs/public/ Site documentation (rendered by DocEngine)
62 - rustdoc/ Generated API reference
63 - deploy/ Security config copies
64 -
65 - /opt/git/ Bare git repos (source browser)
66 - makenotwork.git/
67 - synckit-client.git/
68 - ...
69 -
70 - /etc/caddy/Caddyfile Reverse proxy config
71 - /etc/caddy/cloudflare-origin.pem Cloudflare Origin CA cert
72 - /etc/caddy/cloudflare-origin-key.pem Origin CA private key
73 - /etc/caddy/cloudflare-authenticated-origin-pull-ca.pem Cloudflare AOP CA
74 - /etc/caddy/maxj-phd-origin.pem maxj.phd Origin CA cert
75 - /etc/caddy/maxj-phd-origin-key.pem maxj.phd Origin CA key
76 - /etc/systemd/system/makenotwork.service systemd unit
77 - ```
78 -
79 - ### Services
80 -
81 - | Service | Role | Port |
82 - |---------|------|------|
83 - | `makenotwork` | Application (systemd, runs as `makenotwork` user) | 3000 |
84 - | `caddy` | Reverse proxy, TLS termination | 443 |
85 - | `postgresql` | Database (`makenotwork` db + user) | 5432 |
86 -
87 - ### Networking
88 -
89 - - **Cloudflare** proxies all HTTP/HTTPS traffic (origin IP hidden)
90 - - **SSL:** Full (Strict) mode with Cloudflare Origin CA (15yr wildcard for `*.makenot.work` and `*.maxj.phd`)
91 - - **Authenticated Origin Pulls** enabled (mTLS between Cloudflare and origin)
92 - - **SSH:** `ssh.makenot.work` DNS A record points directly to public IP (proxy OFF) for git push/pull
93 - - **Firewall:** ufw + fail2ban, sshd hardened
94 -
95 - ## Scripts Reference
96 -
97 - | Script | Purpose |
98 - |--------|---------|
99 - | `deploy.sh` | Main deployment script (build, upload, restart) |
100 - | `backup-db.sh` | PostgreSQL backup (pg_dump, uploaded to server) |
101 - | `generate-rustdoc.sh` | Generate rustdoc for library crates |
102 - | `ota-publish.sh` | Publish OTA release (auth, create release, presigned upload, verify) |
103 - | `run-ci.sh` | CI runner (check, test, clippy, audit) -- runs on astra |
104 - | `setup-firewall.sh` | Configure ufw rules |
105 - | `setup-git-ssh.sh` | Configure git SSH access |
106 - | `setup-ssh-keys.sh` | Deploy SSH authorized keys |
107 -
108 - ## Configuration Files
109 -
110 - | File | Deployed to | Purpose |
111 - |------|-------------|---------|
112 - | `Caddyfile` | `/etc/caddy/Caddyfile` | Reverse proxy rules for all domains |
113 - | `makenotwork.service` | `/etc/systemd/system/` | systemd unit (EnvironmentFile, restart policy) |
114 - | `env.production` | Reference for `.env` format | **Not deployed** -- `.env` is edited on server |
115 - | `fail2ban-sshd.conf` | `/opt/makenotwork/deploy/` | fail2ban jail config |
116 - | `sshd-git.conf` | `/opt/makenotwork/deploy/` | SSH config for git user |
117 - | `error-pages/*.html` | `/opt/makenotwork/error-pages/` | Custom Caddy error pages |
118 -
119 - ## Environment Variables
120 -
121 - All secrets live in `/opt/makenotwork/.env` (loaded by systemd `EnvironmentFile`). See `env.production` for the template. Key variables:
122 -
123 - - `DATABASE_URL` -- PostgreSQL connection string
124 - - `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_CLIENT_ID` -- Stripe Connect
125 - - `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET` -- Hetzner Object Storage
126 - - `POSTMARK_SERVER_TOKEN` -- transactional email
127 - - `JWT_SECRET`, `SESSION_SECRET` -- authentication
128 - - `SYNCKIT_JWT_SECRET` -- SyncKit token signing
129 - - `HOST_URL` -- public base URL (`https://makenot.work`)
130 -
131 - ## Astra (dev/test server)
132 -
133 - - **Tailscale IP:** `100.106.221.39`
134 - - **OS:** Pop!_OS 24.04 LTS, aarch64 (96 cores, 125GB RAM)
135 - - **PostgreSQL 16** with tuned settings
136 - - **Used for:** CI, integration tests, staging
137 -
138 - ### Running tests on astra
139 -
140 - ```sh
141 - ssh 100.106.221.39
142 - /home/max/staging/run-tests.sh # All tests
143 - /home/max/staging/run-tests.sh auth:: # Filtered
144 - ```
145 -
146 - Use `--test-threads=8` (or the `RUST_TEST_THREADS=8` env var set in `.bashrc`) to avoid overwhelming PostgreSQL with 96 concurrent `CREATE DATABASE` calls.
147 -
148 - ## Versioning
149 -
150 - - Version is set in `Cargo.toml` and compiled into the binary via `env!("CARGO_PKG_VERSION")`
151 - - **Always ask before bumping** -- never auto-increment
152 - - Edit `Cargo.toml` version field before building
153 -
154 - ## Full Setup
155 -
156 - See `SERVER_SETUP.md` for the complete provisioning checklist (PostgreSQL, Caddy, systemd, Stripe, S3, DNS, Cloudflare, security hardening) and `RECOVERY.md` for disaster recovery procedures.
@@ -1,172 +0,0 @@
1 - # Database Recovery Procedure
2 -
3 - How to restore the Makenotwork database from a backup.
4 -
5 - Backups are gzipped SQL dumps in `/opt/makenotwork/backups/`, named `makenotwork-YYYYMMDD-HHMMSS.sql.gz`. Kept for 30 days.
6 -
7 - ---
8 -
9 - ## List Available Backups
10 -
11 - ```bash
12 - ls -lh /opt/makenotwork/backups/makenotwork-*.sql.gz
13 - ```
14 -
15 - ## Full Restore
16 -
17 - Replaces the entire database with the backup contents.
18 -
19 - ### 1. Stop the application
20 -
21 - ```bash
22 - sudo systemctl stop makenotwork
23 - ```
24 -
25 - ### 2. Drop and recreate the database
26 -
27 - ```bash
28 - sudo -u postgres psql <<EOF
29 - DROP DATABASE makenotwork;
30 - CREATE DATABASE makenotwork OWNER makenotwork;
31 - EOF
32 - ```
33 -
34 - ### 3. Restore from backup
35 -
36 - ```bash
37 - gunzip -c /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz \
38 - | psql -U makenotwork -d makenotwork
39 - ```
40 -
41 - ### 4. Verify
42 -
43 - ```bash
44 - psql -U makenotwork -d makenotwork -c "SELECT COUNT(*) FROM users;"
45 - psql -U makenotwork -d makenotwork -c "SELECT COUNT(*) FROM projects;"
46 - psql -U makenotwork -d makenotwork -c "SELECT COUNT(*) FROM items;"
47 - ```
48 -
49 - ### 5. Restart the application
50 -
51 - ```bash
52 - sudo systemctl start makenotwork
53 - sudo systemctl status makenotwork
54 - ```
55 -
56 - ### 6. Smoke test
57 -
58 - - Visit https://makenot.work/ and confirm it loads
59 - - Check /health for system status
60 - - Try logging in
61 -
62 - ---
63 -
64 - ## Selective Restore (Single Table)
65 -
66 - If only one table is corrupted, extract and restore it without touching the rest.
67 -
68 - ### 1. Extract the table from the backup
69 -
70 - ```bash
71 - gunzip -c /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz \
72 - | grep -A9999999 "^COPY public.TABLE_NAME" \
73 - | sed '/^\\\.$/q' > /tmp/table_restore.sql
74 - ```
75 -
76 - ### 2. Review the extracted data
77 -
78 - ```bash
79 - head -20 /tmp/table_restore.sql
80 - wc -l /tmp/table_restore.sql
81 - ```
82 -
83 - ### 3. Clear and restore the table
84 -
85 - ```bash
86 - psql -U makenotwork -d makenotwork -c "DELETE FROM TABLE_NAME;"
87 - psql -U makenotwork -d makenotwork < /tmp/table_restore.sql
88 - ```
89 -
90 - **Note:** Watch for foreign key constraints. If the table has dependencies, you may need to temporarily disable triggers:
91 -
92 - ```bash
93 - psql -U makenotwork -d makenotwork <<EOF
94 - SET session_replication_role = 'replica';
95 - DELETE FROM TABLE_NAME;
96 - \i /tmp/table_restore.sql
97 - SET session_replication_role = 'origin';
98 - EOF
99 - ```
100 -
101 - ---
102 -
103 - ## Restore to a Separate Database (For Inspection)
104 -
105 - Useful when you want to check backup contents without touching production.
106 -
107 - ```bash
108 - sudo -u postgres createdb makenotwork_restore -O makenotwork
109 - gunzip -c /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz \
110 - | psql -U makenotwork -d makenotwork_restore
111 -
112 - # Inspect
113 - psql -U makenotwork -d makenotwork_restore
114 -
115 - # Clean up when done
116 - sudo -u postgres dropdb makenotwork_restore
117 - ```
118 -
119 - ---
120 -
121 - ## Failure Scenarios
122 -
123 - ### Application won't start after restore
124 -
125 - Check migration state. The backup includes the `_sqlx_migrations` table, so the app should recognize the schema. If migrations are ahead of the backup:
126 -
127 - ```bash
128 - # Check what the app expects vs what's in the DB
129 - psql -U makenotwork -d makenotwork \
130 - -c "SELECT version, description FROM _sqlx_migrations ORDER BY version;"
131 - ```
132 -
133 - If the backup is from before a migration was applied, the app will attempt to run pending migrations on startup.
134 -
135 - ### Backup file is corrupted
136 -
137 - ```bash
138 - # Test gzip integrity
139 - gzip -t /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz
140 - ```
141 -
142 - If the most recent backup is bad, use the previous day's backup.
143 -
144 - ### No backups available
145 -
146 - If all backups have been lost, the only option is to start fresh:
147 -
148 - ```bash
149 - sudo -u postgres psql <<EOF
150 - DROP DATABASE makenotwork;
151 - CREATE DATABASE makenotwork OWNER makenotwork;
152 - EOF
153 - sudo systemctl restart makenotwork
154 - # The app will run all migrations and create a clean schema
155 - ```
156 -
157 - ---
158 -
159 - ## Backup Verification
160 -
161 - To confirm backups are running and healthy:
162 -
163 - ```bash
164 - # Check the most recent backup
165 - ls -lt /opt/makenotwork/backups/makenotwork-*.sql.gz | head -1
166 -
167 - # Check backup log for errors
168 - tail -20 /opt/makenotwork/backups/backup.log
169 -
170 - # Check cron is scheduled
171 - sudo crontab -u makenotwork -l
172 - ```
@@ -1,352 +0,0 @@
1 - # Makenotwork Server Setup Guide
2 -
3 - Complete checklist for deploying to Hetzner VPS.
4 -
5 - ---
6 -
7 - ## Pre-Deployment Checklist (Do Now)
8 -
9 - These can be done before provisioning the server:
10 -
11 - ### Stripe Setup
12 - - [ ] Create Stripe account (if not already)
13 - - [ ] Switch to live mode (or stay in test mode for initial testing)
14 - - [ ] Note your **Secret Key** (`sk_live_...` or `sk_test_...`)
15 - - [ ] Go to Settings > Connect settings
16 - - [ ] Note your **Client ID** (`ca_...`)
17 - - [ ] Go to Developers > Webhooks > Add endpoint
18 - - URL: `https://makenot.work/stripe/webhook`
19 - - Events: `checkout.session.completed`, `account.updated`
20 - - Note the **Webhook Secret** (`whsec_...`)
21 -
22 - ### Hetzner Object Storage Setup
23 - - [ ] Create Object Storage bucket in Hetzner Cloud Console
24 - - [ ] Bucket name: `makenotwork-files` (or your choice)
25 - - [ ] Region: `fsn1` (Frankfurt) or your preferred
26 - - [ ] Generate S3 credentials
27 - - [ ] Note: Endpoint, Access Key, Secret Key
28 -
29 - ### DNS Setup
30 - - [ ] Point `makenot.work` A record to server IP
31 - - [ ] Point `www.makenot.work` A record to server IP (for redirect)
32 -
33 - ### Generate Secrets
34 - Run locally and save for later:
35 - ```bash
36 - # JWT Secret
37 - openssl rand -base64 32
38 -
39 - # Session Secret
40 - openssl rand -base64 32
41 -
42 - # Database Password
43 - openssl rand -base64 24
44 - ```
45 -
46 - ---
47 -
48 - ## Server Provisioning
49 -
50 - ### 1. Create Hetzner VPS — DONE
51 - - Type: CCX13 x86 (US-West)
52 - - Disk: 80GB + 10GB
53 - - IP: `5.78.144.244`
54 - - DNS: Cloudflare pointing makenot.work + maxj.phd to this IP
55 -
56 - ### 2. Initial Server Setup
57 - ```bash
58 - # SSH into server
59 - ssh root@100.120.174.96
60 -
61 - # Update system
62 - apt update && apt upgrade -y
63 -
64 - # Set timezone
65 - timedatectl set-timezone America/New_York # or your timezone
66 -
67 - # Create non-root user (optional but recommended)
68 - adduser makenotwork
69 - usermod -aG sudo makenotwork
70 - ```
71 -
72 - ### 3. Install PostgreSQL
73 - ```bash
74 - # Install PostgreSQL
75 - apt install postgresql postgresql-contrib -y
76 -
77 - # Start and enable
78 - systemctl start postgresql
79 - systemctl enable postgresql
80 -
81 - # Create database and user
82 - sudo -u postgres psql << EOF
83 - CREATE USER makenotwork WITH PASSWORD '<DB_PASSWORD>';
84 - CREATE DATABASE makenotwork OWNER makenotwork;
85 - GRANT ALL PRIVILEGES ON DATABASE makenotwork TO makenotwork;
86 - EOF
87 -
88 - # Test connection
89 - psql -U makenotwork -h localhost -d makenotwork
90 - ```
91 -
92 - ### 4. Install Caddy
93 - ```bash
94 - # Install Caddy
95 - apt install -y debian-keyring debian-archive-keyring apt-transport-https
96 - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
97 - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
98 - apt update
99 - apt install caddy -y
100 -
101 - # Create log directory
102 - mkdir -p /var/log/caddy
103 - chown caddy:caddy /var/log/caddy
104 - ```
105 -
106 - ### 5. Create Application Directory
107 - ```bash
108 - # Create directories
109 - mkdir -p /opt/makenotwork/docs
110 - chown -R makenotwork:makenotwork /opt/makenotwork
111 -
112 - # If using root for deployment initially:
113 - # mkdir -p /opt/makenotwork/docs
114 - ```
115 -
116 - ### 6. Upload Configuration Files
117 -
118 - From your local machine:
119 - ```bash
120 - # Copy Caddyfile
121 - scp deploy/Caddyfile root@100.120.174.96:/etc/caddy/Caddyfile
122 -
123 - # Copy systemd service
124 - scp deploy/makenotwork.service root@100.120.174.96:/etc/systemd/system/
125 -
126 - # Copy environment template
127 - scp deploy/env.production root@100.120.174.96:/opt/makenotwork/.env
128 - ```
129 -
130 - ### 7. Configure Environment
131 - ```bash
132 - # SSH into server
133 - ssh root@100.120.174.96
134 -
135 - # Edit .env with your actual values
136 - nano /opt/makenotwork/.env
137 -
138 - # Secure the file
139 - chmod 600 /opt/makenotwork/.env
140 - chown makenotwork:makenotwork /opt/makenotwork/.env
141 - ```
142 -
143 - ### 8. Enable Services
144 - ```bash
145 - # Reload systemd
146 - systemctl daemon-reload
147 -
148 - # Enable services
149 - systemctl enable makenotwork
150 - systemctl enable caddy
151 -
152 - # Start Caddy (will get SSL certificate)
153 - systemctl restart caddy
154 - ```
155 -
156 - ---
157 -
158 - ## First Deployment
159 -
160 - ### Cross-Compilation Setup (one-time, on Mac)
161 - ```bash
162 - brew install zig
163 - cargo install cargo-zigbuild
164 - rustup target add x86_64-unknown-linux-gnu
165 - ```
166 -
167 - ### Build and Deploy
168 - From your local machine in the `MNW/` directory:
169 -
170 - ```bash
171 - # Make deploy script executable
172 - chmod +x deploy/deploy.sh
173 -
174 - # Deploy — cross-compiles for x86_64 Linux, uploads binary, restarts service
175 - ./deploy/deploy.sh root@100.120.174.96
176 - ```
177 -
178 - ### Verify Deployment
179 - ```bash
180 - # Check service status
181 - ssh root@100.120.174.96 "systemctl status makenotwork"
182 -
183 - # Check logs
184 - ssh root@100.120.174.96 "journalctl -u makenotwork -f"
185 -
186 - # Test endpoints
187 - curl https://makenot.work/
188 - curl https://makenot.work/docs/
189 - ```
190 -
191 - ---
192 -
193 - ## Git SSH Access
194 -
195 - Public SSH access via `git.makenot.work` for clone/push from anywhere.
196 -
197 - ### Prerequisites
198 -
199 - - `setup-git-ssh.sh` and `setup-ssh-keys.sh` already exist in `deploy/`
200 - - `mnw-admin` binary with `rebuild-keys` and `git-auth` subcommands
201 - - SSH key management UI in dashboard already functional
202 -
203 - ### 1. DNS Record
204 -
205 - Add in Cloudflare (proxy **OFF** — SSH cannot go through Cloudflare):
206 - - Type: `A`
207 - - Name: `git`
208 - - Content: `5.78.144.244`
209 - - Proxy: DNS only (grey cloud)
210 -
211 - ### 2. Create git system user
212 - ```bash
213 - ssh root@100.120.174.96
214 - bash /opt/makenotwork/deploy/setup-git-ssh.sh
215 - ```
216 -
217 - ### 3. Set up sudoers for authorized_keys rebuild
218 - ```bash
219 - bash /opt/makenotwork/deploy/setup-ssh-keys.sh
220 - ```
221 -
222 - ### 4. Install sshd config
223 - ```bash
224 - cp /opt/makenotwork/deploy/sshd-git.conf /etc/ssh/sshd_config.d/git.conf
225 - systemctl restart sshd
226 - ```
227 -
228 - ### 5. Install fail2ban
229 - ```bash
230 - apt install fail2ban -y
231 - cp /opt/makenotwork/deploy/fail2ban-sshd.conf /etc/fail2ban/jail.d/sshd.conf
232 - systemctl enable fail2ban
233 - systemctl restart fail2ban
234 - ```
235 -
236 - ### 6. Configure firewall
237 - ```bash
238 - apt install ufw -y
239 - bash /opt/makenotwork/deploy/setup-firewall.sh
240 - ```
241 -
242 - ### 7. Add GIT_SSH_HOST to .env
243 - ```bash
244 - echo 'GIT_SSH_HOST=git.makenot.work' >> /opt/makenotwork/.env
245 - systemctl restart makenotwork
246 - ```
247 -
248 - ### 8. Verify
249 - ```bash
250 - # Should print "Interactive login disabled" or similar
251 - ssh git@git.makenot.work
252 -
253 - # Clone test (after adding SSH key in dashboard)
254 - git clone git@git.makenot.work:max/makenotwork.git /tmp/test-clone
255 - rm -rf /tmp/test-clone
256 - ```
257 -
258 - ---
259 -
260 - ## Post-Deployment
261 -
262 - ### Remove Demo Data
263 - The demo seed creates a test account. Remove it:
264 - ```bash
265 - # On the server
266 - psql -U makenotwork -d makenotwork << EOF
267 - DELETE FROM transactions WHERE buyer_id IN (SELECT id FROM users WHERE email = 'elena@example.com');
268 - DELETE FROM transactions WHERE seller_id IN (SELECT id FROM users WHERE email = 'elena@example.com');
269 - DELETE FROM items WHERE project_id IN (SELECT id FROM projects WHERE user_id IN (SELECT id FROM users WHERE email = 'elena@example.com'));
270 - DELETE FROM projects WHERE user_id IN (SELECT id FROM users WHERE email = 'elena@example.com');
271 - DELETE FROM custom_links WHERE user_id IN (SELECT id FROM users WHERE email = 'elena@example.com');
272 - DELETE FROM users WHERE email = 'elena@example.com';
273 - EOF
274 - ```
275 -
276 - ### Create Your Account
277 - 1. Go to https://makenot.work/join
278 - 2. Create your account
279 - 3. Go to Dashboard > Account
280 - 4. Connect Stripe
281 - 5. Create a project and upload content
282 -
283 - ---
284 -
285 - ## Troubleshooting
286 -
287 - ### Service won't start
288 - ```bash
289 - # Check logs
290 - journalctl -u makenotwork -n 50
291 -
292 - # Common issues:
293 - # - Database connection: check DATABASE_URL
294 - # - Missing .env: check /opt/makenotwork/.env exists
295 - # - Permission denied: check file ownership
296 - ```
297 -
298 - ### SSL Certificate Issues
299 - ```bash
300 - # Check Caddy logs
301 - journalctl -u caddy -f
302 -
303 - # Verify DNS is pointing to server
304 - dig makenot.work
305 - ```
306 -
307 - ### Database Connection Failed
308 - ```bash
309 - # Test connection
310 - psql -U makenotwork -h localhost -d makenotwork
311 -
312 - # Check PostgreSQL is running
313 - systemctl status postgresql
314 -
315 - # Check pg_hba.conf allows local connections
316 - cat /etc/postgresql/*/main/pg_hba.conf | grep makenotwork
317 - ```
318 -
319 - ### Stripe Webhooks Not Working
320 - 1. Check webhook is configured in Stripe Dashboard
321 - 2. Verify URL: `https://makenot.work/stripe/webhook`
322 - 3. Check STRIPE_WEBHOOK_SECRET matches
323 - 4. Test with Stripe CLI: `stripe listen --forward-to localhost:3000/stripe/webhook`
324 -
325 - ---
326 -
327 - ## Maintenance
328 -
329 - ### Update Application
330 - ```bash
331 - ./deploy/deploy.sh root@100.120.174.96
332 - ```
333 -
334 - ### View Logs
335 - ```bash
336 - # Application logs
337 - ssh root@100.120.174.96 "journalctl -u makenotwork -f"
338 -
339 - # Caddy logs
340 - ssh root@100.120.174.96 "tail -f /var/log/caddy/makenotwork.log"
341 - ```
342 -
343 - ### Database Backups
344 - Automated daily backups with 30-day retention. See setup in `backup-db.sh` header comments.
345 -
346 - For recovery procedures, see `RECOVERY.md`.
347 -
348 - ### Restart Services
349 - ```bash
350 - ssh root@100.120.174.96 "sudo systemctl restart makenotwork"
351 - ssh root@100.120.174.96 "sudo systemctl restart caddy"
352 - ```
@@ -1,57 +0,0 @@
1 - #!/bin/bash
2 - # Makenotwork Database Backup Script
3 - # Runs daily via cron, keeps 30 days of backups.
4 - #
5 - # Setup:
6 - # 1. Copy to server:
7 - # scp deploy/backup-db.sh root@<server>:/opt/makenotwork/
8 - # chmod +x /opt/makenotwork/backup-db.sh
9 - #
10 - # 2. Create backup directory:
11 - # mkdir -p /opt/makenotwork/backups
12 - # chown makenotwork:makenotwork /opt/makenotwork/backups
13 - #
14 - # 3. Add cron job (as makenotwork user):
15 - # sudo crontab -u makenotwork -e
16 - # # Daily at 03:00 UTC:
17 - # 0 3 * * * /opt/makenotwork/backup-db.sh >> /opt/makenotwork/backups/backup.log 2>&1
18 -
19 - set -euo pipefail
20 -
21 - # Configuration
22 - BACKUP_DIR="/opt/makenotwork/backups"
23 - DB_NAME="makenotwork"
24 - DB_USER="makenotwork"
25 - RETENTION_DAYS=30
26 -
27 - # Derived
28 - TIMESTAMP=$(date +%Y%m%d-%H%M%S)
29 - BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}-${TIMESTAMP}.sql.gz"
30 -
31 - echo "[$(date -Iseconds)] Starting backup..."
32 -
33 - # Ensure backup directory exists
34 - mkdir -p "$BACKUP_DIR"
35 -
36 - # Dump and compress
37 - # Uses peer auth (no password needed when running as makenotwork user)
38 - pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE"
39 -
40 - # Verify the file is non-empty
41 - FILESIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || stat -f%z "$BACKUP_FILE" 2>/dev/null)
42 - if [ "$FILESIZE" -lt 100 ]; then
43 - echo "[$(date -Iseconds)] ERROR: Backup file suspiciously small (${FILESIZE} bytes)"
44 - exit 1
45 - fi
46 -
47 - echo "[$(date -Iseconds)] Backup complete: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
48 -
49 - # Prune backups older than retention period
50 - DELETED=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
51 - if [ "$DELETED" -gt 0 ]; then
52 - echo "[$(date -Iseconds)] Pruned $DELETED backup(s) older than ${RETENTION_DAYS} days"
53 - fi
54 -
55 - # Summary
56 - TOTAL=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" | wc -l)
57 - echo "[$(date -Iseconds)] Total backups on disk: $TOTAL"
@@ -1,35 +0,0 @@
1 - -----BEGIN CERTIFICATE-----
2 - MIIGCjCCA/KgAwIBAgIIV5G6lVbCLmEwDQYJKoZIhvcNAQENBQAwgZAxCzAJBgNV
3 - BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMRQwEgYDVQQLEwtPcmln
4 - aW4gUHVsbDEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZv
5 - cm5pYTEjMCEGA1UEAxMab3JpZ2luLXB1bGwuY2xvdWRmbGFyZS5uZXQwHhcNMTkx
6 - MDEwMTg0NTAwWhcNMjkxMTAxMTcwMDAwWjCBkDELMAkGA1UEBhMCVVMxGTAXBgNV
7 - BAoTEENsb3VkRmxhcmUsIEluYy4xFDASBgNVBAsTC09yaWdpbiBQdWxsMRYwFAYD
8 - VQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMSMwIQYDVQQD
9 - ExpvcmlnaW4tcHVsbC5jbG91ZGZsYXJlLm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD
10 - ggIPADCCAgoCggIBAN2y2zojYfl0bKfhp0AJBFeV+jQqbCw3sHmvEPwLmqDLqynI
11 - 42tZXR5y914ZB9ZrwbL/K5O46exd/LujJnV2b3dzcx5rtiQzso0xzljqbnbQT20e
12 - ihx/WrF4OkZKydZzsdaJsWAPuplDH5P7J82q3re88jQdgE5hqjqFZ3clCG7lxoBw
13 - hLaazm3NJJlUfzdk97ouRvnFGAuXd5cQVx8jYOOeU60sWqmMe4QHdOvpqB91bJoY
14 - QSKVFjUgHeTpN8tNpKJfb9LIn3pun3bC9NKNHtRKMNX3Kl/sAPq7q/AlndvA2Kw3
15 - Dkum2mHQUGdzVHqcOgea9BGjLK2h7SuX93zTWL02u799dr6Xkrad/WShHchfjjRn
16 - aL35niJUDr02YJtPgxWObsrfOU63B8juLUphW/4BOjjJyAG5l9j1//aUGEi/sEe5
17 - lqVv0P78QrxoxR+MMXiJwQab5FB8TG/ac6mRHgF9CmkX90uaRh+OC07XjTdfSKGR
18 - PpM9hB2ZhLol/nf8qmoLdoD5HvODZuKu2+muKeVHXgw2/A6wM7OwrinxZiyBk5Hh
19 - CvaADH7PZpU6z/zv5NU5HSvXiKtCzFuDu4/Zfi34RfHXeCUfHAb4KfNRXJwMsxUa
20 - +4ZpSAX2G6RnGU5meuXpU5/V+DQJp/e69XyyY6RXDoMywaEFlIlXBqjRRA2pAgMB
21 - AAGjZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud
22 - DgQWBBRDWUsraYuA4REzalfNVzjann3F6zAfBgNVHSMEGDAWgBRDWUsraYuA4REz
23 - alfNVzjann3F6zANBgkqhkiG9w0BAQ0FAAOCAgEAkQ+T9nqcSlAuW/90DeYmQOW1
24 - QhqOor5psBEGvxbNGV2hdLJY8h6QUq48BCevcMChg/L1CkznBNI40i3/6heDn3IS
25 - zVEwXKf34pPFCACWVMZxbQjkNRTiH8iRur9EsaNQ5oXCPJkhwg2+IFyoPAAYURoX
26 - VcI9SCDUa45clmYHJ/XYwV1icGVI8/9b2JUqklnOTa5tugwIUi5sTfipNcJXHhgz
27 - 6BKYDl0/UP0lLKbsUETXeTGDiDpxZYIgbcFrRDDkHC6BSvdWVEiH5b9mH2BON60z
28 - 0O0j8EEKTwi9jnafVtZQXP/D8yoVowdFDjXcKkOPF/1gIh9qrFR6GdoPVgB3SkLc
29 - 5ulBqZaCHm563jsvWb/kXJnlFxW+1bsO9BDD6DweBcGdNurgmH625wBXksSdD7y/
30 - fakk8DagjbjKShYlPEFOAqEcliwjF45eabL0t27MJV61O/jHzHL3dknXeE4BDa2j
31 - bA+JbyJeUMtU7KMsxvx82RmhqBEJJDBCJ3scVptvhDMRrtqDBW5JShxoAOcpFQGm
32 - iYWicn46nPDjgTU0bX1ZPpTpryXbvciVL5RkVBuyX2ntcOLDPlZWgxZCBp96x07F
33 - AnOzKgZk4RzZPNAxCXERVxajn/FLcOhglVAKo5H0ac+AitlQ0ip55D2/mf8o72tM
34 - fVQ6VpyjEXdiIXWUq/o=
35 - -----END CERTIFICATE-----
@@ -1,156 +0,0 @@
1 - #!/bin/bash
2 - # Makenotwork Deployment Script
3 - # Cross-compiles for x86_64 Linux on macOS, uploads everything, restarts services.
4 - # Run from the MNW directory.
5 - #
6 - # Usage:
7 - # ./deploy/deploy.sh # Full deploy (build + upload + config + restart)
8 - # ./deploy/deploy.sh --quick # Quick deploy (build + upload binary + restart app)
9 - # ./deploy/deploy.sh --config # Config only (upload Caddyfile, systemd, error pages, backup script)
10 - #
11 - # Prerequisites (one-time):
12 - # brew install zig
13 - # cargo install cargo-zigbuild
14 - # rustup target add x86_64-unknown-linux-gnu
15 -
16 - set -e
17 -
18 - # Configuration
19 - SERVER="root@100.120.174.96"
20 - REMOTE_DIR="/opt/makenotwork"
21 - BINARY_NAME="makenotwork"
22 - TARGET="x86_64-unknown-linux-gnu"
23 - DEPLOY_DIR="deploy"
24 -
25 - # Check we're in the right directory
26 - if [ ! -f "Cargo.toml" ]; then
27 - echo "Error: Run this script from the MNW directory"
28 - exit 1
29 - fi
30 -
31 - upload_config() {
32 - echo "[config] Uploading configuration files..."
33 - scp $DEPLOY_DIR/Caddyfile $SERVER:/etc/caddy/Caddyfile
34 - scp $DEPLOY_DIR/makenotwork.service $SERVER:/etc/systemd/system/makenotwork.service
35 - scp $DEPLOY_DIR/backup-db.sh $SERVER:$REMOTE_DIR/backup-db.sh
36 - ssh $SERVER "chmod +x $REMOTE_DIR/backup-db.sh"
37 -
38 - # Error pages
39 - ssh $SERVER "mkdir -p $REMOTE_DIR/error-pages"
40 - scp $DEPLOY_DIR/error-pages/*.html $SERVER:$REMOTE_DIR/error-pages/
41 -
42 - # Git SSH and security config files
43 - ssh $SERVER "mkdir -p $REMOTE_DIR/deploy"
44 - scp $DEPLOY_DIR/sshd-git.conf $DEPLOY_DIR/fail2ban-sshd.conf $DEPLOY_DIR/setup-firewall.sh $SERVER:$REMOTE_DIR/deploy/
45 - scp $DEPLOY_DIR/setup-git-ssh.sh $DEPLOY_DIR/setup-ssh-keys.sh $SERVER:$REMOTE_DIR/deploy/ 2>/dev/null || true
46 - ssh $SERVER "chmod +x $REMOTE_DIR/deploy/setup-firewall.sh $REMOTE_DIR/deploy/setup-git-ssh.sh $REMOTE_DIR/deploy/setup-ssh-keys.sh 2>/dev/null || true"
47 -
48 - # Minify CSS for production (restore source on exit)
49 - echo "[config] Minifying CSS..."
50 - cp static/style.css static/style.css.src
51 - restore_css() { [ -f static/style.css.src ] && mv static/style.css.src static/style.css; }
52 - trap restore_css EXIT
53 - npx --yes clean-css-cli -o static/style.css static/style.css.src
54 - echo "[config] CSS: $(wc -c < static/style.css.src | tr -d ' ')B -> $(wc -c < static/style.css | tr -d ' ')B"
55 -
56 - # Static assets (CSS, JS, fonts, images)
57 - echo "[config] Uploading static assets..."
58 - rsync -az --delete static/ $SERVER:$REMOTE_DIR/static/
59 -
60 - # Restore unminified CSS
61 - restore_css
62 - trap - EXIT
63 -
64 - # Documentation (public markdown files)
65 - echo "[config] Uploading documentation..."
66 - rsync -az --delete site-docs/public/ $SERVER:$REMOTE_DIR/docs/public/
67 -
68 - # Rustdoc (API reference for library crates)
69 - echo "[config] Generating rustdoc..."
70 - "$DEPLOY_DIR/generate-rustdoc.sh"
71 - echo "[config] Uploading rustdoc..."
72 - rsync -az --delete rustdoc-out/ $SERVER:$REMOTE_DIR/rustdoc/
73 -
74 - # Reload systemd and restart Caddy
75 - ssh $SERVER "systemctl daemon-reload && systemctl restart caddy"
76 - echo "[config] Done"
77 - }
78 -
79 - build_binary() {
80 - echo "[build] Cross-compiling for $TARGET..."
81 - ulimit -n 65536 2>/dev/null || true
82 - cargo zigbuild --release --target $TARGET
83 - echo "[build] Done: target/$TARGET/release/$BINARY_NAME"
84 - }
85 -
86 - upload_binary() {
87 - echo "[upload] Stopping service and uploading binary..."
88 - ssh $SERVER "systemctl stop makenotwork || true"
89 - scp target/$TARGET/release/$BINARY_NAME $SERVER:$REMOTE_DIR/$BINARY_NAME
90 - ssh $SERVER "chmod +x $REMOTE_DIR/$BINARY_NAME"
91 - # Also upload mnw-admin binary (used for SSH key management)
92 - if [ -f "target/$TARGET/release/mnw-admin" ]; then
93 - scp target/$TARGET/release/mnw-admin $SERVER:$REMOTE_DIR/mnw-admin
94 - ssh $SERVER "chmod +x $REMOTE_DIR/mnw-admin"
95 - echo "[upload] mnw-admin binary uploaded"
96 - fi
97 - echo "[upload] Done"
98 - }
99 -
100 - send_restart_warning() {
101 - echo "[warning] Sending 30s restart warning to users..."
102 - local token
103 - token=$(ssh $SERVER "grep '^CLI_SERVICE_TOKEN=' $REMOTE_DIR/.env 2>/dev/null | cut -d= -f2-" | tr -d '\r\n')
104 - if [ -z "$token" ]; then
105 - echo "[warning] CLI_SERVICE_TOKEN not found in .env, skipping warning"
106 - return 0
107 - fi
108 - local status
109 - status=$(ssh $SERVER "curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3000/api/internal/restart-warning -H 'Authorization: Bearer $token' -H 'Content-Type: application/json' -d '{\"seconds\": 30}'")
110 - if [ "$status" = "204" ]; then
111 - echo "[warning] Restart warning sent, waiting 30s..."
112 - sleep 30
113 - else
114 - echo "[warning] Warning request returned HTTP $status, continuing without delay"
115 - fi
116 - }
117 -
118 - restart_app() {
119 - echo "[restart] Restarting makenotwork..."
120 - ssh $SERVER "systemctl restart makenotwork"
121 - sleep 1
122 - echo ""
123 - ssh $SERVER "systemctl status makenotwork --no-pager"
124 - echo ""
125 - echo "[restart] Verifying app responds..."
126 - ssh $SERVER "curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:3000"
127 - }
128 -
129 - case "${1:-full}" in
130 - --quick)
131 - echo "=== Quick Deploy ==="
132 - build_binary
133 - send_restart_warning
134 - upload_binary
135 - restart_app
136 - ;;
137 - --config)
138 - echo "=== Config Deploy ==="
139 - upload_config
140 - ;;
141 - full|"")
142 - echo "=== Full Deploy ==="
143 - build_binary
144 - upload_config
145 - send_restart_warning
146 - upload_binary
147 - restart_app
148 - ;;
149 - *)
150 - echo "Usage: $0 [--quick|--config]"
151 - exit 1
152 - ;;
153 - esac
154 -
155 - echo ""
156 - echo "=== Deploy Complete ==="
@@ -1,75 +0,0 @@
1 - <!DOCTYPE html>
2 - <html lang="en">
3 - <head>
4 - <meta charset="UTF-8">
5 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 - <title>Page Not Found - makenot.work</title>
7 - <link rel="preconnect" href="https://fonts.googleapis.com">
8 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9 - <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Lato&family=Young+Serif&display=swap" rel="stylesheet">
10 - <style>
11 - * { margin: 0; padding: 0; box-sizing: border-box; }
12 - body {
13 - min-height: 100vh;
14 - display: flex;
15 - flex-direction: column;
16 - align-items: center;
17 - justify-content: center;
18 - padding: 2rem;
19 - background: #ede8e1;
20 - color: #3d3530;
21 - font-family: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
22 - }
23 - .wordmark {
24 - position: absolute;
25 - top: 2rem;
26 - left: 2rem;
27 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
28 - font-size: 1.25rem;
29 - color: #3d3530;
30 - text-decoration: none;
31 - }
32 - .wordmark .dot { color: #6c5ce7; }
33 - .container { text-align: center; max-width: 500px; }
34 - .code {
35 - font-size: 8rem;
36 - font-weight: 400;
37 - line-height: 1;
38 - margin-bottom: 1rem;
39 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
40 - color: #3d3530;
41 - }
42 - .title {
43 - font-size: 1.25rem;
44 - font-family: "IBM Plex Mono", "Courier New", Consolas, monospace;
45 - color: #8a8480;
46 - margin-bottom: 1.5rem;
47 - }
48 - .message {
49 - color: #8a8480;
50 - margin-bottom: 2rem;
51 - line-height: 1.6;
52 - }
53 - a.btn {
54 - display: inline-block;
55 - padding: 0.75rem 1.5rem;
56 - background: #3d3530;
57 - color: #ede8e1;
58 - text-decoration: none;
59 - border-radius: 6px;
60 - font-weight: 500;
61 - transition: opacity 0.2s;
62 - }
63 - a.btn:hover { opacity: 0.85; }
64 - </style>
65 - </head>
66 - <body>
67 - <a href="/" class="wordmark">Makenot<span class="dot">.</span>work</a>
68 - <div class="container">
69 - <div class="code">404</div>
70 - <div class="title">Page not found</div>
71 - <p class="message">The page you're looking for doesn't exist or has been moved.</p>
72 - <a href="/" class="btn">Go Home</a>
73 - </div>
74 - </body>
75 - </html>
@@ -1,83 +0,0 @@
1 - <!DOCTYPE html>
2 - <html lang="en">
3 - <head>
4 - <meta charset="UTF-8">
5 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 - <title>Something Went Wrong - makenot.work</title>
7 - <meta http-equiv="refresh" content="15">
8 - <link rel="preconnect" href="https://fonts.googleapis.com">
9 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10 - <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Lato&family=Young+Serif&display=swap" rel="stylesheet">
11 - <style>
12 - * { margin: 0; padding: 0; box-sizing: border-box; }
13 - body {
14 - min-height: 100vh;
15 - display: flex;
16 - flex-direction: column;
17 - align-items: center;
18 - justify-content: center;
19 - padding: 2rem;
20 - background: #ede8e1;
21 - color: #3d3530;
22 - font-family: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
23 - }
24 - .wordmark {
25 - position: absolute;
26 - top: 2rem;
27 - left: 2rem;
28 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
29 - font-size: 1.25rem;
30 - color: #3d3530;
31 - text-decoration: none;
32 - }
33 - .wordmark .dot { color: #6c5ce7; }
34 - .container { text-align: center; max-width: 500px; }
35 - .code {
36 - font-size: 8rem;
37 - font-weight: 400;
38 - line-height: 1;
39 - margin-bottom: 1rem;
40 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
41 - color: #3d3530;
42 - }
43 - .title {
44 - font-size: 1.25rem;
45 - font-family: "IBM Plex Mono", "Courier New", Consolas, monospace;
46 - color: #8a8480;
47 - margin-bottom: 1.5rem;
48 - }
49 - .message {
50 - color: #8a8480;
51 - margin-bottom: 2rem;
52 - line-height: 1.6;
53 - }
54 - .retry {
55 - font-family: "IBM Plex Mono", "Courier New", Consolas, monospace;
56 - font-size: 0.85rem;
57 - color: #8a8480;
58 - }
59 - a.btn {
60 - display: inline-block;
61 - padding: 0.75rem 1.5rem;
62 - background: #3d3530;
63 - color: #ede8e1;
64 - text-decoration: none;
65 - border-radius: 6px;
66 - font-weight: 500;
67 - transition: opacity 0.2s;
68 - margin-bottom: 1.5rem;
69 - }
70 - a.btn:hover { opacity: 0.85; }
71 - </style>
72 - </head>
73 - <body>
74 - <a href="/" class="wordmark">Makenot<span class="dot">.</span>work</a>
75 - <div class="container">
76 - <div class="code">500</div>
77 - <div class="title">Something went wrong</div>
78 - <p class="message">An unexpected error occurred. This has been noted and will be looked into.</p>
79 - <a href="/" class="btn">Go Home</a>
80 - <p class="retry">This page will retry automatically.</p>
81 - </div>
82 - </body>
83 - </html>
@@ -1,83 +0,0 @@
1 - <!DOCTYPE html>
2 - <html lang="en">
3 - <head>
4 - <meta charset="UTF-8">
5 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 - <title>Temporarily Unavailable - makenot.work</title>
7 - <meta http-equiv="refresh" content="10">
8 - <link rel="preconnect" href="https://fonts.googleapis.com">
9 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10 - <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Lato&family=Young+Serif&display=swap" rel="stylesheet">
11 - <style>
12 - * { margin: 0; padding: 0; box-sizing: border-box; }
13 - body {
14 - min-height: 100vh;
15 - display: flex;
16 - flex-direction: column;
17 - align-items: center;
18 - justify-content: center;
19 - padding: 2rem;
20 - background: #ede8e1;
21 - color: #3d3530;
22 - font-family: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
23 - }
24 - .wordmark {
25 - position: absolute;
26 - top: 2rem;
27 - left: 2rem;
28 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
29 - font-size: 1.25rem;
30 - color: #3d3530;
31 - text-decoration: none;
32 - }
33 - .wordmark .dot { color: #6c5ce7; }
34 - .container { text-align: center; max-width: 500px; }
35 - .code {
36 - font-size: 8rem;
37 - font-weight: 400;
38 - line-height: 1;
39 - margin-bottom: 1rem;
40 - font-family: "Young Serif", Georgia, "Times New Roman", serif;
41 - color: #3d3530;
42 - }
43 - .title {
44 - font-size: 1.25rem;
45 - font-family: "IBM Plex Mono", "Courier New", Consolas, monospace;
46 - color: #8a8480;
47 - margin-bottom: 1.5rem;
48 - }
49 - .message {
50 - color: #8a8480;
51 - margin-bottom: 2rem;
52 - line-height: 1.6;
53 - }
54 - .retry {
55 - font-family: "IBM Plex Mono", "Courier New", Consolas, monospace;
56 - font-size: 0.85rem;
57 - color: #8a8480;
58 - }
59 - a.btn {
60 - display: inline-block;
61 - padding: 0.75rem 1.5rem;
62 - background: #3d3530;
63 - color: #ede8e1;
64 - text-decoration: none;
65 - border-radius: 6px;
66 - font-weight: 500;
67 - transition: opacity 0.2s;
68 - margin-bottom: 1.5rem;
69 - }
70 - a.btn:hover { opacity: 0.85; }
71 - </style>
72 - </head>
73 - <body>
74 - <a href="/" class="wordmark">Makenot<span class="dot">.</span>work</a>
75 - <div class="container">
76 - <div class="code">502</div>
77 - <div class="title">Temporarily unavailable</div>
78 - <p class="message">makenot.work is briefly offline for maintenance. Please wait a moment.</p>
79 - <a href="/" class="btn">Try Again</a>
80 - <p class="retry">This page will retry automatically.</p>
81 - </div>
82 - </body>
83 - </html>
@@ -1,10 +0,0 @@
1 - # fail2ban jail for SSH brute force protection
2 - # Drop-in for /etc/fail2ban/jail.d/
3 - [sshd]
4 - enabled = true
5 - port = ssh
6 - filter = sshd
7 - backend = systemd
8 - maxretry = 5
9 - findtime = 600
10 - bantime = 3600
@@ -1,40 +0,0 @@
1 - #!/bin/bash
2 - # Generate rustdoc for shared library crates.
3 - # Output goes to rustdoc-out/ (relative to MNW/).
4 - # Run from the MNW directory.
5 -
6 - set -euo pipefail
7 -
8 - if [ ! -f "Cargo.toml" ]; then
9 - echo "Error: Run this script from the MNW directory"
10 - exit 1
11 - fi
12 -
13 - OUT_DIR="$(pwd)/rustdoc-out"
14 - SHARED_DIR="$(cd ../Shared && pwd)"
15 -
16 - CRATES=("synckit-client" "docengine" "tagtree" "s3-storage" "theme-common")
17 -
18 - rm -rf "$OUT_DIR"
19 - mkdir -p "$OUT_DIR"
20 -
21 - for crate in "${CRATES[@]}"; do
22 - crate_dir="$SHARED_DIR/$crate"
23 - if [ ! -d "$crate_dir" ]; then
24 - echo "Warning: $crate_dir not found, skipping"
25 - continue
26 - fi
27 -
28 - echo "Generating docs for $crate..."
29 - (cd "$crate_dir" && cargo doc --no-deps --target-dir "$OUT_DIR/.target" 2>&1 | tail -1)
30 - done
31 -
32 - # Move generated docs from target/doc/ to output root
33 - if [ -d "$OUT_DIR/.target/doc" ]; then
34 - cp -r "$OUT_DIR/.target/doc/"* "$OUT_DIR/"
35 - rm -rf "$OUT_DIR/.target"
36 - fi
37 -
38 - echo ""
39 - echo "Rustdoc generated in $OUT_DIR/"
40 - ls -1 "$OUT_DIR/" | head -20
@@ -1,413 +0,0 @@
1 - # Makenotwork — Pre-Launch Manual Testing
2 -
3 - ## How to Test
4 -
5 - - Automated tests cover units and integration (1,060+ passing) but can't catch visual bugs, broken flows, or UX issues
6 - - Work through each section sequentially, checking boxes as you go
7 - - If something fails, note the issue inline and keep going — don't block the whole run
8 - - Prioritized: P0 first (launch-blocking), then P1 (core features), then P2 (edge cases)
9 -
10 - ### Environment Setup
11 -
12 - - [ ] PostgreSQL running locally with migrations applied (`cargo sqlx migrate run`)
13 - - [ ] Server running (`cargo run` or release binary)
14 - - [ ] `.env` has Stripe **test** keys (sk_test_*, not sk_live_*)
15 - - [ ] `.env` has SIGNING_SECRET set
16 - - [ ] S3 credentials configured (Hetzner Object Storage or compatible)
17 - - [ ] Postmark token set, or accept console-logged emails for dev
18 - - [ ] ADMIN_USER_ID set to your user's UUID
19 -
20 - ### Tips
21 -
22 - - Open browser devtools Network tab — HTMX requests show as XHR, check for 422/500s
23 - - Run server in a second terminal so you can watch logs in real time
24 - - Use incognito/private window when testing auth flows to avoid session bleed
25 - - Stripe test card: `4242 4242 4242 4242`, any future expiry, any CVC
26 -
27 - ---
28 -
29 - ## P0 — Critical Path
30 -
31 - > If any of these fail, do not launch.
32 -
33 - ### Signup → Verify → Login → Logout
34 -
35 - - [ ] `GET /join` — signup form renders
36 - - [ ] Submit signup with valid username, email, password (8+ chars)
37 - - [ ] Server logs verification email (or Postmark sends it)
38 - - [ ] Verification link in email works (`/verify-email?user=...&expires=...&sig=...`)
39 - - [ ] After verification, email_verified flag is true (check `/dashboard` details tab)
40 - - [ ] `GET /login` — login form renders
41 - - [ ] Login with correct credentials — redirects to `/dashboard`
42 - - [ ] `POST /logout` — session destroyed, redirects to `/`
43 - - [ ] Accessing `/dashboard` after logout redirects to `/login`
44 - - [ ] Login with wrong password — shows error, does not reveal whether user exists
45 - - [ ] Resend verification email works (`/api/resend-verification`)
46 -
47 - ### Account Lockout + Recovery
48 -
49 - - [ ] Fail login 5 times — account locks for 15 minutes
50 - - [ ] Lockout notification email sent with one-time login link
51 - - [ ] One-time login link works (logs you in)
52 - - [ ] One-time login link cannot be reused (single-use)
53 - - [ ] After lockout expires, normal login works again
54 -
55 - ### Password Reset
56 -
57 - - [ ] `GET /forgot-password` — form renders
58 - - [ ] Submit email — reset email sent (15-minute expiry link)
59 - - [ ] Reset link loads form (`/reset-password?user=...&expires=...&sig=...`)
60 - - [ ] Submit new password — succeeds, can login with new password
61 - - [ ] Old password no longer works
62 - - [ ] Expired reset link rejected
63 - - [ ] Reusing same reset link after password change rejected (HMAC includes password hash)
64 -
65 - ### Creator Onboarding
66 -
67 - - [ ] As a regular user, `/dashboard` creator tab shows waitlist apply form
68 - - [ ] Submit waitlist application with pitch text
69 - - [ ] As admin, `GET /admin/waitlist` — shows pending entries
70 - - [ ] Approve entry via `POST /api/admin/waitlist/{id}/approve`
71 - - [ ] Approved user now has can_create_projects flag
72 - - [ ] Approved user sees Stripe Connect setup in dashboard
73 - - [ ] `GET /stripe/connect` — disclaimer page renders
74 - - [ ] `POST /stripe/connect/proceed` — redirects to Stripe OAuth (use test mode)
75 - - [ ] Stripe callback (`/stripe/callback`) saves account ID
76 - - [ ] Dashboard now shows connected Stripe status
77 -
78 - ### Content Creation + Publishing
79 -
80 - - [ ] Create project (`POST /api/projects`) — appears in dashboard
81 - - [ ] Project page renders at `/p/{slug}`
82 - - [ ] Create text item — set title, price (free), description
83 - - [ ] Edit text body (`PUT /api/items/{id}/text`) — markdown renders correctly
84 - - [ ] Create audio item — presign upload, upload file to S3, confirm
85 - - [ ] Audio player works on item page (`/i/{item_id}`)
86 - - [ ] Create download item — presign version upload, upload, confirm
87 - - [ ] Download link works for authorized users
88 - - [ ] Set item visibility to public — appears on `/discover`
89 - - [ ] Set item visibility to private — disappears from `/discover`
90 - - [ ] Create paid item (set price > 0)
91 -
92 - ### Purchase Flow (Fixed Price)
93 -
94 - - [ ] As buyer (different account), browse `/discover` — find the paid item
95 - - [ ] `GET /purchase/{item_id}` — purchase page shows price and fee breakdown
96 - - [ ] `POST /stripe/checkout/{item_id}` — redirects to Stripe Checkout
97 - - [ ] Complete payment with test card (`4242 4242 4242 4242`)
98 - - [ ] `/stripe/success` — success page renders
99 - - [ ] Webhook fires (`checkout.session.completed`) — transaction recorded
100 - - [ ] Item appears in buyer's `/library`
101 - - [ ] Buyer can access item content (stream audio, read text, download file)
102 - - [ ] Cancel checkout — `/stripe/cancel` renders, no transaction created
103 -
104 - ### Pay-What-You-Want (PWYW) Purchase
105 -
106 - - [ ] Create PWYW item with $0 minimum — save succeeds
107 - - [ ] Purchase page shows PWYW input with suggested prices
108 - - [ ] Complete purchase at $0 — item added to library, no Stripe checkout
109 - - [ ] Complete purchase at custom amount (e.g. $5) — Stripe Checkout, item in library
110 - - [ ] Create PWYW item with non-zero minimum (e.g. $5)
111 - - [ ] Attempt purchase below minimum — rejected
112 -
113 - ### Subscription Flow
114 -
115 - - [ ] Create subscription tier on a project (e.g. $3/mo)
116 - - [ ] As buyer, subscription page renders with tier details
117 - - [ ] `POST /stripe/subscribe/{project_id}` — redirects to Stripe Checkout (subscription mode)
118 - - [ ] Complete subscription with test card
119 - - [ ] Webhook fires (`customer.subscription.created`) — subscription recorded
120 - - [ ] Subscriber can access subscriber-only items
121 - - [ ] Non-subscriber cannot access subscriber-only content
122 - - [ ] Cancel subscription — access continues until end of billing period
123 -
124 - ### Discount Codes
125 -
126 - - [ ] Create discount code (e.g. LAUNCH50, 50% off, limited uses)
127 - - [ ] Apply code at checkout — price reduced correctly
128 - - [ ] Discount shows in fee breakdown
129 - - [ ] Exhausted code rejected (after max uses reached)
130 - - [ ] Expired code rejected
131 -
132 - ### License Keys
133 -
134 - - [ ] Create item with license keys enabled
135 - - [ ] After purchase, license key displayed to buyer
136 - - [ ] `POST /api/licenses/{key}/activate` — activation succeeds
137 - - [ ] Activation count increments
138 - - [ ] `GET /api/licenses/{key}/verify` — returns valid status
139 - - [ ] Exceed activation limit — activation rejected
140 -
141 - ### Free Item Claim
142 -
143 - - [ ] As buyer, find a free item on `/discover`
144 - - [ ] `POST /api/library/add/{item_id}` — item added to library
145 - - [ ] Item content accessible
146 - - [ ] `DELETE /api/library/remove/{item_id}` — item removed from library
147 -
148 - ### File Upload + Delivery
149 -
150 - - [ ] Presign request (`POST /api/upload/presign`) returns valid S3 URL
151 - - [ ] Direct upload to presigned URL succeeds
152 - - [ ] Confirm upload (`POST /api/upload/confirm`) stores S3 key
153 - - [ ] Audio streaming URL (`GET /api/stream/{item_id}`) returns presigned URL
154 - - [ ] Version file download (`GET /api/versions/{version_id}/download`) works
155 - - [ ] Cover image upload and display works
156 - - [ ] Presigned URLs expire (check after 1+ hours)
157 -
158 - ---
159 -
160 - ## P1 — Core Features
161 -
162 - ### Dashboard
163 -
164 - - [ ] `/dashboard` renders with projects list
165 - - [ ] Details tab (`/dashboard/tabs/details`) — shows username, email, bio
166 - - [ ] Payments tab (`/dashboard/tabs/payments`) — shows transaction history
167 - - [ ] Projects tab (`/dashboard/tabs/projects`) — lists all projects
168 - - [ ] Creator tab (`/dashboard/tabs/creator`) — shows waitlist or Stripe status
169 - - [ ] Profile update (`PUT /api/users/me`) — display name and bio save correctly
170 - - [ ] Password update (`PUT /api/users/me/password`) — works with correct current password
171 -
172 - ### Project Management
173 -
174 - - [ ] Project dashboard (`/dashboard/project/{slug}`) renders
175 - - [ ] Overview tab — project stats display
176 - - [ ] Content tab — items listed
177 - - [ ] Analytics tab — renders (even if empty)
178 - - [ ] Settings tab — project settings editable
179 - - [ ] Update project (`PUT /api/projects/{id}`) — title, description, type, visibility
180 - - [ ] Delete project (`DELETE /api/projects/{id}`) — cascade deletes items
181 -
182 - ### Item Management
183 -
184 - - [ ] Item dashboard (`/dashboard/item/{id}`) renders
185 - - [ ] Inline edit row (`/dashboard/item/{id}/edit-row`) works via HTMX
186 - - [ ] Update item metadata (`PUT /api/items/{id}`) — title, price, type, description
187 - - [ ] Version list (`GET /api/items/{id}/versions`) renders
188 - - [ ] Create new version (`POST /api/items/{id}/versions`) with file upload
189 -
190 - ### Discover
191 -
192 - - [ ] `/discover` renders with default results
193 - - [ ] Search by text — results filter correctly (trigram search)
194 - - [ ] Filter by category — correct items shown, category counts update
195 - - [ ] Filter by price range (Free, <$25, $25-50, $50-100, $100+)
196 - - [ ] Switch between Items and Projects mode
197 - - [ ] Sort options work (newest, oldest, price, sales)
198 - - [ ] Pagination — next/prev pages load via HTMX
199 - - [ ] `/discover/results` partial loads correctly (check Network tab)
200 -
201 - ### Public Profiles
202 -
203 - - [ ] `/u/{username}` — user profile renders with projects and custom links
204 - - [ ] `/p/{slug}` — project page renders with items
205 - - [ ] `/i/{item_id}` — text item renders markdown correctly
206 - - [ ] `/i/{item_id}` — audio item shows player with chapters
207 - - [ ] `/i/{item_id}` — download item shows version list
208 -
209 - ### Custom Links
210 -
211 - - [ ] Create link (`POST /api/links`) — appears on profile
212 - - [ ] Update link (`PUT /api/links/{id}`) — changes reflected
213 - - [ ] Delete link (`DELETE /api/links/{id}`) — removed from profile
214 - - [ ] Reorder links (`PUT /api/links/reorder`) — order persists
215 -
216 - ### Tags + Chapters
217 -
218 - - [ ] Add tag to item (`POST /api/items/{id}/tags`) — tag appears
219 - - [ ] Remove tag (`DELETE /api/items/{id}/tags/{tag}`) — tag removed
220 - - [ ] Create chapter (`POST /api/items/{id}/chapters`) — chapter marker appears
221 - - [ ] Update chapter (`PUT /api/chapters/{id}`) — changes saved
222 - - [ ] Delete chapter (`DELETE /api/chapters/{id}`) — removed
223 - - [ ] Chapters display on audio item page with correct timestamps
224 -
225 - ### RSS Feeds
226 -
227 - - [ ] `/u/{username}/rss` — valid RSS 2.0, includes public items
228 - - [ ] `/p/{slug}/rss` — valid RSS 2.0, includes project's public items
229 - - [ ] Feed updates when new item published
230 -
231 - ### Blog Posts
232 -
233 - - [ ] Create blog post on a project — title, slug, body (markdown)
234 - - [ ] Blog post renders at `/p/{slug}/blog/{post_slug}`
235 - - [ ] Blog post appears in project RSS feed
236 - - [ ] Edit blog post — changes saved and visible
237 - - [ ] Delete blog post — removed from project page and RSS
238 -
239 - ### Two-Factor Authentication
240 -
241 - - [ ] Enable TOTP 2FA — QR code and secret displayed
242 - - [ ] Login with 2FA enabled — prompted for TOTP code after password
243 - - [ ] Correct TOTP code — login succeeds
244 - - [ ] Wrong TOTP code — login rejected
245 - - [ ] Backup codes — one works, same code cannot be reused
246 - - [ ] Disable 2FA — login no longer prompts for code
247 -
248 - ### Passkeys (WebAuthn)
249 -
250 - - [ ] Register passkey from dashboard security section
251 - - [ ] Login with passkey — bypasses password
252 - - [ ] Remove passkey — can no longer use it to login
253 -
254 - ### Git Browser
255 -
256 - - [ ] `/git/{username}/{repo}` — file tree renders
257 - - [ ] Click file — blob view with syntax highlighting
258 - - [ ] `/git/{username}/{repo}/commits` — commit log renders
259 - - [ ] Click commit — diff view renders
260 - - [ ] `/git/{username}/{repo}/blame/{path}` — blame view renders
261 - - [ ] Clone URL displayed and correct (`ssh.makenot.work`)
262 -
263 - ### Data Export
264 -
265 - - [ ] Projects export (`POST /api/export/projects`) — downloads JSON
266 - - [ ] Sales export (`POST /api/export/sales`) — downloads CSV
267 - - [ ] Purchases export (`POST /api/export/purchases`) — downloads CSV
268 - - [ ] Exported data is accurate (spot-check a few records)
269 -
270 - ---
271 -
272 - ## P2 — Edge Cases + Security
273 -
274 - ### Access Control
275 -
276 - - [ ] Cannot view another user's dashboard (`/dashboard` only shows your data)
277 - - [ ] Cannot edit another user's project (`PUT /api/projects/{id}` — 403/404)
278 - - [ ] Cannot delete another user's item (`DELETE /api/items/{id}` — 403/404)
279 - - [ ] Cannot access paid item content without purchase
280 - - [ ] Cannot access private/draft items via direct URL
281 - - [ ] Admin routes (`/admin/*`) return 403 for non-admin users
282 - - [ ] Stripe disconnect (`DELETE /api/users/me/stripe`) only affects your account
283 -
284 - ### Rate Limiting
285 -
286 - - [ ] Hit `/login` rapidly (>5 times) — returns 429 Too Many Requests
287 - - [ ] Hit `/api/upload/presign` rapidly (>10 times) — returns 429
288 - - [ ] Hit `/api/export/projects` rapidly (>3 times) — returns 429
289 - - [ ] Rate limits reset after the window passes
290 -
291 - ### CSRF
292 -
293 - - [ ] Submit a POST/PUT/DELETE without CSRF token — rejected
294 - - [ ] Submit with invalid CSRF token — rejected
295 - - [ ] Normal form submissions with valid token — succeed
296 - - [ ] Exempt routes work without CSRF: `/login`, `/join`, `/logout`, `/stripe/webhook`
297 -
298 - ### Input Validation
299 -
300 - - [ ] XSS attempt in username/bio/project fields — HTML escaped in output
301 - - [ ] SQL injection attempt in search/form fields — no errors, input treated as text
302 - - [ ] Overlong input (10k+ chars in text fields) — rejected or truncated gracefully
303 - - [ ] Negative price on item — rejected
304 - - [ ] Zero-length required fields — rejected with validation error
305 - - [ ] Markdown rendering sanitized (no script tags, no raw HTML that could execute)
306 -
307 - ### Account Deletion
308 -
309 - - [ ] Request deletion (`POST /api/account/request-deletion`) — confirmation email sent
310 - - [ ] Confirmation link (`/confirm-delete?user=...&expires=...&sig=...`) — deletes account
311 - - [ ] After deletion, login with old credentials fails
312 - - [ ] Deleted user's public pages return 404
313 - - [ ] Purchases by deleted user are preserved (preserve_purchases migration)
314 -
315 - ### Error Pages
316 -
317 - - [ ] Hit nonexistent route — custom 404 page renders
318 - - [ ] Error templates render correctly (check `/deploy/error-pages/`)
319 -
320 - ---
321 -
322 - ## Infrastructure Verification
323 -
324 - > Run these checks on the production server after deploy.
325 -
326 - ### DNS + HTTPS
327 -
328 - - [ ] A record points to server IP (`dig makenot.work`)
329 - - [ ] HTTPS certificate valid (Cloudflare Origin CA, 15yr wildcard)
330 - - [ ] `Strict-Transport-Security` header present
331 - - [ ] `http://makenot.work` redirects to `https://makenot.work`
332 - - [ ] `www.makenot.work` redirects to `makenot.work` (if configured)
333 -
334 - ### Security Headers
335 -
336 - - [ ] `Content-Security-Policy` header present
337 - - [ ] `X-Frame-Options: DENY` or `SAMEORIGIN`
338 - - [ ] `X-Content-Type-Options: nosniff`
339 - - [ ] `Referrer-Policy` header present
340 - - [ ] `Permissions-Policy` header present
341 - - [ ] Check headers: `curl -I https://makenot.work`
342 -
343 - ### Systemd Service
344 -
345 - - [ ] Service running: `systemctl status makenotwork`
346 - - [ ] Restart policy active: `Restart=on-failure` in service file
347 - - [ ] Service starts on boot: `systemctl is-enabled makenotwork`
348 - - [ ] Security hardening active (check `ProtectSystem`, `NoNewPrivileges`, etc. in service file)
349 - - [ ] Test restart: `systemctl restart makenotwork` — comes back healthy
350 -
351 - ### Database
352 -
353 - - [ ] Migrations applied: all 45 migrations (`cargo sqlx migrate info` or check schema)
354 - - [ ] Connection healthy: `GET /health` shows database green
355 - - [ ] Demo seed data removed (migrations 011-014, 016-017 are seed data — verify no test users/items in production)
356 - - [ ] pg_trgm extension installed (required for search)
357 -
358 - ### Backups
359 -
360 - - [ ] Cron job configured: `crontab -l` shows daily 3 AM backup
361 - - [ ] Manual backup works: `bash deploy/backup-db.sh`
362 - - [ ] Backup file created and non-empty in backup directory
363 - - [ ] Test restore to a scratch database (see `RECOVERY.md`)
364 - - [ ] 30-day retention — old backups cleaned up
365 -
366 - ### Environment
367 -
368 - - [ ] `.env` file permissions: `600` (owner read/write only)
369 - - [ ] No test keys in production (grep for `sk_test_`, `pk_test_`)
370 - - [ ] `SIGNING_SECRET` is set and is a strong random value
371 - - [ ] `HOST_URL` is `https://makenot.work` (not localhost)
372 - - [ ] `STRIPE_WEBHOOK_SECRET` matches the webhook configured in Stripe dashboard
373 - - [ ] `POSTMARK_TOKEN` is set (not console mode)
374 - - [ ] `ADMIN_USER_ID` is set to the correct UUID
375 -
376 - ### Health Endpoint
377 -
378 - - [ ] `GET /health` returns 200
379 - - [ ] Database: connected, shows table counts
380 - - [ ] Sessions: store active
381 - - [ ] S3: configured
382 - - [ ] Stripe: configured, **live mode** (not test)
383 - - [ ] Email: Postmark (not console)
384 -
385 - ### Logs
386 -
387 - - [ ] Server logs flowing: `journalctl -u makenotwork -f`
388 - - [ ] Caddy logs flowing: `journalctl -u caddy -f`
389 - - [ ] No errors or panics on startup
390 - - [ ] A test request shows up in logs
391 -
392 - ### Firewall
393 -
394 - - [ ] Ports 80, 443 open to all (required for custom domains + on-demand TLS): `ufw status`
395 - - [ ] Port 22 open (SSH)
396 - - [ ] All other ports blocked
397 - - [ ] makenot.work protected by Cloudflare mTLS even with open ports
398 -
399 - ---
400 -
401 - ## Sign-Off
402 -
403 - | Field | Value |
404 - |-------|-------|
405 - | Date | |
406 - | Tester | |
407 - | Environment | local / staging / production |
408 - | Automated tests passing | yes / no |
409 - | P0 result | pass / fail |
410 - | P1 result | pass / fail |
411 - | P2 result | pass / fail / skipped |
412 - | Infrastructure result | pass / fail / N/A |
413 - | Notes | |
@@ -1,56 +0,0 @@
1 - # Makenotwork systemd service
2 - # Place in /etc/systemd/system/makenotwork.service
3 - #
4 - # Commands:
5 - # sudo systemctl daemon-reload
6 - # sudo systemctl enable makenotwork
7 - # sudo systemctl start makenotwork
8 - # sudo systemctl status makenotwork
9 - # journalctl -u makenotwork -f
10 -
11 - [Unit]
12 - Description=Makenotwork - Fair creator platform
13 - Documentation=https://makenot.work/docs
14 - After=network.target postgresql.service
15 - Requires=postgresql.service
16 -
17 - [Service]
18 - Type=simple
19 - User=makenotwork
20 - Group=makenotwork
21 - WorkingDirectory=/opt/makenotwork
22 - ExecStart=/opt/makenotwork/makenotwork
23 - Restart=always
24 - RestartSec=5
25 -
26 - # Environment file with secrets
27 - EnvironmentFile=/opt/makenotwork/.env
28 - Environment=HOME=/opt/makenotwork
29 -
30 - # Security hardening
31 - NoNewPrivileges=true
32 - ProtectSystem=strict
33 - ProtectHome=true
34 - PrivateTmp=true
35 - ReadWritePaths=/opt/makenotwork /opt/git
36 - RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
37 - RestrictNamespaces=true
38 - RestrictRealtime=true
39 - RestrictSUIDSGID=true
40 - LockPersonality=true
41 - ProtectKernelTunables=true
42 - ProtectKernelModules=true
43 - ProtectControlGroups=true
44 - SystemCallArchitectures=native
45 -
46 - # Resource limits
47 - LimitNOFILE=65535
48 - MemoryMax=512M
49 -
50 - # Logging (goes to journald)
51 - StandardOutput=journal
52 - StandardError=journal
53 - SyslogIdentifier=makenotwork
54 -
55 - [Install]
56 - WantedBy=multi-user.target
D docs/audit.md -132
D docs/cli.md -617
D docs/todo.md -318
A pom/Cargo.lock +3672
A pom/LICENSE +120
A pom/pom.toml +16
A pom/src/db.rs +1376
D seed_demo.sh -12
D seed_demo.sql -243
D src/auth.rs -489
D src/config.rs -435
D src/csrf.rs -296
D src/db/enums.rs -1286
D src/db/ota.rs -159
D src/error.rs -233
D src/lib.rs -147
D src/main.rs -340
D src/rss.rs -238
D src/wordlist.rs -2056
D static/mnw.js -244