max / makenotwork
1396 files changed,
+141366 insertions,
-128923 deletions
| @@ -0,0 +1,35 @@ | |||
| 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 |
| @@ -23,7 +23,12 @@ Thumbs.db | |||
| 23 | 23 | .sqlx/ | |
| 24 | 24 | ||
| 25 | 25 | # Generated template partial (build.rs output) | |
| 26 | - | server_code/makenotwork/templates/_head_assets.html | |
| 26 | + | templates/_head_assets.html | |
| 27 | 27 | ||
| 28 | 28 | # Generated rustdoc output | |
| 29 | 29 | rustdoc-out/ | |
| 30 | + | ||
| 31 | + | # Nested repos (separate git projects colocated in MNW/) | |
| 32 | + | /multithreaded/ | |
| 33 | + | /pom/ | |
| 34 | + | /mnw-cli/ |
| @@ -1,24 +1,179 @@ | |||
| 1 | 1 | # Makenotwork | |
| 2 | 2 | ||
| 3 | - | Fair creator platform. Rust/Axum backend, HTMX frontend, PostgreSQL, Stripe Connect. | |
| 3 | + | Fair creator platform with 0% platform fee (only Stripe's ~3% processing fee). Mission: Prove platforms can be fair — no percentage cuts, no lock-in, everything exportable. Stage: Private alpha — live at makenot.work. | |
| 4 | + | ||
| 5 | + | ## Critical Rules | |
| 6 | + | ||
| 7 | + | **No Emoji.** The diamond mark (Young Serif period glyph) is the only graphic element. No emoji anywhere. | |
| 8 | + | ||
| 9 | + | **Typography (Three-Tier System):** | |
| 10 | + | - **H1**: Young Serif (wordmark, page/section headings) | |
| 11 | + | - **H2/H3/meta**: IBM Plex Mono (subheadings, taglines, footer) | |
| 12 | + | - **Body**: Lato (paragraphs, lists, table content) | |
| 13 | + | ||
| 14 | + | **Colors:** | |
| 15 | + | - Background: warm beige `#ede8e1` — never pure white | |
| 16 | + | - Text: dark charcoal-brown `#3d3530` — never pure black | |
| 17 | + | - Accent: violet `#6c5ce7` — diamond mark only, used sparingly | |
| 18 | + | ||
| 19 | + | **Platform Principles:** | |
| 20 | + | - **0% platform fee** — Stripe's ~3% processing fee is the only cost | |
| 21 | + | - **No lock-in** — Full data export, month-to-month cancellation | |
| 22 | + | - **Source available** — PolyForm Noncommercial 1.0.0 | |
| 23 | + | ||
| 24 | + | ## Pricing Tiers | |
| 25 | + | ||
| 26 | + | - **Basic** — $10/mo (text, all base features) | |
| 27 | + | - **Small Files** — $20/mo (audio, software, plugins, small downloads) | |
| 28 | + | - **Big Files** — $30/mo (video, courses, large downloads) | |
| 29 | + | - **Streaming** — $40/mo (live streaming + everything above) | |
| 30 | + | ||
| 31 | + | ## Ecosystem | |
| 32 | + | ||
| 33 | + | This directory contains the MNW server and related ecosystem projects (each a separate git repo): | |
| 34 | + | ||
| 35 | + | | Project | Path | Description | | |
| 36 | + | |---------|------|-------------| | |
| 37 | + | | MNW Server | `.` (crate root) | Rust/Axum backend, HTMX frontend, PostgreSQL, Stripe Connect | | |
| 38 | + | | Multithreaded | `multithreaded/` | Forum software integrated with MNW (Rust/Axum/PostgreSQL, MNW OAuth) | | |
| 39 | + | | PoM | `pom/` | Production operations monitor (health checks, TLS tracking, email alerts) | | |
| 40 | + | | mnw-cli | `mnw-cli/` | CLI tool for MNW platform | | |
| 41 | + | ||
| 42 | + | Shared libraries live at `../Shared/` (docengine, tagtree, synckit-client, theme-common, themes). | |
| 4 | 43 | ||
| 5 | 44 | ## Repository Layout | |
| 6 | 45 | ||
| 7 | 46 | ``` | |
| 8 | - | server_code/makenotwork/ # Main Rust application | |
| 9 | - | src/ # Application source | |
| 10 | - | migrations/ # SQLx migrations (numbered, auto-applied on boot) | |
| 11 | - | templates/ # Askama HTML templates | |
| 12 | - | static/ # CSS, JS, fonts, images | |
| 13 | - | tests/ # Integration tests (workflows/, load/, harness/) | |
| 14 | - | deploy/ # Deployment scripts and config files | |
| 15 | - | deploy.sh # Cross-compile + upload + restart | |
| 16 | - | makenotwork.service # systemd unit file | |
| 17 | - | Caddyfile # Reverse proxy config | |
| 18 | - | backup-db.sh # DB backup script | |
| 19 | - | error-pages/ # Custom 404/500/502 pages | |
| 20 | - | docs/ # Documentation (public/ and unpublished/) | |
| 47 | + | MNW/ # Repository root = crate root | |
| 48 | + | src/ # Application source | |
| 49 | + | migrations/ # SQLx migrations (numbered, auto-applied on boot) | |
| 50 | + | templates/ # Askama HTML templates | |
| 51 | + | static/ # CSS, JS, fonts, images | |
| 52 | + | tests/ # Integration tests (workflows/, load/, harness/) | |
| 53 | + | deploy/ # Deployment scripts and config files | |
| 54 | + | deploy.sh # Cross-compile + upload + restart | |
| 55 | + | makenotwork.service # systemd unit file | |
| 56 | + | Caddyfile # Reverse proxy config | |
| 57 | + | backup-db.sh # DB backup script | |
| 58 | + | error-pages/ # Custom 404/500/502 pages | |
| 59 | + | site-docs/ # DocEngine content (public/ and unpublished/) | |
| 60 | + | docs/ # Project docs (todo, audit, architecture, etc.) | |
| 61 | + | ``` | |
| 62 | + | ||
| 63 | + | ## Code Patterns | |
| 64 | + | ||
| 21 | 65 | ``` | |
| 66 | + | src/ | |
| 67 | + | ├── main.rs Entry point | |
| 68 | + | ├── lib.rs Library root | |
| 69 | + | ├── config.rs Configuration | |
| 70 | + | ├── constants.rs Shared constants | |
| 71 | + | ├── error.rs Error handling | |
| 72 | + | ├── auth.rs Authentication | |
| 73 | + | ├── csrf.rs CSRF protection | |
| 74 | + | ├── db/ Database queries (module) | |
| 75 | + | ├── docs.rs Documentation rendering | |
| 76 | + | ├── email/ Email handling (directory module) | |
| 77 | + | ├── git.rs Git source browser logic | |
| 78 | + | ├── helpers.rs Shared helper functions | |
| 79 | + | ├── markdown.rs Markdown rendering | |
| 80 | + | ├── monitor.rs Health monitoring | |
| 81 | + | ├── payments.rs Stripe integration | |
| 82 | + | ├── rss.rs RSS feed generation | |
| 83 | + | ├── scanning/ File scanning (ClamAV, YARA, hash lookup) | |
| 84 | + | ├── scheduler.rs Background task scheduler | |
| 85 | + | ├── sentry_layer.rs Sentry error tracking integration | |
| 86 | + | ├── storage.rs S3 storage | |
| 87 | + | ├── synckit_auth.rs SyncKit JWT auth | |
| 88 | + | ├── templates/ Askama templates (directory module) | |
| 89 | + | ├── types/ Shared types (directory module) | |
| 90 | + | ├── validation.rs Input validation | |
| 91 | + | ├── wordlist.rs Wordlist for invite codes | |
| 92 | + | └── routes/ | |
| 93 | + | ├── mod.rs | |
| 94 | + | ├── admin.rs Admin panel | |
| 95 | + | ├── auth.rs Login, signup, logout | |
| 96 | + | ├── api/ JSON API endpoints (directory module) | |
| 97 | + | ├── git.rs Git source browser routes | |
| 98 | + | ├── git_issues.rs Git issue tracker routes | |
| 99 | + | ├── pages/ HTML page routes (directory module) | |
| 100 | + | │ ├── mod.rs Route composer | |
| 101 | + | │ ├── public/ Public-facing pages (directory module) | |
| 102 | + | │ ├── dashboard/ Creator dashboard + HTMX tabs (directory module) | |
| 103 | + | │ ├── email_actions.rs Email link handlers | |
| 104 | + | │ ├── feeds.rs RSS feeds | |
| 105 | + | │ └── blog.rs Blog pages | |
| 106 | + | ├── oauth.rs OAuth provider routes | |
| 107 | + | ├── postmark.rs Postmark webhook handler | |
| 108 | + | ├── storage.rs File upload/download | |
| 109 | + | ├── stripe/ Stripe webhooks + connect (directory module) | |
| 110 | + | └── synckit.rs SyncKit API endpoints | |
| 111 | + | ``` | |
| 112 | + | ||
| 113 | + | Route files should stay under 500 lines. When a route module grows beyond that, split it into a directory module grouped by domain. | |
| 114 | + | ||
| 115 | + | ## Key Patterns | |
| 116 | + | ||
| 117 | + | - `impl_str_enum!` macro for enum <-> string (Display, FromStr, sqlx Type/Encode/Decode) | |
| 118 | + | - `define_pg_uuid_id!` macro for newtype UUID ID wrappers | |
| 119 | + | - `EnvironmentFile=/opt/makenotwork/.env` for all secrets | |
| 120 | + | - SQLx compile-time checked queries; migrations auto-run on boot | |
| 121 | + | - HTMX responses return HTML fragments; JSON fallback for non-HTMX requests | |
| 122 | + | - Tests: each integration test creates/drops its own PostgreSQL database | |
| 123 | + | - **Rust 2024 edition** (Rust 1.85+) | |
| 124 | + | - `site-docs/` = DocEngine content (public/ and unpublished/). Project docs are in `docs/`. | |
| 125 | + | ||
| 126 | + | ## Versioning | |
| 127 | + | ||
| 128 | + | - Semver in `Cargo.toml` (`env!("CARGO_PKG_VERSION")` compiles it into the binary for Sentry release strings) | |
| 129 | + | - **Before every deploy to production**, ask the user what version to set — never auto-bump | |
| 130 | + | - Version bump = edit `Cargo.toml` version field before building | |
| 131 | + | ||
| 132 | + | --- | |
| 133 | + | ||
| 134 | + | ## MNW SyncKit | |
| 135 | + | ||
| 136 | + | Developer infrastructure for indie apps, hosted on Makenotwork. | |
| 137 | + | ||
| 138 | + | ### Services | |
| 139 | + | ||
| 140 | + | - **Cloud Sync** — Push/pull changelog sync with E2E encryption, device management, conflict resolution | |
| 141 | + | - **OTA Updates** — App auto-update server (Tauri-compatible protocol), no app store dependency | |
| 142 | + | ||
| 143 | + | ### Design Philosophy | |
| 144 | + | ||
| 145 | + | - **General-purpose first** — API and SDK decisions should make sense for any app, not just GO | |
| 146 | + | - **E2E encrypted by default** — Server stores only encrypted blobs, never plaintext user data | |
| 147 | + | - **Bring your own schema** — Table names, row IDs, and data shapes are opaque to the server | |
| 148 | + | - **Auth via MNW accounts** — Users authenticate with their Makenot.work credentials | |
| 149 | + | ||
| 150 | + | ### Components | |
| 151 | + | ||
| 152 | + | | Component | Location | Role | | |
| 153 | + | |-----------|----------|------| | |
| 154 | + | | Server API | `src/routes/synckit.rs` | Axum endpoints (auth, push/pull, devices, keys) | | |
| 155 | + | | Server DB | `src/db/synckit.rs` | PostgreSQL queries (sync_apps, sync_devices, sync_log, sync_keys) | | |
| 156 | + | | Server Auth | `src/synckit_auth.rs` | JWT token creation + extraction | | |
| 157 | + | | Client SDK | `../Shared/synckit-client/` | Rust crate — HTTP client, E2E crypto, keychain storage | | |
| 158 | + | | Integration tests | `tests/workflows/synckit.rs` | 7 tests covering auth, devices, push/pull, keys, validation | | |
| 159 | + | ||
| 160 | + | ### Consumers | |
| 161 | + | ||
| 162 | + | | App | What it syncs | Status | | |
| 163 | + | |-----|---------------|--------| | |
| 164 | + | | GoingsOn | Tasks, projects, events, contacts, emails | Implemented | | |
| 165 | + | | Balanced Breakfast | Configs, feed sources, plugin manifests | Integrated | | |
| 166 | + | | audiofiles | Sample metadata, tags, VFS mappings | Integrated | | |
| 167 | + | ||
| 168 | + | --- | |
| 169 | + | ||
| 170 | + | ## CI | |
| 171 | + | ||
| 172 | + | MNW CI runs self-hosted on astra (`deploy/run-ci.sh` — check, test, clippy, audit). GO, BB, and AF still have `.build.yml` manifests for builds.sr.ht (Arch Linux + Rust). Sourcehut (`https://sr.ht/~maxmj/`) remains active as a git mirror. MNW has a built-in git browser (G1, `git2`-based) that reads bare repos from disk. | |
| 173 | + | ||
| 174 | + | ## Infrastructure Diagrams | |
| 175 | + | ||
| 176 | + | Mermaid diagrams documenting the full MNW ecosystem live at `../_meta/diagrams/infra/`. See `index.md` for the table of contents, or open `viewer.html` in a browser to view all 52 diagrams rendered. Keep diagrams in sync when making infrastructure changes. | |
| 22 | 177 | ||
| 23 | 178 | ## Production Server (hetzner) | |
| 24 | 179 | ||
| @@ -53,7 +208,7 @@ Hetzner VPS, x86_64 Linux. Tailscale hostname: `alpha-west-1` (IP: `100.120.174. | |||
| 53 | 208 | ||
| 54 | 209 | ### Deployment | |
| 55 | 210 | ||
| 56 | - | From `server_code/makenotwork/`: | |
| 211 | + | From the `MNW/` directory: | |
| 57 | 212 | ```sh | |
| 58 | 213 | ./deploy/deploy.sh # Full: build + config + binary + restart | |
| 59 | 214 | ./deploy/deploy.sh --quick # Build + binary + restart (no config upload) | |
| @@ -106,15 +261,6 @@ psql -t -c "SELECT datname FROM pg_database WHERE datname LIKE 'mnw_test_%';" po | |||
| 106 | 261 | - **Cloudflare** — DNS, CDN, DDoS protection | |
| 107 | 262 | - **Fastmail** — business email (support@, legal@, max@) | |
| 108 | 263 | ||
| 109 | - | ## Key Patterns | |
| 110 | - | ||
| 111 | - | - `impl_str_enum!` macro for enum ↔ string (Display, FromStr, sqlx Type/Encode/Decode) | |
| 112 | - | - `define_pg_uuid_id!` macro for newtype UUID ID wrappers | |
| 113 | - | - `EnvironmentFile=/opt/makenotwork/.env` for all secrets | |
| 114 | - | - SQLx compile-time checked queries; migrations auto-run on boot | |
| 115 | - | - HTMX responses return HTML fragments; JSON fallback for non-HTMX requests | |
| 116 | - | - Tests: each integration test creates/drops its own PostgreSQL database | |
| 117 | - | ||
| 118 | 264 | ## Testing | |
| 119 | 265 | ||
| 120 | 266 | ```sh |
| @@ -0,0 +1,7523 @@ | |||
| 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
| @@ -0,0 +1,111 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "makenotwork" | |
| 3 | + | version = "0.3.17" | |
| 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 | + | tower = "0.5.3" | |
| 18 | + | tower-http = { version = "0.6.8", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] } | |
| 19 | + | tracing = "0.1.44" | |
| 20 | + | tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } | |
| 21 | + | ||
| 22 | + | # Templates | |
| 23 | + | askama = "0.13.1" | |
| 24 | + | ||
| 25 | + | # Environment & Configuration | |
| 26 | + | dotenvy = "0.15.7" | |
| 27 | + | ||
| 28 | + | # Database | |
| 29 | + | sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] } | |
| 30 | + | uuid = { version = "1.20.0", features = ["v4", "serde"] } | |
| 31 | + | chrono = { version = "0.4.43", features = ["serde"] } | |
| 32 | + | ||
| 33 | + | # Authentication | |
| 34 | + | argon2 = "0.5.3" | |
| 35 | + | tower-sessions = { version = "0.14.0", features = ["axum-core"] } | |
| 36 | + | tower-sessions-sqlx-store = { version = "0.15.0", features = ["postgres"] } | |
| 37 | + | ||
| 38 | + | # Concurrent hash map (session touch cache) | |
| 39 | + | dashmap = "6" | |
| 40 | + | ||
| 41 | + | # Rate Limiting | |
| 42 | + | tower_governor = "0.6.0" | |
| 43 | + | governor = "0.8.1" | |
| 44 | + | ||
| 45 | + | # JWT (SyncKit) | |
| 46 | + | jsonwebtoken = "9.3.1" | |
| 47 | + | ||
| 48 | + | # TOTP / 2FA | |
| 49 | + | totp-rs = { version = "5.7", features = ["qr"] } | |
| 50 | + | ||
| 51 | + | # WebAuthn / Passkeys | |
| 52 | + | webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "conditional-ui"] } | |
| 53 | + | webauthn-rs-proto = "0.5" | |
| 54 | + | ||
| 55 | + | # OpenSSL (transitive dep from git2, webauthn-rs — vendored for cross-compilation) | |
| 56 | + | openssl = { version = "0.10", features = ["vendored"] } | |
| 57 | + | ||
| 58 | + | # Security | |
| 59 | + | rand = "0.8.5" | |
| 60 | + | hmac = "0.12.1" | |
| 61 | + | sha1 = "0.10.6" | |
| 62 | + | sha2 = "0.10.9" | |
| 63 | + | hex = "0.4.3" | |
| 64 | + | base64 = "0.22.1" | |
| 65 | + | ||
| 66 | + | # File scanning | |
| 67 | + | infer = "0.19" | |
| 68 | + | goblin = "0.10" | |
| 69 | + | zip = "8.2" | |
| 70 | + | yara-x = "1.13" | |
| 71 | + | ||
| 72 | + | # CLI | |
| 73 | + | clap = { version = "4", features = ["derive"] } | |
| 74 | + | ||
| 75 | + | # Error handling | |
| 76 | + | thiserror = "2.0.18" | |
| 77 | + | anyhow = "1.0.101" | |
| 78 | + | ||
| 79 | + | # Markdown rendering + documentation engine | |
| 80 | + | docengine = { path = "../Shared/docengine", features = ["doc-loader", "frontmatter"] } | |
| 81 | + | ||
| 82 | + | # Tag standard | |
| 83 | + | tagtree = { path = "../Shared/tagtree" } | |
| 84 | + | ||
| 85 | + | # Git source browser | |
| 86 | + | git2 = { version = "0.20", features = ["vendored-libgit2"] } | |
| 87 | + | syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "html", "regex-fancy"] } | |
| 88 | + | regex = "1" | |
| 89 | + | semver = "1" | |
| 90 | + | ||
| 91 | + | # S3 Storage | |
| 92 | + | aws-sdk-s3 = "1.119.0" | |
| 93 | + | aws-config = { version = "1.8.14", features = ["behavior-version-latest"] } | |
| 94 | + | ||
| 95 | + | # Stripe Payments | |
| 96 | + | async-stripe = { version = "0.37.3", features = ["runtime-tokio-hyper", "checkout", "connect", "billing"] } | |
| 97 | + | reqwest = { version = "0.12", features = ["json", "cookies"] } | |
| 98 | + | urlencoding = "2.1.3" | |
| 99 | + | ||
| 100 | + | # URL parsing | |
| 101 | + | url = "2.5.8" | |
| 102 | + | ||
| 103 | + | [[bin]] | |
| 104 | + | name = "mnw-admin" | |
| 105 | + | path = "src/bin/mnw-admin.rs" | |
| 106 | + | ||
| 107 | + | [dev-dependencies] | |
| 108 | + | tower = { version = "0.5.3", features = ["util"] } | |
| 109 | + | http-body-util = "0.1" | |
| 110 | + | webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } | |
| 111 | + | tempfile = "3" |
| @@ -12,7 +12,7 @@ Built with Rust (2024 edition), Axum, PostgreSQL, Askama templates, and HTMX. | |||
| 12 | 12 | ||
| 13 | 13 | ## Build and Run | |
| 14 | 14 | ||
| 15 | - | All commands run from `server_code/makenotwork/`: | |
| 15 | + | All commands run from the `MNW/` directory: | |
| 16 | 16 | ||
| 17 | 17 | ```sh | |
| 18 | 18 | # Development | |
| @@ -33,7 +33,7 @@ Production deployment uses `cargo zigbuild` for cross-compilation to x86_64 Linu | |||
| 33 | 33 | ## Project Structure | |
| 34 | 34 | ||
| 35 | 35 | ``` | |
| 36 | - | server_code/makenotwork/ | |
| 36 | + | MNW/ | |
| 37 | 37 | src/ | |
| 38 | 38 | main.rs Entry point | |
| 39 | 39 | lib.rs Library root |
| @@ -0,0 +1,63 @@ | |||
| 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 | + | } |
| @@ -0,0 +1,209 @@ | |||
| 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 | + | } |
| @@ -0,0 +1,172 @@ | |||
| 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 | + | ``` |
| @@ -0,0 +1,352 @@ | |||
| 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 | + | ``` |
| @@ -0,0 +1,57 @@ | |||
| 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" |
| @@ -0,0 +1,35 @@ | |||
| 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----- |
| @@ -0,0 +1,136 @@ | |||
| 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 | + | restart_app() { | |
| 101 | + | echo "[restart] Restarting makenotwork..." | |
| 102 | + | ssh $SERVER "systemctl restart makenotwork" | |
| 103 | + | sleep 1 | |
| 104 | + | echo "" | |
| 105 | + | ssh $SERVER "systemctl status makenotwork --no-pager" | |
| 106 | + | echo "" | |
| 107 | + | echo "[restart] Verifying app responds..." | |
| 108 | + | ssh $SERVER "curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:3000" | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | case "${1:-full}" in | |
| 112 | + | --quick) | |
| 113 | + | echo "=== Quick Deploy ===" | |
| 114 | + | build_binary | |
| 115 | + | upload_binary | |
| 116 | + | restart_app | |
| 117 | + | ;; | |
| 118 | + | --config) | |
| 119 | + | echo "=== Config Deploy ===" | |
| 120 | + | upload_config | |
| 121 | + | ;; | |
| 122 | + | full|"") | |
| 123 | + | echo "=== Full Deploy ===" | |
| 124 | + | build_binary | |
| 125 | + | upload_config | |
| 126 | + | upload_binary | |
| 127 | + | restart_app | |
| 128 | + | ;; | |
| 129 | + | *) | |
| 130 | + | echo "Usage: $0 [--quick|--config]" | |
| 131 | + | exit 1 | |
| 132 | + | ;; | |
| 133 | + | esac | |
| 134 | + | ||
| 135 | + | echo "" | |
| 136 | + | echo "=== Deploy Complete ===" |
| @@ -0,0 +1,75 @@ | |||
| 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> |
| @@ -0,0 +1,83 @@ | |||
| 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> |
| @@ -0,0 +1,83 @@ | |||
| 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> |
| @@ -0,0 +1,10 @@ | |||
| 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 |
| @@ -0,0 +1,40 @@ | |||
| 1 | + | #!/bin/bash | |
| 2 | + | # Generate rustdoc for library crates (synckit-client, docengine, tagtree). | |
| 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") | |
| 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 |
| @@ -0,0 +1,413 @@ | |||
| 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 | | |
| @@ -0,0 +1,56 @@ | |||
| 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 |