max / makenotwork
2026 files changed,
+238199 insertions,
-148016 deletions
| @@ -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 |
| @@ -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/ |
| @@ -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
| @@ -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" |
| @@ -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 | ``` |
| @@ -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 |