max / makenotwork
102 files changed,
+7547 insertions,
-979 deletions
| @@ -3453,7 +3453,7 @@ | |||
| 3453 | 3453 | ||
| 3454 | 3454 | [[package]] | |
| 3455 | 3455 | name = "makenotwork" | |
| 3456 | - | version = "0.1.5" | |
| 3456 | + | version = "0.1.6" | |
| 3457 | 3457 | dependencies = [ | |
| 3458 | 3458 | "ammonia", | |
| 3459 | 3459 | "anyhow", | |
| @@ -4601,9 +4601,9 @@ | |||
| 4601 | 4601 | ||
| 4602 | 4602 | [[package]] | |
| 4603 | 4603 | name = "quinn-proto" | |
| 4604 | - | version = "0.11.13" | |
| 4604 | + | version = "0.11.14" | |
| 4605 | 4605 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4606 | - | checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" | |
| 4606 | + | checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" | |
| 4607 | 4607 | dependencies = [ | |
| 4608 | 4608 | "bytes", | |
| 4609 | 4609 | "getrandom 0.3.4", |
| @@ -23,8 +23,7 @@ | |||
| 23 | 23 | - **Pay-what-you-want**: Buyer chooses the amount, optional minimum price | |
| 24 | 24 | - **Subscriptions**: Monthly recurring tiers per project with Stripe billing (multiple tiers, active/inactive toggle) | |
| 25 | 25 | - **License keys**: Auto-generated on purchase, configurable activation limits, machine tracking, public validation endpoint for software phone-home | |
| 26 | - | - **Discount codes**: Percentage or fixed-amount, item-scoped or seller-wide, usage limits, expiration dates, auto-apply via URL parameter | |
| 27 | - | - **Download codes**: Single-use codes for free access, optional max uses and expiration | |
| 26 | + | - **Promo codes**: Unified code system supporting percentage/fixed discounts, free access grants, and free trial periods for subscriptions. Item-scoped or project-wide, usage limits, expiration dates, auto-apply via URL parameter | |
| 28 | 27 | ||
| 29 | 28 | ### Discovery & Organization | |
| 30 | 29 | ||
| @@ -39,14 +38,17 @@ | |||
| 39 | 38 | - **Library**: All purchased content in one place, download files, view license keys | |
| 40 | 39 | - **Audio streaming**: In-browser player with chapter navigation | |
| 41 | 40 | - **Text reader**: Clean typography, reading time estimate | |
| 42 | - | - **Contact sharing**: Opt-in email sharing with creators at purchase time | |
| 41 | + | - **Contact sharing**: Opt-in email sharing with creators at purchase time, revocable by the buyer | |
| 43 | 42 | - **Free accounts**: Fans never pay for the platform, only for content they choose to buy | |
| 43 | + | - **Email notifications**: Sale alerts, follower alerts, new release announcements, new device login warnings (each individually toggleable) | |
| 44 | 44 | ||
| 45 | 45 | ### Creator Dashboard | |
| 46 | 46 | ||
| 47 | 47 | - **Project management**: Overview with revenue and sales stats, content tab, blog tab, settings, subscriptions | |
| 48 | - | - **Item management**: Settings, content editor, versions, tags, license keys, download codes, chapters | |
| 48 | + | - **Item management**: Settings, content editor, versions, tags, license keys, promo codes, chapters | |
| 49 | 49 | - **Transactions**: Full purchase and sales history, filterable | |
| 50 | + | - **Contacts**: View fans who shared their email at purchase, with purchase count and total spent | |
| 51 | + | - **Broadcasts**: Send plain-text email updates to all your followers (rate-limited to one per 24 hours) | |
| 50 | 52 | - **Data export**: All projects, items, blog posts, sales (CSV), and purchases (CSV) downloadable anytime | |
| 51 | 53 | - **Custom links**: Add external links to your profile | |
| 52 | 54 | ||
| @@ -74,10 +76,12 @@ | |||
| 74 | 76 | - **Creator waitlist**: Invite-only launch with lottery waves and hand-picked approvals | |
| 75 | 77 | - **Admin CLI** (`mnw-admin`): Command-line tool for waitlist management, creator approval, spam flagging, wave execution, stats, user suspension/unsuspension, appeal processing, revenue reports, transaction history, CSV data export, and S3 storage audits -- connects directly to the database, no web UI needed | |
| 76 | 78 | - **Documentation**: Server-rendered from markdown, auto-linked cross-references | |
| 79 | + | - **Transactional email**: Password reset, email verification, purchase receipts, subscription lifecycle, sale and follower notifications via Postmark with bounce/complaint suppression | |
| 80 | + | - **Git source browser**: Browse server-hosted bare repositories with syntax highlighting | |
| 77 | 81 | - **Health monitoring**: Real uptime tracking, database status, service connectivity checks | |
| 78 | - | - **Malware scanning**: ClamAV + VirusTotal hash lookup on file uploads | |
| 82 | + | - **Malware scanning**: ClamAV + YARA rules + MalwareBazaar hash lookup on file uploads | |
| 79 | 83 | - **Creator guide**: 12-page documentation covering the full UX surface area | |
| 80 | - | - **619 automated tests**: Unit, integration, workflow, and health tests | |
| 84 | + | - **621 automated tests**: Unit, integration, workflow, and health tests | |
| 81 | 85 | ||
| 82 | 86 | ### Developer Infrastructure (SyncKit) | |
| 83 | 87 | ||
| @@ -95,12 +99,9 @@ | |||
| 95 | 99 | ||
| 96 | 100 | Near-term work. No timelines because we ship when it's ready. | |
| 97 | 101 | ||
| 98 | - | - **Deploy to production** (server setup complete, final manual backup test remaining) | |
| 99 | - | - **Postmark email setup** (SPF/DKIM, bounce handling for transactional email) | |
| 100 | - | - **Free trial support** for subscription tiers | |
| 101 | - | - **Sale and follower notifications** (email alerts for creators) | |
| 102 | - | - **Contacts dashboard** (view fans who shared their email at purchase) | |
| 103 | - | - **Admin CLI expansion**: Broadcast sending (deferred until Postmark integration) | |
| 102 | + | - **Beta launch**: Final testing pass, onboard first creators | |
| 103 | + | - **Notification preferences UI**: Dashboard settings page for managing email notification toggles | |
| 104 | + | - **Admin CLI expansion**: Broadcast sending to all users for platform announcements | |
| 104 | 105 | ||
| 105 | 106 | --- | |
| 106 | 107 | ||
| @@ -130,7 +131,7 @@ | |||
| 130 | 131 | ||
| 131 | 132 | ### Open source creator tools | |
| 132 | 133 | ||
| 133 | - | Integration with Sourcehut and GitHub for software creators. Sponsor tiers, license display, release hosting, build status badges. | |
| 134 | + | Built-in git hosting with source browser, plus GitHub integration for software creators. Sponsor tiers, license display, release hosting, build status badges. | |
| 134 | 135 | ||
| 135 | 136 | ### Analytics | |
| 136 | 137 |
| @@ -13,7 +13,7 @@ | |||
| 13 | 13 | ## Repository | |
| 14 | 14 | ||
| 15 | 15 | ``` | |
| 16 | - | sr.ht/~maxmj/ | |
| 16 | + | makenot.work/git/maxmj/ | |
| 17 | 17 | ``` | |
| 18 | 18 | ||
| 19 | 19 | Available for review: |
| @@ -170,7 +170,7 @@ | |||
| 170 | 170 | - Check for vulnerabilities | |
| 171 | 171 | - Report issues responsibly | |
| 172 | 172 | ||
| 173 | - | Repository: [sr.ht/~maxmj/](https://sr.ht/~maxmj/) (Sourcehut) | |
| 173 | + | Repository: [makenot.work/git/maxmj/](https://makenot.work/git/maxmj/) | |
| 174 | 174 | ||
| 175 | 175 | --- | |
| 176 | 176 |
| @@ -8,7 +8,7 @@ | |||
| 8 | 8 | use crate::error::{AppError, Result}; | |
| 9 | 9 | ||
| 10 | 10 | /// Email service configuration | |
| 11 | - | #[derive(Clone, Debug)] | |
| 11 | + | #[derive(Clone)] | |
| 12 | 12 | pub struct EmailConfig { | |
| 13 | 13 | /// Postmark API token (optional, logs if not set) | |
| 14 | 14 | pub postmark_token: Option<String>, | |
| @@ -18,6 +18,16 @@ | |||
| 18 | 18 | pub from_name: String, | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | + | impl std::fmt::Debug for EmailConfig { | |
| 22 | + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 23 | + | f.debug_struct("EmailConfig") | |
| 24 | + | .field("postmark_token", &self.postmark_token.as_ref().map(|_| "[REDACTED]")) | |
| 25 | + | .field("from_address", &self.from_address) | |
| 26 | + | .field("from_name", &self.from_name) | |
| 27 | + | .finish() | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 21 | 31 | impl EmailConfig { | |
| 22 | 32 | /// Load email configuration from environment | |
| 23 | 33 | pub fn from_env() -> Self { | |
| @@ -36,11 +46,12 @@ | |||
| 36 | 46 | pub struct EmailClient { | |
| 37 | 47 | config: EmailConfig, | |
| 38 | 48 | http_client: reqwest::Client, | |
| 49 | + | pool: Option<sqlx::PgPool>, | |
| 39 | 50 | } | |
| 40 | 51 | ||
| 41 | 52 | impl EmailClient { | |
| 42 | 53 | /// Create a new email client | |
| 43 | - | pub fn new(config: EmailConfig) -> Self { | |
| 54 | + | pub fn new(config: EmailConfig, pool: Option<sqlx::PgPool>) -> Self { | |
| 44 | 55 | let http_client = reqwest::Client::builder() | |
| 45 | 56 | .timeout(std::time::Duration::from_secs(10)) | |
| 46 | 57 | .build() | |
| @@ -49,6 +60,7 @@ | |||
| 49 | 60 | EmailClient { | |
| 50 | 61 | config, | |
| 51 | 62 | http_client, | |
| 63 | + | pool, | |
| 52 | 64 | } | |
| 53 | 65 | } | |
| 54 | 66 | ||
| @@ -590,6 +602,21 @@ | |||
| 590 | 602 | body: &str, | |
| 591 | 603 | extra_headers: &[(&str, String)], | |
| 592 | 604 | ) -> Result<()> { | |
| 605 | + | // Check suppression list before sending | |
| 606 | + | if let Some(ref pool) = self.pool { | |
| 607 | + | match crate::db::email_suppressions::is_suppressed(pool, to).await { | |
| 608 | + | Ok(true) => { | |
| 609 | + | tracing::info!(recipient = %to, subject = %subject, "email skipped (suppressed)"); | |
| 610 | + | return Ok(()); | |
| 611 | + | } | |
| 612 | + | Ok(false) => {} | |
| 613 | + | Err(e) => { | |
| 614 | + | // Log but don't block sending on suppression check failure | |
| 615 | + | tracing::warn!(recipient = %to, error = %e, "suppression check failed, sending anyway"); | |
| 616 | + | } | |
| 617 | + | } | |
| 618 | + | } | |
| 619 | + | ||
| 593 | 620 | if let Some(ref token) = self.config.postmark_token { | |
| 594 | 621 | self.send_via_postmark(token, to, subject, body, extra_headers).await | |
| 595 | 622 | } else { | |
| @@ -826,7 +853,7 @@ | |||
| 826 | 853 | /// | |
| 827 | 854 | /// * `action` — one of: `broadcast`, `release`, `sale`, `follower`, `login` | |
| 828 | 855 | /// * `target` — for `broadcast`: the creator's user ID to unfollow; | |
| 829 | - | /// for preferences: same as `user_id` | |
| 856 | + | /// for preferences: same as `user_id` | |
| 830 | 857 | pub fn generate_unsubscribe_url( | |
| 831 | 858 | host_url: &str, | |
| 832 | 859 | user_id: UserId, |
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file