Skip to main content

max / makenotwork

v0.3.3: Landing page updates, Sentry removal, storage and dashboard improvements Landing: ad-free guarantee in How It Works, centered CTA with Learn More button. Sentry removed — tracing-only observability. Storage upload validation hardened. Dashboard user management improvements. Health page cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-18 20:50 UTC
Commit: 2a3235d1cbecb0a651392a14d835745e125680f4
Parent: 834676a
32 files changed, +212 insertions, -250 deletions
M CLAUDE.md -1
@@ -104,7 +104,6 @@ psql -t -c "SELECT datname FROM pg_database WHERE datname LIKE 'mnw_test_%';" po
104 104 - **Hetzner Object Storage** — S3-compatible file storage (fsn1 region)
105 105 - **Postmark** — transactional email (password reset, verification, purchase receipts, notifications). Live mode.
106 106 - **Cloudflare** — DNS, CDN, DDoS protection
107 - - **Sentry** — error tracking
108 107 - **Fastmail** — business email (support@, legal@, max@)
109 108
110 109 ## Key Patterns
@@ -8,7 +8,7 @@ version = "0.25.1"
8 8 source = "registry+https://github.com/rust-lang/crates.io-index"
9 9 checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
10 10 dependencies = [
11 - "gimli 0.32.3",
11 + "gimli",
12 12 ]
13 13
14 14 [[package]]
@@ -68,9 +68,9 @@ dependencies = [
68 68
69 69 [[package]]
70 70 name = "annotate-snippets"
71 - version = "0.12.12"
71 + version = "0.12.13"
72 72 source = "registry+https://github.com/rust-lang/crates.io-index"
73 - checksum = "c86cd1c51b95d71dde52bca69ed225008f6ff4c8cc825b08042aa1ef823e1980"
73 + checksum = "74fc7650eedcb2fee505aad48491529e408f0e854c2d9f63eb86c1361b9b3f93"
74 74 dependencies = [
75 75 "anstyle",
76 76 "memchr",
@@ -79,9 +79,9 @@ dependencies = [
79 79
80 80 [[package]]
81 81 name = "anstream"
82 - version = "0.6.21"
82 + version = "1.0.0"
83 83 source = "registry+https://github.com/rust-lang/crates.io-index"
84 - checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
84 + checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
85 85 dependencies = [
86 86 "anstyle",
87 87 "anstyle-parse",
@@ -94,15 +94,15 @@ dependencies = [
94 94
95 95 [[package]]
96 96 name = "anstyle"
97 - version = "1.0.13"
97 + version = "1.0.14"
98 98 source = "registry+https://github.com/rust-lang/crates.io-index"
99 - checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
99 + checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
100 100
101 101 [[package]]
102 102 name = "anstyle-parse"
103 - version = "0.2.7"
103 + version = "1.0.0"
104 104 source = "registry+https://github.com/rust-lang/crates.io-index"
105 - checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
105 + checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
106 106 dependencies = [
107 107 "utf8parse",
108 108 ]
@@ -129,18 +129,15 @@ dependencies = [
129 129
130 130 [[package]]
131 131 name = "anyhow"
132 - version = "1.0.101"
132 + version = "1.0.102"
133 133 source = "registry+https://github.com/rust-lang/crates.io-index"
134 - checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
134 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
135 135
136 136 [[package]]
137 137 name = "arbitrary"
138 138 version = "1.4.2"
139 139 source = "registry+https://github.com/rust-lang/crates.io-index"
140 140 checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
141 - dependencies = [
142 - "derive_arbitrary",
143 - ]
144 141
145 142 [[package]]
146 143 name = "argon2"
@@ -187,7 +184,7 @@ dependencies = [
187 184 "rustc-hash 2.1.1",
188 185 "serde",
189 186 "serde_derive",
190 - "syn 2.0.114",
187 + "syn 2.0.117",
191 188 ]
192 189
193 190 [[package]]
@@ -242,7 +239,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
242 239 dependencies = [
243 240 "proc-macro2",
244 241 "quote",
245 - "syn 2.0.114",
242 + "syn 2.0.117",
246 243 "synstructure",
247 244 ]
248 245
@@ -254,7 +251,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
254 251 dependencies = [
255 252 "proc-macro2",
256 253 "quote",
257 - "syn 2.0.114",
254 + "syn 2.0.117",
258 255 "synstructure",
259 256 ]
260 257
@@ -266,7 +263,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
266 263 dependencies = [
267 264 "proc-macro2",
268 265 "quote",
269 - "syn 2.0.114",
266 + "syn 2.0.117",
270 267 ]
271 268
272 269 [[package]]
@@ -299,7 +296,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
299 296 dependencies = [
300 297 "proc-macro2",
301 298 "quote",
302 - "syn 2.0.114",
299 + "syn 2.0.117",
303 300 ]
304 301
305 302 [[package]]
@@ -335,7 +332,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
335 332 dependencies = [
336 333 "proc-macro2",
337 334 "quote",
338 - "syn 2.0.114",
335 + "syn 2.0.117",
339 336 ]
340 337
341 338 [[package]]
@@ -361,9 +358,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
361 358
362 359 [[package]]
363 360 name = "aws-config"
364 - version = "1.8.14"
361 + version = "1.8.15"
365 362 source = "registry+https://github.com/rust-lang/crates.io-index"
366 - checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2"
363 + checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc"
367 364 dependencies = [
368 365 "aws-credential-types",
369 366 "aws-runtime",
@@ -371,8 +368,8 @@ dependencies = [
371 368 "aws-sdk-ssooidc",
372 369 "aws-sdk-sts",
373 370 "aws-smithy-async",
374 - "aws-smithy-http 0.63.4",
375 - "aws-smithy-json 0.62.4",
371 + "aws-smithy-http 0.63.6",
372 + "aws-smithy-json 0.62.5",
376 373 "aws-smithy-runtime",
377 374 "aws-smithy-runtime-api",
378 375 "aws-smithy-types",
@@ -381,7 +378,7 @@ dependencies = [
381 378 "fastrand 2.3.0",
382 379 "hex",
383 380 "http 1.4.0",
384 - "ring",
381 + "sha1",
385 382 "time",
386 383 "tokio",
387 384 "tracing",
@@ -391,9 +388,9 @@ dependencies = [
391 388
392 389 [[package]]
393 390 name = "aws-credential-types"
394 - version = "1.2.12"
391 + version = "1.2.14"
395 392 source = "registry+https://github.com/rust-lang/crates.io-index"
396 - checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79"
393 + checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7"
397 394 dependencies = [
398 395 "aws-smithy-async",
399 396 "aws-smithy-runtime-api",
@@ -403,9 +400,9 @@ dependencies = [
403 400
404 401 [[package]]
405 402 name = "aws-lc-rs"
406 - version = "1.15.4"
403 + version = "1.16.1"
407 404 source = "registry+https://github.com/rust-lang/crates.io-index"
408 - checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
405 + checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
409 406 dependencies = [
410 407 "aws-lc-sys",
411 408 "zeroize",
@@ -413,9 +410,9 @@ dependencies = [
413 410
414 411 [[package]]
415 412 name = "aws-lc-sys"
416 - version = "0.37.1"
413 + version = "0.38.0"
417 414 source = "registry+https://github.com/rust-lang/crates.io-index"
418 - checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
415 + checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
419 416 dependencies = [
420 417 "cc",
421 418 "cmake",
@@ -425,15 +422,15 @@ dependencies = [
425 422
426 423 [[package]]
427 424 name = "aws-runtime"
428 - version = "1.7.0"
425 + version = "1.7.2"
429 426 source = "registry+https://github.com/rust-lang/crates.io-index"
430 - checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12"
427 + checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17"
431 428 dependencies = [
432 429 "aws-credential-types",
433 430 "aws-sigv4",
434 431 "aws-smithy-async",
435 432 "aws-smithy-eventstream",
436 - "aws-smithy-http 0.63.4",
433 + "aws-smithy-http 0.63.6",
437 434 "aws-smithy-runtime",
438 435 "aws-smithy-runtime-api",
439 436 "aws-smithy-types",
@@ -448,7 +445,7 @@ dependencies = [
448 445 "percent-encoding",
449 446 "pin-project-lite",
450 447 "tracing",
451 - "uuid 1.20.0",
448 + "uuid 1.22.0",
452 449 ]
453 450
454 451 [[package]]
@@ -487,15 +484,15 @@ dependencies = [
487 484
488 485 [[package]]
489 486 name = "aws-sdk-sso"
490 - version = "1.94.0"
487 + version = "1.97.0"
491 488 source = "registry+https://github.com/rust-lang/crates.io-index"
492 - checksum = "699da1961a289b23842d88fe2984c6ff68735fdf9bdcbc69ceaeb2491c9bf434"
489 + checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567"
493 490 dependencies = [
494 491 "aws-credential-types",
495 492 "aws-runtime",
496 493 "aws-smithy-async",
497 - "aws-smithy-http 0.63.4",
498 - "aws-smithy-json 0.62.4",
494 + "aws-smithy-http 0.63.6",
495 + "aws-smithy-json 0.62.5",
499 496 "aws-smithy-observability",
500 497 "aws-smithy-runtime",
501 498 "aws-smithy-runtime-api",
@@ -511,15 +508,15 @@ dependencies = [
511 508
512 509 [[package]]
513 510 name = "aws-sdk-ssooidc"
514 - version = "1.96.0"
511 + version = "1.99.0"
515 512 source = "registry+https://github.com/rust-lang/crates.io-index"
516 - checksum = "e3e3a4cb3b124833eafea9afd1a6cc5f8ddf3efefffc6651ef76a03cbc6b4981"
513 + checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8"
517 514 dependencies = [
518 515 "aws-credential-types",
519 516 "aws-runtime",
520 517 "aws-smithy-async",
521 - "aws-smithy-http 0.63.4",
522 - "aws-smithy-json 0.62.4",
518 + "aws-smithy-http 0.63.6",
519 + "aws-smithy-json 0.62.5",
523 520 "aws-smithy-observability",
524 521 "aws-smithy-runtime",
525 522 "aws-smithy-runtime-api",
@@ -535,15 +532,15 @@ dependencies = [
535 532
536 533 [[package]]
537 534 name = "aws-sdk-sts"
538 - version = "1.98.0"
535 + version = "1.101.0"
539 536 source = "registry+https://github.com/rust-lang/crates.io-index"
540 - checksum = "89c4f19655ab0856375e169865c91264de965bd74c407c7f1e403184b1049409"
537 + checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a"
541 538 dependencies = [
542 539 "aws-credential-types",
543 540 "aws-runtime",
544 541 "aws-smithy-async",
545 - "aws-smithy-http 0.63.4",
546 - "aws-smithy-json 0.62.4",
542 + "aws-smithy-http 0.63.6",
543 + "aws-smithy-json 0.62.5",
547 544 "aws-smithy-observability",
548 545 "aws-smithy-query",
549 546 "aws-smithy-runtime",
@@ -560,13 +557,13 @@ dependencies = [
560 557
561 558 [[package]]
562 559 name = "aws-sigv4"
563 - version = "1.4.0"
560 + version = "1.4.2"
564 561 source = "registry+https://github.com/rust-lang/crates.io-index"
565 - checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e"
562 + checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4"
566 563 dependencies = [
567 564 "aws-credential-types",
568 565 "aws-smithy-eventstream",
569 - "aws-smithy-http 0.63.4",
566 + "aws-smithy-http 0.63.6",
570 567 "aws-smithy-runtime-api",
571 568 "aws-smithy-types",
572 569 "bytes",
@@ -588,9 +585,9 @@ dependencies = [
588 585
589 586 [[package]]
590 587 name = "aws-smithy-async"
591 - version = "1.2.12"
588 + version = "1.2.14"
592 589 source = "registry+https://github.com/rust-lang/crates.io-index"
593 - checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70"
590 + checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc"
594 591 dependencies = [
595 592 "futures-util",
596 593 "pin-project-lite",
@@ -619,9 +616,9 @@ dependencies = [
619 616
620 617 [[package]]
621 618 name = "aws-smithy-eventstream"
622 - version = "0.60.19"
619 + version = "0.60.20"
623 620 source = "registry+https://github.com/rust-lang/crates.io-index"
624 - checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79"
621 + checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548"
625 622 dependencies = [
626 623 "aws-smithy-types",
627 624 "bytes",
@@ -652,9 +649,9 @@ dependencies = [
652 649
653 650 [[package]]
654 651 name = "aws-smithy-http"
655 - version = "0.63.4"
652 + version = "0.63.6"
656 653 source = "registry+https://github.com/rust-lang/crates.io-index"
657 - checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e"
654 + checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
658 655 dependencies = [
659 656 "aws-smithy-runtime-api",
660 657 "aws-smithy-types",
@@ -673,9 +670,9 @@ dependencies = [
673 670
674 671 [[package]]
675 672 name = "aws-smithy-http-client"
676 - version = "1.1.10"
673 + version = "1.1.12"
677 674 source = "registry+https://github.com/rust-lang/crates.io-index"
678 - checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b"
675 + checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769"
679 676 dependencies = [
680 677 "aws-smithy-async",
681 678 "aws-smithy-runtime-api",
@@ -692,7 +689,7 @@ dependencies = [
692 689 "hyper-util",
693 690 "pin-project-lite",
694 691 "rustls 0.21.12",
695 - "rustls 0.23.36",
692 + "rustls 0.23.37",
696 693 "rustls-native-certs",
697 694 "rustls-pki-types",
698 695 "tokio",
@@ -712,27 +709,27 @@ dependencies = [
712 709
713 710 [[package]]
714 711 name = "aws-smithy-json"
715 - version = "0.62.4"
712 + version = "0.62.5"
716 713 source = "registry+https://github.com/rust-lang/crates.io-index"
717 - checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb"
714 + checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a"
718 715 dependencies = [
719 716 "aws-smithy-types",
720 717 ]
721 718
722 719 [[package]]
723 720 name = "aws-smithy-observability"
724 - version = "0.2.5"
721 + version = "0.2.6"
725 722 source = "registry+https://github.com/rust-lang/crates.io-index"
726 - checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b"
723 + checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c"
727 724 dependencies = [
728 725 "aws-smithy-runtime-api",
729 726 ]
730 727
731 728 [[package]]
732 729 name = "aws-smithy-query"
733 - version = "0.60.14"
730 + version = "0.60.15"
734 731 source = "registry+https://github.com/rust-lang/crates.io-index"
735 - checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0"
732 + checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd"
736 733 dependencies = [
737 734 "aws-smithy-types",
738 735 "urlencoding",
@@ -740,12 +737,12 @@ dependencies = [
740 737
741 738 [[package]]
742 739 name = "aws-smithy-runtime"
743 - version = "1.10.1"
740 + version = "1.10.3"
744 741 source = "registry+https://github.com/rust-lang/crates.io-index"
745 - checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa"
742 + checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110"
746 743 dependencies = [
747 744 "aws-smithy-async",
748 - "aws-smithy-http 0.63.4",
745 + "aws-smithy-http 0.63.6",
749 746 "aws-smithy-http-client",
750 747 "aws-smithy-observability",
751 748 "aws-smithy-runtime-api",
@@ -765,9 +762,9 @@ dependencies = [
765 762
766 763 [[package]]
767 764 name = "aws-smithy-runtime-api"
768 - version = "1.11.4"
765 + version = "1.11.6"
769 766 source = "registry+https://github.com/rust-lang/crates.io-index"
770 - checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec"
767 + checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6"
771 768 dependencies = [
772 769 "aws-smithy-async",
773 770 "aws-smithy-types",
@@ -782,9 +779,9 @@ dependencies = [
782 779
783 780 [[package]]
784 781 name = "aws-smithy-types"
785 - version = "1.4.4"
782 + version = "1.4.7"
786 783 source = "registry+https://github.com/rust-lang/crates.io-index"
787 - checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd"
784 + checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c"
788 785 dependencies = [
789 786 "base64-simd",
790 787 "bytes",
@@ -808,18 +805,18 @@ dependencies = [
808 805
809 806 [[package]]
810 807 name = "aws-smithy-xml"
811 - version = "0.60.14"
808 + version = "0.60.15"
812 809 source = "registry+https://github.com/rust-lang/crates.io-index"
813 - checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
810 + checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3"
814 811 dependencies = [
815 812 "xmlparser",
816 813 ]
817 814
818 815 [[package]]
819 816 name = "aws-types"
820 - version = "1.3.12"
817 + version = "1.3.14"
821 818 source = "registry+https://github.com/rust-lang/crates.io-index"
822 - checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a"
819 + checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9"
823 820 dependencies = [
824 821 "aws-credential-types",
825 822 "aws-smithy-async",
@@ -917,22 +914,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
917 914 dependencies = [
918 915 "proc-macro2",
919 916 "quote",
920 - "syn 2.0.114",
921 - ]
922 -
923 - [[package]]
924 - name = "backtrace"
925 - version = "0.3.76"
926 - source = "registry+https://github.com/rust-lang/crates.io-index"
927 - checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
928 - dependencies = [
929 - "addr2line",
930 - "cfg-if",
931 - "libc",
932 - "miniz_oxide",
933 - "object",
934 - "rustc-demangle",
935 - "windows-link",
917 + "syn 2.0.117",
936 918 ]
937 919
938 920 [[package]]
@@ -1065,9 +1047,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
1065 1047
1066 1048 [[package]]
1067 1049 name = "bitflags"
1068 - version = "2.10.0"
1050 + version = "2.11.0"
1069 1051 source = "registry+https://github.com/rust-lang/crates.io-index"
1070 - checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
1052 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
1071 1053 dependencies = [
1072 1054 "serde_core",
1073 1055 ]
@@ -1103,15 +1085,6 @@ dependencies = [
1103 1085 ]
1104 1086
1105 1087 [[package]]
1106 - name = "block2"
1107 - version = "0.6.2"
1108 - source = "registry+https://github.com/rust-lang/crates.io-index"
1109 - checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
1110 - dependencies = [
1111 - "objc2",
1112 - ]
1113 -
1114 - [[package]]
1115 1088 name = "bstr"
1116 1089 version = "1.12.1"
1117 1090 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1124,9 +1097,9 @@ dependencies = [
1124 1097
1125 1098 [[package]]
1126 1099 name = "bumpalo"
1127 - version = "3.19.1"
1100 + version = "3.20.2"
1128 1101 source = "registry+https://github.com/rust-lang/crates.io-index"
1129 - checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
1102 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
1130 1103 dependencies = [
1131 1104 "allocator-api2",
1132 1105 ]
@@ -1176,9 +1149,9 @@ dependencies = [
1176 1149
1177 1150 [[package]]
1178 1151 name = "cc"
1179 - version = "1.2.55"
1152 + version = "1.2.57"
1180 1153 source = "registry+https://github.com/rust-lang/crates.io-index"
1181 - checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
1154 + checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
1182 1155 dependencies = [
1183 1156 "find-msvc-tools",
1184 1157 "jobserver",
@@ -1194,7 +1167,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
1194 1167 dependencies = [
1195 1168 "byteorder",
1196 1169 "fnv",
1197 - "uuid 1.20.0",
1170 + "uuid 1.22.0",
1198 1171 ]
1199 1172
Lines truncated
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.3.0"
3 + version = "0.3.3"
4 4 edition = "2024"
5 5 license-file = "../../LICENSE"
6 6
@@ -98,9 +98,6 @@ urlencoding = "2.1.3"
98 98 # URL parsing
99 99 url = "2.5.8"
100 100
101 - # Error tracking
102 - sentry = { version = "0.38", default-features = false, features = ["backtrace", "contexts", "panic", "tracing", "reqwest", "rustls"] }
103 -
104 101 [[bin]]
105 102 name = "mnw-admin"
106 103 path = "src/bin/mnw-admin.rs"
@@ -17,7 +17,7 @@
17 17
18 18 use argon2::{
19 19 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
20 - Argon2,
20 + Algorithm, Argon2, Params, Version,
21 21 };
22 22 use axum::{
23 23 extract::FromRequestParts,
@@ -183,10 +183,12 @@ impl FromRequestParts<crate::AppState> for AdminUser {
183 183 }
184 184 }
185 185
186 - /// Hash a password using Argon2
186 + /// Hash a password using Argon2id (46 MiB, 2 iterations, 1 thread).
187 187 pub fn hash_password(password: &str) -> Result<String, AppError> {
188 188 let salt = SaltString::generate(&mut OsRng);
189 - let argon2 = Argon2::default();
189 + let params = Params::new(46 * 1024, 2, 1, None)
190 + .map_err(|e| AppError::Internal(anyhow::anyhow!("Argon2 params error: {}", e)))?;
191 + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
190 192
191 193 let hash = argon2
192 194 .hash_password(password.as_bytes(), &salt)
@@ -357,7 +359,6 @@ mod tests {
357 359 stripe: None,
358 360 admin_user_id: Some(user.id),
359 361 synckit_jwt_secret: None,
360 - sentry_dsn: None,
361 362 scan: None,
362 363 git_repos_path: None,
363 364 postmark_webhook_token: None,
@@ -412,7 +413,6 @@ mod tests {
412 413 stripe: None,
413 414 admin_user_id: None,
414 415 synckit_jwt_secret: None,
415 - sentry_dsn: None,
416 416 scan: None,
417 417 git_repos_path: None,
418 418 postmark_webhook_token: None,
@@ -27,8 +27,6 @@ pub struct Config {
27 27 pub admin_user_id: Option<UserId>,
28 28 /// JWT secret for SyncKit token signing (optional)
29 29 pub synckit_jwt_secret: Option<String>,
30 - /// Sentry DSN for error tracking (optional)
31 - pub sentry_dsn: Option<String>,
32 30 /// File scanning configuration (optional)
33 31 pub scan: Option<ScanConfig>,
34 32 /// Path to bare git repositories on disk (optional)
@@ -121,9 +119,6 @@ impl Config {
121 119 // SyncKit JWT secret - optional, sync endpoints return 503 if unset
122 120 let synckit_jwt_secret = std::env::var("SYNCKIT_JWT_SECRET").ok();
123 121
124 - // Sentry DSN - optional, error tracking disabled if unset
125 - let sentry_dsn = std::env::var("SENTRY_DSN").ok();
126 -
127 122 // File scanning - enabled by default, set SCAN_ENABLED=false to disable
128 123 let scan = ScanConfig::from_env();
129 124
@@ -161,7 +156,6 @@ impl Config {
161 156 stripe,
162 157 admin_user_id,
163 158 synckit_jwt_secret,
164 - sentry_dsn,
165 159 scan,
166 160 git_repos_path,
167 161 postmark_webhook_token,
@@ -293,7 +287,6 @@ impl std::fmt::Debug for Config {
293 287 .field("stripe", &self.stripe)
294 288 .field("admin_user_id", &self.admin_user_id)
295 289 .field("synckit_jwt_secret", &self.synckit_jwt_secret.as_ref().map(|_| "[REDACTED]"))
296 - .field("sentry_dsn", &self.sentry_dsn.as_ref().map(|_| "[REDACTED]"))
297 290 .field("scan", &self.scan)
298 291 .field("git_repos_path", &self.git_repos_path)
299 292 .field("postmark_webhook_token", &self.postmark_webhook_token.as_ref().map(|_| "[REDACTED]"))
@@ -359,7 +352,6 @@ mod tests {
359 352 stripe: None,
360 353 admin_user_id: None,
361 354 synckit_jwt_secret: None,
362 - sentry_dsn: None,
363 355 scan: None,
364 356 git_repos_path: None,
365 357 postmark_webhook_token: None,
@@ -383,26 +375,4 @@ mod tests {
383 375 assert!(ConfigError::MissingDatabaseUrl.to_string().contains("DATABASE_URL"));
384 376 }
385 377
386 - #[test]
387 - fn storage_config_from_env_returns_none_without_vars() {
388 - // Clear any env vars that might be set
389 - // SAFETY: test-only, no concurrent threads depend on these vars
390 - unsafe {
391 - std::env::remove_var("S3_ENDPOINT");
392 - std::env::remove_var("S3_BUCKET");
393 - std::env::remove_var("S3_ACCESS_KEY");
394 - std::env::remove_var("S3_SECRET_KEY");
395 - }
396 - assert!(StorageConfig::from_env().is_none());
397 - }
398 -
399 - #[test]
400 - fn stripe_config_from_env_returns_none_without_vars() {
401 - // SAFETY: test-only, no concurrent threads depend on these vars
402 - unsafe {
403 - std::env::remove_var("STRIPE_SECRET_KEY");
404 - std::env::remove_var("STRIPE_WEBHOOK_SECRET");
405 - }
406 - assert!(StripeConfig::from_env().is_none());
407 - }
408 378 }
@@ -133,7 +133,14 @@ pub async fn csrf_middleware(request: Request, next: Next) -> Response {
133 133 // - /confirm-delete uses a signed HMAC link as its authorization; the user
134 134 // arrives from an email and may not have an active session, so the
135 135 // standard CSRF header cannot be attached to the vanilla form POST.
136 - let exempt_paths = ["/stripe/webhook", "/stripe/checkout", "/stripe/subscribe", "/login", "/join", "/api/sync", "/oauth", "/auth/passkey", "/postmark/webhook", "/unsubscribe", "/confirm-delete"];
136 + let exempt_paths = [
137 + "/stripe/webhook", "/stripe/checkout", "/stripe/subscribe",
138 + "/login", "/join",
139 + "/api/sync/auth", "/api/sync/push", "/api/sync/pull", "/api/sync/status",
140 + "/api/sync/devices", "/api/sync/keys", "/api/sync/blobs",
141 + "/oauth", "/auth/passkey", "/postmark/webhook",
142 + "/unsubscribe", "/confirm-delete",
143 + ];
137 144
138 145 if exempt_paths.iter().any(|p| path.starts_with(p)) {
139 146 return next.run(request).await;
@@ -16,7 +16,7 @@ pub async fn search_categories(
16 16 SELECT pc.id, pc.name, pc.slug, pc.created_at
17 17 FROM project_categories pc
18 18 LEFT JOIN projects p ON p.category_id = pc.id AND p.is_public = true
19 - WHERE pc.name ILIKE '%' || $1 || '%'
19 + WHERE pc.name ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
20 20 GROUP BY pc.id
21 21 ORDER BY COUNT(p.id) DESC, pc.name
22 22 LIMIT $2
@@ -100,8 +100,8 @@ pub async fn get_category_counts(
100 100 r#" AND (
101 101 p.title % $1
102 102 OR COALESCE(p.description, '') % $1
103 - OR p.title ILIKE '%' || $1 || '%'
104 - OR COALESCE(p.description, '') ILIKE '%' || $1 || '%'
103 + OR p.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
104 + OR COALESCE(p.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
105 105 )"#,
106 106 );
107 107 }
@@ -107,8 +107,8 @@ pub async fn discover_items(
107 107 r#" AND (
108 108 i.title % $1
109 109 OR COALESCE(i.description, '') % $1
110 - OR i.title ILIKE '%' || $1 || '%'
111 - OR COALESCE(i.description, '') ILIKE '%' || $1 || '%'
110 + OR i.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
111 + OR COALESCE(i.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
112 112 )"#,
113 113 );
114 114 }
@@ -201,8 +201,8 @@ pub async fn count_discover_items(
201 201 r#" AND (
202 202 i.title % $1
203 203 OR COALESCE(i.description, '') % $1
204 - OR i.title ILIKE '%' || $1 || '%'
205 - OR COALESCE(i.description, '') ILIKE '%' || $1 || '%'
204 + OR i.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
205 + OR COALESCE(i.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
206 206 )"#,
207 207 );
208 208 }
@@ -321,8 +321,8 @@ pub async fn discover_projects(
321 321 r#" AND (
322 322 p.title % $1
323 323 OR COALESCE(p.description, '') % $1
324 - OR p.title ILIKE '%' || $1 || '%'
325 - OR COALESCE(p.description, '') ILIKE '%' || $1 || '%'
324 + OR p.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
325 + OR COALESCE(p.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
326 326 )"#,
327 327 );
328 328 }
@@ -386,8 +386,8 @@ pub async fn count_discover_projects(
386 386 r#" AND (
387 387 p.title % $1
388 388 OR COALESCE(p.description, '') % $1
389 - OR p.title ILIKE '%' || $1 || '%'
390 - OR COALESCE(p.description, '') ILIKE '%' || $1 || '%'
389 + OR p.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
390 + OR COALESCE(p.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
391 391 )"#,
392 392 );
393 393 }
@@ -443,8 +443,8 @@ pub async fn get_item_type_counts(
443 443 r#" AND (
444 444 i.title % $1
445 445 OR COALESCE(i.description, '') % $1
446 - OR i.title ILIKE '%' || $1 || '%'
447 - OR COALESCE(i.description, '') ILIKE '%' || $1 || '%'
446 + OR i.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
447 + OR COALESCE(i.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
448 448 )"#,
449 449 );
450 450 }
@@ -514,8 +514,8 @@ pub async fn get_price_range_counts(
514 514 r#" AND (
515 515 i.title % $1
516 516 OR COALESCE(i.description, '') % $1
517 - OR i.title ILIKE '%' || $1 || '%'
518 - OR COALESCE(i.description, '') ILIKE '%' || $1 || '%'
517 + OR i.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
518 + OR COALESCE(i.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
519 519 )"#,
520 520 );
521 521 }
@@ -84,7 +84,9 @@ pub async fn list_issues(
84 84 ) -> Result<(Vec<DbIssueWithMeta>, i64)> {
85 85 let offset = (page - 1) * per_page;
86 86 let status_str = status.map(|s| s.to_string());
87 - let search_pattern = search.map(|s| format!("%{}%", s));
87 + let search_pattern = search.map(|s| {
88 + format!("%{}%", s.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"))
89 + });
88 90
89 91 let issues = sqlx::query_as::<_, DbIssueWithMeta>(
90 92 r#"
@@ -13,7 +13,7 @@ pub async fn search_tags(pool: &PgPool, query: &str, limit: i64) -> Result<Vec<D
13 13 r#"
14 14 SELECT id, name, slug, parent_id, sort_order, created_at
15 15 FROM tags
16 - WHERE name ILIKE '%' || $1 || '%'
16 + WHERE name ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
17 17 ORDER BY similarity(name, $1) DESC, name
18 18 LIMIT $2
19 19 "#,
@@ -161,8 +161,8 @@ pub async fn get_tag_counts(
161 161 r#" AND (
162 162 i.title % $1
163 163 OR COALESCE(i.description, '') % $1
164 - OR i.title ILIKE '%' || $1 || '%'
165 - OR COALESCE(i.description, '') ILIKE '%' || $1 || '%'
164 + OR i.title ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
165 + OR COALESCE(i.description, '') ILIKE '%' || replace(replace(replace($1, '\', '\\'), '%', '\%'), '_', '\_') || '%'
166 166 )"#,
167 167 );
168 168 }
@@ -295,13 +295,13 @@ pub async fn suggest_tags_for_item(
295 295 AND (
296 296 t.slug = $2
297 297 OR EXISTS(SELECT 1 FROM tags p WHERE p.id = t.parent_id AND p.slug = $2)
298 - OR $3 ILIKE '%' || t.name || '%'
298 + OR $3 ILIKE '%' || replace(replace(replace(t.name, '\', '\\'), '%', '\%'), '_', '\_') || '%'
299 299 OR similarity(t.name, $3) > 0.3
300 300 )
301 301 ORDER BY
302 302 (t.slug = $2) DESC,
303 303 EXISTS(SELECT 1 FROM tags p WHERE p.id = t.parent_id AND p.slug = $2) DESC,
304 - ($3 ILIKE '%' || t.name || '%') DESC,
304 + ($3 ILIKE '%' || replace(replace(replace(t.name, '\', '\\'), '%', '\%'), '_', '\_') || '%') DESC,
305 305 similarity(t.name, $3) DESC
306 306 LIMIT 5
307 307 "#,
@@ -115,11 +115,6 @@ impl IntoResponse for AppError {
115 115 let status = self.status_code();
116 116 let message = self.user_message();
117 117
118 - // Tag for Sentry filtering before the tracing::error!() that Sentry captures
119 - sentry::configure_scope(|scope| {
120 - scope.set_tag("error.kind", self.tag());
121 - });
122 -
123 118 // Log internal errors
124 119 match &self {
125 120 AppError::Database(e) => {
@@ -18,7 +18,6 @@ pub mod scheduler;
18 18 pub mod routes;
19 19 pub mod rss;
20 20 pub mod scanning;
21 - pub mod sentry_layer;
22 21 pub mod storage;
23 22 pub mod synckit_auth;
24 23 pub mod templates;
@@ -101,7 +100,6 @@ pub fn build_app(state: AppState, session_layer: SessionManagerLayer<PostgresSto
101 100 .service(ServeDir::new("static")),
102 101 )
103 102 .with_state(state)
104 - .layer(middleware::from_fn(sentry_layer::sentry_user_context))
105 103 .layer(middleware::from_fn(csrf::csrf_middleware))
106 104 .layer(session_layer)
107 105 .layer(RequestBodyLimitLayer::new(1024 * 1024))
@@ -26,35 +26,8 @@ async fn main() {
26 26 // Load environment variables from .env file
27 27 dotenvy::dotenv().ok();
28 28
29 - // Initialize Sentry error tracking (no-op if SENTRY_DSN is unset)
30 - let _sentry_guard = sentry::init(sentry::ClientOptions {
31 - dsn: std::env::var("SENTRY_DSN")
32 - .ok()
33 - .and_then(|s| s.parse().ok()),
34 - release: Some(
35 - format!(
36 - "makenotwork@{}-{}",
37 - env!("CARGO_PKG_VERSION"),
38 - option_env!("GIT_HASH").unwrap_or("unknown")
39 - )
40 - .into(),
41 - ),
42 - environment: Some(
43 - if cfg!(debug_assertions) {
44 - "development"
45 - } else {
46 - "production"
47 - }
48 - .into(),
49 - ),
50 - traces_sample_rate: 0.0, // Error tracking only, no performance tracing
51 - ..Default::default()
52 - });
53 -
54 29 // Set up structured logging: JSON in release, human-readable in dev
55 - // sentry_tracing layer captures error!() events and sends them to Sentry
56 30 tracing_subscriber::registry()
57 - .with(sentry::integrations::tracing::layer())
58 31 .with(
59 32 tracing_subscriber::EnvFilter::try_from_default_env()
60 33 .unwrap_or_else(|_| "makenotwork=debug,tower_http=debug,sqlx=info".into()),
@@ -138,6 +138,9 @@ pub(in crate::routes::api) async fn update_password(
138 138 // Check for breached password (advisory only, don't block)
139 139 if let Some(count) = crate::auth::check_password_breach(&req.new_password).await {
140 140 tracing::warn!(user_id = %user.id, event = "breached_password_change", breach_count = count, "User changed to breached password");
141 + session.insert("password_warning", format!(
142 + "This password has appeared in {} known data breach(es). Consider changing it.", count
143 + )).await.ok();
141 144 }
142 145
143 146 // Hash and update
@@ -310,6 +310,9 @@ async fn join_handler(
310 310 // Check for breached password (advisory only, don't block signup)
311 311 if let Some(count) = crate::auth::check_password_breach(&form.password).await {
312 312 tracing::warn!(event = "breached_password_signup", breach_count = count, "New user signed up with breached password");
313 + session.insert("password_warning", format!(
314 + "This password has appeared in {} known data breach(es). Consider changing it.", count
315 + )).await.ok();
313 316 }
314 317
315 318 // Hash password and create user
@@ -127,6 +127,16 @@ pub(super) async fn dashboard(
127 127 let appeal_decision = db_user.appeal_decision.clone();
128 128 let appeal_response = db_user.appeal_response.clone();
129 129
130 + // Check for one-time password breach warning (set during signup/password change)
131 + let password_warning = session
132 + .get::<String>("password_warning")
133 + .await
134 + .ok()
135 + .flatten();
136 + if password_warning.is_some() {
137 + session.remove::<String>("password_warning").await.ok();
138 + }
139 +
130 140 Ok(DashboardUserTemplate {
131 141 csrf_token,
132 142 session_user: Some(session_user),
@@ -140,6 +150,7 @@ pub(super) async fn dashboard(
140 150 has_pending_appeal,
141 151 appeal_decision,
142 152 appeal_response,
153 + password_warning,
143 154 })
144 155 }
145 156
@@ -244,6 +244,9 @@ async fn reset_password_handler(
244 244 // Check for breached password (advisory only, don't block)
245 245 if let Some(count) = crate::auth::check_password_breach(&form.password).await {
246 246 tracing::warn!(user_id = %user_id, event = "breached_password_reset", breach_count = count, "Password reset to breached password");
247 + session.insert("password_warning", format!(
248 + "This password has appeared in {} known data breach(es). Consider changing it.", count
249 + )).await.ok();
247 250 }
248 251
249 252 // Hash new password and update
@@ -118,7 +118,6 @@ struct HealthData {
118 118 synckit_status_class: &'static str,
119 119
120 120 // Security & Monitoring
121 - sentry_configured: bool,
122 121 admin_configured: bool,
123 122
124 123 // Background monitor
@@ -382,7 +381,6 @@ async fn collect_health(state: &AppState) -> HealthData {
382 381 let synckit_status_class = if synckit_configured { "status-ok" } else { "status-warn" };
383 382
384 383 // Security & Monitoring
385 - let sentry_configured = state.config.sentry_dsn.is_some();
386 384 let admin_configured = state.config.admin_user_id.is_some();
387 385
388 386 // Overall tri-state status
@@ -528,7 +526,6 @@ async fn collect_health(state: &AppState) -> HealthData {
528 526 synckit_configured,
529 527 synckit_status,
530 528 synckit_status_class,
531 - sentry_configured,
532 529 admin_configured,
533 530 monitor_enabled: true,
534 531 monitor_interval_secs,
@@ -616,7 +613,6 @@ pub(super) async fn health(
616 613 synckit_app_count: data.stats.sync_app_count.to_string(),
617 614 synckit_device_count: data.stats.sync_device_count.to_string(),
618 615 synckit_log_entries: data.stats.sync_log_entries.to_string(),
619 - sentry_status: if data.sentry_configured { "Configured".to_string() } else { "Not configured".to_string() },
620 616 admin_status: if data.admin_configured { "Configured".to_string() } else { "Not configured".to_string() },
621 617 monitor_enabled: data.monitor_enabled,
622 618 monitor_interval_secs: data.monitor_interval_secs,
@@ -198,6 +198,17 @@ async fn confirm_upload(
198 198 ));
199 199 }
200 200
201 + // Enforce file size limit
202 + if let Some(size) = s3.object_size(&req.s3_key).await?
203 + && size as u64 > file_type.max_size()
204 + {
205 + s3.delete_object(&req.s3_key).await.ok();
206 + return Err(AppError::BadRequest(format!(
207 + "File exceeds maximum size of {} MB",
208 + file_type.max_size() / (1024 * 1024)
209 + )));
210 + }
211 +
201 212 // Scan the file if scanner is available
202 213 if let Some(ref scanner) = state.scanner {
203 214 let data = s3.download_object(&req.s3_key).await?;
@@ -441,6 +452,17 @@ async fn version_confirm_upload(
441 452 ));
442 453 }
443 454
455 + // Enforce file size limit (versions are always downloads)
456 + if let Some(size) = s3.object_size(&req.s3_key).await?
457 + && size as u64 > FileType::Download.max_size()
458 + {
459 + s3.delete_object(&req.s3_key).await.ok();
460 + return Err(AppError::BadRequest(format!(
461 + "File exceeds maximum size of {} MB",
462 + FileType::Download.max_size() / (1024 * 1024)
463 + )));
464 + }
465 +
444 466 // Scan the file if scanner is available
445 467 if let Some(ref scanner) = state.scanner {
446 468 let data = s3.download_object(&req.s3_key).await?;
@@ -412,6 +412,11 @@ pub(super) async fn create_subscription_checkout(
412 412 return Err(AppError::BadRequest("Creator's payment account is not ready".to_string()));
413 413 }
414 414
415 + // A user cannot subscribe to their own project
416 + if user.id == project.user_id {
417 + return Err(AppError::BadRequest("You cannot subscribe to your own project".to_string()));
418 + }
419 +
415 420 // Check if user already has an active subscription to this project
416 421 if db::subscriptions::has_active_subscription_to_project(&state.db, user.id, tier.project_id).await? {
417 422 return Ok(Redirect::to(&format!("/p/{}", project.slug)).into_response());