From dea0c051e287cb467f339c66da1cda60b8a3a8ff Mon Sep 17 00:00:00 2001 From: Josh Stegmaier <104993387+joshuastegmaier@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:07:33 -0400 Subject: [PATCH] Cloudflare Turnstile implementation (#2526) * initial commit for turnstile module approach * start integrating turnstile into concordia forms and views * Update the url for Originally from reference. * First attempt at adding form widget to form and view and template. * settings is now passing TURN values, moved view to save transcription - still not the right place.... * clean up - still stuck * Basic implementation of turnstile to transcription form * Added turnstile to forms that previously used captcha. Removed captcha from codebase. Updated tests to correctly handle Turnstile. Fixed issue with caching and tests that only appeared when running tests multiple times in the same environment more often than once per hour. Updated docs to include Turnstile info * Updated error message for when a user fails Turnstile validation --------- Co-authored-by: Jen Kuenning --- Pipfile | 1 - Pipfile.lock | 586 +++++++++--------- concordia/context_processors.py | 29 + concordia/forms.py | 6 + concordia/settings_ecs.py | 5 + concordia/settings_local_test.py | 16 + concordia/settings_template.py | 16 +- concordia/settings_test.py | 16 + concordia/static/js/src/contribute.js | 70 +-- .../forms/widgets/turnstile_widget.html | 1 + concordia/templates/registration/login.html | 2 + .../transcriptions/asset_detail.html | 4 +- .../asset_detail/captcha_modal.html | 38 -- .../transcriptions/asset_detail/editor.html | 2 + concordia/tests/test_views.py | 242 ++++---- concordia/turnstile/LICENSE | 21 + concordia/turnstile/__init__.py | 0 concordia/turnstile/context_processor.py | 5 + concordia/turnstile/fields.py | 76 +++ concordia/turnstile/widgets.py | 30 + concordia/urls.py | 2 - concordia/views.py | 96 ++- docs/for-developers.md | 10 + 23 files changed, 695 insertions(+), 579 deletions(-) create mode 100644 concordia/templates/forms/widgets/turnstile_widget.html delete mode 100644 concordia/templates/transcriptions/asset_detail/captcha_modal.html create mode 100644 concordia/turnstile/LICENSE create mode 100644 concordia/turnstile/__init__.py create mode 100644 concordia/turnstile/context_processor.py create mode 100644 concordia/turnstile/fields.py create mode 100644 concordia/turnstile/widgets.py diff --git a/Pipfile b/Pipfile index f4b8646a4..44b7b3125 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,6 @@ requests = "*" Django = ">=4.2.14, <5.0" bagit = "*" django-registration = "*" -django-simple-captcha = "*" django-tinymce = "*" elasticsearch = "<7.14.0" django-elasticsearch-dsl = "==7.3" diff --git a/Pipfile.lock b/Pipfile.lock index 7736e92cb..ad2b7c898 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "884c7b6b2c524de8a4cc75faac6d34ee17c14451bb8c3ce3462952945995aef6" + "sha256": "22770418d547f0260d955a46ab385b4d4ffefd53d4a365f726cda4dc055b4b27" }, "pipfile-spec": 6, "requires": { @@ -152,24 +152,23 @@ "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.7.0" }, "boto3": { "hashes": [ - "sha256:06eac4757de2a9c6020381205cb902f05964caad80b56e58c8931284a133b4cb", - "sha256:b9587131372a808bf6f99c5ed8b11be55cd113261cc3b437a917b4acc6c30bfe" + "sha256:247f88eedce9ae4e014a8fc14a9473759bb8e391460d49396a3b600fb649f33b", + "sha256:db5fbbd10248db060f2ccce3ae17764f1641c99c8b9f51d422c26ebe25703a1e" ], "index": "pypi", - "version": "==1.35.8" + "version": "==1.35.21" }, "botocore": { "hashes": [ - "sha256:4b820cf680ab5d778bd2fe4feeef1ff8a2b96d5c535d4638ab30f703ade282f8", - "sha256:adf389eb8fd87775f193300e3431d1353f925807ad3a39958172cb644f0d60a1" + "sha256:3db9ddfe521edc0753fc8c68caef71c7806e1d2d21ce8cbabc2065b7d79192f2", + "sha256:db917e7d7b3a2eed1310c6496784bc813c91f020a021c2ab5f9df7d28cdb4f1d" ], "index": "pypi", - "version": "==1.35.8" + "version": "==1.35.21" }, "brotli": { "hashes": [ @@ -272,11 +271,11 @@ }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cffi": { "hashes": [ @@ -348,7 +347,7 @@ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.8'", "version": "==1.17.1" }, "channels": { @@ -564,7 +563,6 @@ "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==43.0.1" }, @@ -593,11 +591,11 @@ }, "django": { "hashes": [ - "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", - "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" + "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", + "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad" ], "index": "pypi", - "version": "==4.2.15" + "version": "==4.2.16" }, "django-admin-multiple-choice-list-filter": { "hashes": [ @@ -662,12 +660,6 @@ "index": "pypi", "version": "==1.0.0" }, - "django-ranged-response": { - "hashes": [ - "sha256:f71fff352a37316b9bead717fc76e4ddd6c9b99c4680cdf4783b9755af1cf985" - ], - "version": "==0.2.0" - }, "django-ratelimit": { "hashes": [ "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", @@ -700,14 +692,6 @@ "index": "pypi", "version": "==6.1" }, - "django-simple-captcha": { - "hashes": [ - "sha256:3ae9a7e650cb0cdbcfd4a75aa91fdf25dcc523ef541a7b1f004bd4357798fc03", - "sha256:d188516d326fadd2d5ad076eb89649d55c02cabafe3fdcc2154ac18e9f6d4b97" - ], - "index": "pypi", - "version": "==0.6.0" - }, "django-storages": { "hashes": [ "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f", @@ -721,7 +705,7 @@ "sha256:3232e7ecde66ba4464abb6f9e6b8cc739b914efb9b29dc2cf2eee451f7cc2acb", "sha256:aa6f4965838484317b7f08d22c0d91a53d64e7bbbd34264468ae83d4023898a7" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", + "markers": "python_version >= '3.8' and python_version < '4'", "version": "==7.0" }, "django-tinymce": { @@ -793,7 +777,6 @@ "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc" ], - "index": "pypi", "version": "==5.0.0" }, "flask-login": { @@ -981,67 +964,75 @@ }, "greenlet": { "hashes": [ - "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", - "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", - "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", - "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", - "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", - "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", - "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", - "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", - "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", - "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", - "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", - "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", - "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", - "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", - "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", - "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", - "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", - "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", - "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", - "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", - "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", - "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", - "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", - "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", - "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", - "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", - "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", - "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", - "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", - "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", - "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", - "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", - "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", - "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", - "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", - "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", - "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", - "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", - "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", - "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", - "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", - "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", - "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", - "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", - "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", - "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", - "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", - "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", - "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", - "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", - "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", - "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", - "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", - "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", - "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", - "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", - "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", - "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" - ], - "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", - "version": "==3.0.3" + "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9", + "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17", + "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc", + "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637", + "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2", + "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3", + "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6", + "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b", + "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf", + "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27", + "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1", + "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc", + "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a", + "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b", + "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d", + "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28", + "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303", + "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99", + "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f", + "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7", + "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6", + "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a", + "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc", + "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0", + "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8", + "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a", + "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca", + "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b", + "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989", + "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19", + "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6", + "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484", + "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd", + "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25", + "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b", + "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910", + "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0", + "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5", + "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345", + "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6", + "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00", + "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df", + "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811", + "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca", + "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8", + "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33", + "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97", + "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0", + "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b", + "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682", + "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39", + "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64", + "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f", + "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665", + "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f", + "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc", + "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d", + "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a", + "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0", + "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09", + "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b", + "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491", + "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7", + "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954", + "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501", + "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54" + ], + "markers": "platform_python_implementation == 'CPython' and python_version < '3.11'", + "version": "==3.1.0" }, "gunicorn": { "hashes": [ @@ -1199,11 +1190,11 @@ }, "idna": { "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], "markers": "python_version >= '3.6'", - "version": "==3.8" + "version": "==3.10" }, "incremental": { "hashes": [ @@ -1247,19 +1238,19 @@ }, "kombu": { "hashes": [ - "sha256:ad200a8dbdaaa2bbc5f26d2ee7d707d9a1fded353a0f4bd751ce8c7d9f449c60", - "sha256:c8dd99820467610b4febbc7a9e8a0d3d7da2d35116b67184418b51cc520ea6b6" + "sha256:1c05178826dab811f8cab5b0a154d42a7a33d8bcdde9fa3d7b4582e43c3c03db", + "sha256:621d365f234e4c089596f3a2510f1ade07026efc28caca426161d8f458786cab" ], "index": "pypi", - "version": "==5.4.0" + "version": "==5.4.1" }, "locust": { "hashes": [ - "sha256:1f056173aefa6ba42501c3bf04bb700df9eddd165e38bb721f7b00643b68b825", - "sha256:566d89b5c4a7b69e3ab6844c1a373909918ee9d04f5d2bab6a8104e43a5721d8" + "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b", + "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c" ], "index": "pypi", - "version": "==2.31.4" + "version": "==2.31.6" }, "markdown": { "hashes": [ @@ -1353,73 +1344,81 @@ }, "more-itertools": { "hashes": [ - "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", - "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923" + "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6" ], "index": "pypi", - "version": "==10.4.0" + "version": "==10.5.0" }, "msgpack": { "hashes": [ - "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", - "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", - "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", - "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", - "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", - "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", - "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", - "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", - "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", - "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", - "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", - "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", - "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", - "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", - "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", - "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", - "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", - "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", - "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", - "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", - "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", - "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", - "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", - "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", - "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", - "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", - "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", - "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", - "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", - "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", - "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", - "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", - "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", - "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", - "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", - "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", - "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", - "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", - "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", - "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", - "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", - "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", - "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", - "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", - "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", - "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", - "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", - "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", - "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", - "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", - "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", - "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", - "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", - "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", - "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", - "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" + "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", + "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", + "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", + "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", + "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", + "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", + "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", + "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", + "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", + "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", + "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", + "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", + "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", + "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", + "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", + "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", + "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", + "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", + "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", + "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", + "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", + "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", + "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", + "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", + "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", + "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", + "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", + "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", + "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", + "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", + "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", + "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", + "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", + "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", + "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", + "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", + "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", + "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", + "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", + "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", + "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", + "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", + "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", + "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", + "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", + "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", + "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", + "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", + "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", + "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", + "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", + "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", + "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", + "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", + "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", + "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", + "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", + "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", + "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", + "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", + "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", + "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", + "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", + "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788" ], "markers": "python_version >= '3.8'", - "version": "==1.0.8" + "version": "==1.1.0" }, "mypy-extensions": { "hashes": [ @@ -1571,11 +1570,11 @@ }, "platformdirs": { "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.3.6" }, "pluggy": { "hashes": [ @@ -1652,19 +1651,19 @@ }, "pyasn1": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.6.1" }, "pyasn1-modules": { "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pycparser": { "hashes": [ @@ -1676,106 +1675,106 @@ }, "pydantic": { "hashes": [ - "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a", - "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8" + "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", + "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12" ], "markers": "python_version >= '3.8'", - "version": "==2.8.2" + "version": "==2.9.2" }, "pydantic-core": { "hashes": [ - "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d", - "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f", - "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686", - "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482", - "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006", - "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83", - "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6", - "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88", - "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86", - "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a", - "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6", - "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a", - "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6", - "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6", - "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43", - "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c", - "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4", - "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e", - "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203", - "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd", - "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1", - "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24", - "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc", - "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc", - "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3", - "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598", - "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98", - "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331", - "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2", - "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a", - "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6", - "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688", - "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91", - "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa", - "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b", - "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0", - "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840", - "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c", - "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd", - "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3", - "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231", - "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1", - "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953", - "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250", - "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a", - "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2", - "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20", - "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434", - "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab", - "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703", - "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a", - "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2", - "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac", - "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611", - "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121", - "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e", - "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b", - "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09", - "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906", - "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9", - "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7", - "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b", - "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987", - "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c", - "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b", - "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e", - "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237", - "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1", - "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19", - "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b", - "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad", - "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0", - "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94", - "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312", - "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f", - "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669", - "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1", - "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe", - "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99", - "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a", - "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a", - "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52", - "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c", - "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad", - "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1", - "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a", - "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f", - "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a", - "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27" + "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", + "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", + "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", + "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", + "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", + "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", + "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", + "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", + "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", + "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", + "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", + "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", + "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", + "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", + "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", + "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", + "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368", + "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", + "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", + "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2", + "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", + "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", + "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", + "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", + "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", + "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", + "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271", + "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", + "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb", + "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13", + "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", + "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556", + "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665", + "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", + "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", + "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", + "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", + "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", + "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", + "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", + "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", + "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", + "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", + "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", + "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", + "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", + "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658", + "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", + "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", + "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", + "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", + "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", + "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", + "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", + "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", + "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", + "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", + "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad", + "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", + "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", + "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", + "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", + "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", + "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", + "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", + "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", + "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", + "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", + "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555", + "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", + "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6", + "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", + "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", + "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", + "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", + "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", + "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", + "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", + "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", + "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12", + "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", + "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", + "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", + "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", + "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", + "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", + "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", + "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", + "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607" ], "markers": "python_version >= '3.8'", - "version": "==2.20.1" + "version": "==2.23.4" }, "pydyf": { "hashes": [ @@ -1859,7 +1858,6 @@ "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.7.1" }, "pytesseract": { @@ -1872,11 +1870,11 @@ }, "pytest": { "hashes": [ - "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", - "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce" + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" ], "markers": "python_version >= '3.8'", - "version": "==8.3.2" + "version": "==8.3.3" }, "pytest-xdist": { "hashes": [ @@ -1898,7 +1896,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-fsutil": { @@ -2041,10 +2039,10 @@ }, "rich": { "hashes": [ - "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc", - "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4" + "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", + "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" ], - "version": "==13.8.0" + "version": "==13.8.1" }, "s3transfer": { "hashes": [ @@ -2064,11 +2062,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", - "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260" + "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", + "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" ], "index": "pypi", - "version": "==2.13.0" + "version": "==2.14.0" }, "service-identity": { "hashes": [ @@ -2079,11 +2077,11 @@ }, "setuptools": { "hashes": [ - "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f", - "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e" + "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", + "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" ], "markers": "python_version >= '3.8'", - "version": "==74.0.0" + "version": "==75.1.0" }, "setuptools-scm": { "hashes": [ @@ -2105,7 +2103,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -2522,11 +2520,11 @@ }, "django": { "hashes": [ - "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", - "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" + "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", + "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad" ], "index": "pypi", - "version": "==4.2.15" + "version": "==4.2.16" }, "django-debug-toolbar": { "hashes": [ @@ -2546,19 +2544,19 @@ }, "filelock": { "hashes": [ - "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", - "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], "markers": "python_version >= '3.8'", - "version": "==3.15.4" + "version": "==3.16.1" }, "identify": { "hashes": [ - "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", - "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0" + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" ], "markers": "python_version >= '3.8'", - "version": "==2.6.0" + "version": "==2.6.1" }, "invoke": { "hashes": [ @@ -2578,11 +2576,11 @@ }, "platformdirs": { "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.3.6" }, "pre-commit": { "hashes": [ @@ -2669,11 +2667,11 @@ }, "virtualenv": { "hashes": [ - "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a", - "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589" + "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6", + "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4" ], "markers": "python_version >= '3.7'", - "version": "==20.26.3" + "version": "==20.26.5" } } } diff --git a/concordia/context_processors.py b/concordia/context_processors.py index 74731351d..0899dea8d 100644 --- a/concordia/context_processors.py +++ b/concordia/context_processors.py @@ -32,3 +32,32 @@ def site_navigation(request): def maintenance_mode_frontend_available(request): value = cache.get("maintenance_mode_frontend_available", False) return {"maintenance_mode_frontend_available": value} + + +def turnstile_default_settings(request): + """ + Expose turnstile default settings to the default template context + - Cloudflare Turnstile + """ + + return { + "TURNSTILE_JS_API_URL": getattr( + settings, + "TURN_JS_API_URL", + "https://challenges.cloudflare.com/turnstile/v0/api.js", + ), + "TURNSTILE_VERIFY_URL": getattr( + settings, + "TURNSTILE_VERIFY_URL", + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + ), + "TURNSTILE_SITEKEY": getattr( + settings, "TURNSTILE_SITEKEY", "1x00000000000000000000BB" + ), + "TURNSTILE_SECRET": getattr( + settings, "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" + ), + "TURNSTILE_TIMEOUT": getattr(settings, "TURNSTILE_TIMEOUT", 5), + "TURNSTILE_DEFAULT_CONFIG": getattr(settings, "TURNSTILE_DEFAULT_CONFIG", {}), + "TURNSTILE_PROXIES": getattr(settings, "TURNSTILE_PROXIES", {}), + } diff --git a/concordia/forms.py b/concordia/forms.py index 09d470198..06d3122bc 100644 --- a/concordia/forms.py +++ b/concordia/forms.py @@ -12,6 +12,8 @@ from django_registration.forms import RegistrationForm from django_registration.signals import user_activated +from .turnstile.fields import TurnstileField + User = get_user_model() @@ -119,6 +121,10 @@ def __init__(self, *, request, **kwargs): super().__init__(**kwargs) +class TurnstileForm(forms.Form): + turnstile = TurnstileField() + + class ContactUsForm(forms.Form): referrer = forms.CharField( label="Referring Page", widget=forms.HiddenInput(), required=False diff --git a/concordia/settings_ecs.py b/concordia/settings_ecs.py index abe0b6b6c..8faf99de2 100644 --- a/concordia/settings_ecs.py +++ b/concordia/settings_ecs.py @@ -22,6 +22,11 @@ DATABASES["default"].update({"PASSWORD": postgres_secret["password"]}) + cf_turnstile_secret_json = get_secret("crowd/%s/Turnstile" % ENV_NAME) + cf_turnstile_secret = json.loads(cf_turnstile_secret_json) + TURNSTILE_SITEKEY = cf_turnstile_secret["TurnstileSiteKey"] + TURNSTILE_SECRET = cf_turnstile_secret["TurnstileSecret"] + smtp_secret_json = get_secret("concordia/SMTP") smtp_secret = json.loads(smtp_secret_json) EMAIL_HOST = smtp_secret["Hostname"] diff --git a/concordia/settings_local_test.py b/concordia/settings_local_test.py index b683d3c25..3224052dc 100644 --- a/concordia/settings_local_test.py +++ b/concordia/settings_local_test.py @@ -1,3 +1,5 @@ +import os + from .settings_template import * # NOQA ignore=F405 from .settings_template import DATABASES @@ -21,3 +23,17 @@ "CONFIG": {"hosts": [("localhost", 63791)]}, } } + +# Turnstile settings +TURNSTILE_JS_API_URL = os.environ.get( + "TURNSTILE_JS_API_URL", "https://challenges.cloudflare.com/turnstile/v0/api.js" +) +TURNSTILE_VERIFY_URL = os.environ.get( + "TURNSTILE_VERIFY_URL", "https://challenges.cloudflare.com/turnstile/v0/siteverify" +) +TURNSTILE_SITEKEY = os.environ.get( + "TURNSTILE_SITEKEY", "1x00000000000000000000BB" +) # Always pass, invisible +TURNSTILE_SECRET = os.environ.get( + "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" +) # Always pass diff --git a/concordia/settings_template.py b/concordia/settings_template.py index d5d452939..784bf0c9c 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -114,7 +114,6 @@ "concordia.apps.ConcordiaAppConfig", "exporter", "importer", - "captcha", "prometheus_metrics.apps.PrometheusMetricsConfig", "robots", "django_celery_beat", @@ -161,6 +160,7 @@ "concordia.context_processors.system_configuration", "concordia.context_processors.site_navigation", "concordia.context_processors.maintenance_mode_frontend_available", + "concordia.context_processors.turnstile_default_settings", ], "libraries": { "staticfiles": "django.templatetags.static", @@ -317,12 +317,14 @@ "concordia.authentication_backends.EmailOrUsernameModelBackend" ] -CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" -#: Anonymous sessions require captcha validation every day by default: -ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL = 86400 - -CAPTCHA_IMAGE_SIZE = [150, 100] -CAPTCHA_FONT_SIZE = 40 +# Turnstile settings +TURNSTILE_JS_API_URL = os.environ.get("TURNSTILE_JS_API_URL", "") +TURNSTILE_VERIFY_URL = os.environ.get("TURNSTILE_VERIFY_URL", "") +TURNSTILE_SITEKEY = os.environ.get("TURNSTILE_SITEKEY", "") +TURNSTILE_SECRET = os.environ.get("TURNSTILE_SECRET", "") +TURNSTILE_TIMEOUT = os.environ.get("TURNSTILE_TIMEOUT", 5) +TURNSTILE_DEFAULT_CONFIG = os.environ.get("TURNSTILE_DEFAULT_CONFIG", {}) +TURNSTILE_PROXIES = os.environ.get("TURNSTILE_PROXIES", {}) STORAGES = { "default": { diff --git a/concordia/settings_test.py b/concordia/settings_test.py index b49eed049..56876b8e9 100644 --- a/concordia/settings_test.py +++ b/concordia/settings_test.py @@ -1,3 +1,5 @@ +import os + from .settings_template import * # NOQA ignore=F405 from .settings_template import DATABASES @@ -21,3 +23,17 @@ "CONFIG": {"hosts": [("localhost", 6379)]}, } } + +# Turnstile settings +TURNSTILE_JS_API_URL = os.environ.get( + "TURNSTILE_JS_API_URL", "https://challenges.cloudflare.com/turnstile/v0/api.js" +) +TURNSTILE_VERIFY_URL = os.environ.get( + "TURNSTILE_VERIFY_URL", "https://challenges.cloudflare.com/turnstile/v0/siteverify" +) +TURNSTILE_SITEKEY = os.environ.get( + "TURNSTILE_SITEKEY", "1x00000000000000000000BB" +) # Always pass, invisible +TURNSTILE_SECRET = os.environ.get( + "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" +) # Always pass diff --git a/concordia/static/js/src/contribute.js b/concordia/static/js/src/contribute.js index 355419f4b..d914638f3 100644 --- a/concordia/static/js/src/contribute.js +++ b/concordia/static/js/src/contribute.js @@ -62,40 +62,6 @@ $(document).on('keydown', function (event) { }); function setupPage() { - var $captchaModal = $('#captcha-modal'); - var $triggeringCaptchaForm = false; - var $captchaForm = $captchaModal - .find('form') - .on('submit', function (event) { - event.preventDefault(); - - var formData = $captchaForm.serializeArray(); - - $.ajax({ - url: $captchaForm.attr('action'), - method: 'POST', - dataType: 'json', - data: $.param(formData), - }) - .done(function () { - $captchaModal.modal('hide'); - if ($triggeringCaptchaForm) { - $triggeringCaptchaForm.submit(); - } - $triggeringCaptchaForm = false; - }) - .fail(function (jqXHR) { - if (jqXHR.status == 401) { - $captchaModal - .find('[name=key]') - .val(jqXHR.responseJSON.key); - $captchaModal - .find('#captcha-image') - .attr('src', jqXHR.responseJSON.image); - } - }); - }); - $('form.ajax-submission').each(function (index, formElement) { /* Generic AJAX submission logic which takes a form and POSTs its data to the @@ -149,27 +115,16 @@ function setupPage() { } }) .fail(function (jqXHR, textStatus, errorThrown) { - if (jqXHR.status == 401) { - $captchaModal - .find('[name=key]') - .val(jqXHR.responseJSON.key); - $captchaModal - .find('#captcha-image') - .attr('src', jqXHR.responseJSON.image); - $triggeringCaptchaForm = $form; - $captchaModal.modal(); - } else { - $form.trigger('form-submit-failure', { - textStatus: textStatus, - errorThrown: errorThrown, - requestData: formData, - $form: $form, - jqXHR: jqXHR, - }); - unlockControls($form); - if (eventData.lockElement) { - unlockControls($(eventData.lockElement)); - } + $form.trigger('form-submit-failure', { + textStatus: textStatus, + errorThrown: errorThrown, + requestData: formData, + $form: $form, + jqXHR: jqXHR, + }); + unlockControls($form); + if (eventData.lockElement) { + unlockControls($(eventData.lockElement)); } }); @@ -518,6 +473,11 @@ function setupPage() { url: url, method: 'POST', dataType: 'json', + data: { + 'cf-turnstile-response': $transcriptionEditor + .find('input[name="cf-turnstile-response"]') + .val(), + }, }) .done(function (responseData) { displayMessage( diff --git a/concordia/templates/forms/widgets/turnstile_widget.html b/concordia/templates/forms/widgets/turnstile_widget.html new file mode 100644 index 000000000..1dcd92df6 --- /dev/null +++ b/concordia/templates/forms/widgets/turnstile_widget.html @@ -0,0 +1 @@ +
diff --git a/concordia/templates/registration/login.html b/concordia/templates/registration/login.html index 26af4419d..19e493958 100644 --- a/concordia/templates/registration/login.html +++ b/concordia/templates/registration/login.html @@ -5,6 +5,7 @@ {% block head_content %} {{ block.super }} + {% endblock head_content %} {% block title %}Login{% endblock title %} @@ -23,6 +24,7 @@

Welcome back!

{% endif %} {% bootstrap_form form %} +
{{ turnstile_form.turnstile }}

By using this system, you agree to comply with the Library's diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index d832a69da..86cb202f9 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -41,6 +41,7 @@ + @@ -115,9 +116,6 @@

- diff --git a/concordia/templates/transcriptions/asset_detail/captcha_modal.html b/concordia/templates/transcriptions/asset_detail/captcha_modal.html deleted file mode 100644 index 2ca5c457d..000000000 --- a/concordia/templates/transcriptions/asset_detail/captcha_modal.html +++ /dev/null @@ -1,38 +0,0 @@ - diff --git a/concordia/templates/transcriptions/asset_detail/editor.html b/concordia/templates/transcriptions/asset_detail/editor.html index de98041c7..b9e0baa40 100644 --- a/concordia/templates/transcriptions/asset_detail/editor.html +++ b/concordia/templates/transcriptions/asset_detail/editor.html @@ -86,6 +86,8 @@

+
{{ turnstile_form.turnstile }}
+ diff --git a/concordia/tests/test_views.py b/concordia/tests/test_views.py index ea912bef1..8636807a9 100644 --- a/concordia/tests/test_views.py +++ b/concordia/tests/test_views.py @@ -1,9 +1,10 @@ from datetime import date, timedelta from unittest.mock import patch -from captcha.models import CaptchaStore +from django import forms from django.conf import settings from django.contrib.auth.models import User +from django.core.cache import cache from django.http import HttpResponse, JsonResponse from django.test import ( Client, @@ -31,7 +32,6 @@ from concordia.views import ( AccountProfileView, CompletedCampaignListView, - ConcordiaLoginView, ratelimit_view, registration_rate, ) @@ -113,6 +113,12 @@ class ConcordiaViewTests(CreateTestUsers, JSONAssertMixin, TestCase): This class contains the unit tests for the view in the concordia app. """ + def setUp(self): + cache.clear() + + def tearDown(self): + cache.clear() + def test_ratelimit_view(self): c = Client() response = c.get("/error/429/") @@ -477,27 +483,6 @@ def test_campaign_report(self): RATELIMIT_ENABLE=False, SESSION_ENGINE="django.contrib.sessions.backends.cache" ) class TransactionalViewTests(CreateTestUsers, JSONAssertMixin, TransactionTestCase): - def completeCaptcha(self, key=None): - """Submit a CAPTCHA response using the provided challenge key""" - - if key is None: - challenge_data = self.assertValidJSON( - self.client.get(reverse("ajax-captcha")), expected_status=401 - ) - self.assertIn("key", challenge_data) - self.assertIn("image", challenge_data) - key = challenge_data["key"] - - self.assertValidJSON( - self.client.post( - reverse("ajax-captcha"), - data={ - "key": key, - "response": CaptchaStore.objects.get(hashkey=key).response, - }, - ) - ) - def test_asset_reservation(self): """ Test the basic Asset reservation process @@ -749,74 +734,67 @@ def test_asset_reservation_tombstone_expiration(self): self.assertEqual(reservation.reservation_token, data["reservation_token"]) self.assertEqual(reservation.tombstoned, False) - def test_anonymous_transcription_save_captcha(self): - asset = create_asset() - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=401) - self.assertIn("key", data) - self.assertIn("image", data) - - self.completeCaptcha(data["key"]) - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=201) - def test_transcription_save(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=401) + self.assertIn("error", data) - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) - # Test attempts to create a second transcription without marking that it - # supersedes the previous one: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=409) - self.assertIn("error", data) + # Test attempts to create a second transcription without marking that it + # supersedes the previous one: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=409) + self.assertIn("error", data) - # This should work with the chain specified: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={"text": "test", "supersedes": asset.transcription_set.get().pk}, - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + # This should work with the chain specified: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={"text": "test", "supersedes": asset.transcription_set.get().pk}, + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) - # We should see an error if you attempt to supersede a transcription - # which has already been superseded: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={ - "text": "test", - "supersedes": asset.transcription_set.order_by("pk").first().pk, - }, - ) - data = self.assertValidJSON(resp, expected_status=409) - self.assertIn("error", data) + # We should see an error if you attempt to supersede a transcription + # which has already been superseded: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={ + "text": "test", + "supersedes": asset.transcription_set.order_by("pk").first().pk, + }, + ) + data = self.assertValidJSON(resp, expected_status=409) + self.assertIn("error", data) - # A logged in user can take over from an anonymous user: - self.login_user() - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={ - "text": "test", - "supersedes": asset.transcription_set.order_by("pk").last().pk, - }, - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + # A logged in user can take over from an anonymous user: + self.login_user() + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={ + "text": "test", + "supersedes": asset.transcription_set.order_by("pk").last().pk, + }, + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) def test_anonymous_transcription_submission(self): asset = create_asset() @@ -826,36 +804,48 @@ def test_anonymous_transcription_submission(self): transcription.full_clean() transcription.save() - resp = self.client.post( - reverse("submit-transcription", args=(transcription.pk,)) - ) + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + resp = self.client.post( + reverse("submit-transcription", args=(transcription.pk,)) + ) data = self.assertValidJSON(resp, expected_status=401) - self.assertIn("key", data) - self.assertIn("image", data) + self.assertIn("error", data) self.assertFalse(Transcription.objects.filter(submitted__isnull=False).exists()) - self.completeCaptcha(data["key"]) - self.client.post(reverse("submit-transcription", args=(transcription.pk,))) - self.assertTrue(Transcription.objects.filter(submitted__isnull=False).exists()) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + self.client.post( + reverse("submit-transcription", args=(transcription.pk,)), + ) + self.assertTrue( + Transcription.objects.filter(submitted__isnull=False).exists() + ) def test_transcription_submission(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) data = self.assertValidJSON(resp, expected_status=201) transcription = Transcription.objects.get() self.assertIsNone(transcription.submitted) - resp = self.client.post( - reverse("submit-transcription", args=(transcription.pk,)) - ) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("submit-transcription", args=(transcription.pk,)) + ) data = self.assertValidJSON(resp, expected_status=200) self.assertIn("id", data) self.assertEqual(data["id"], transcription.pk) @@ -866,9 +856,6 @@ def test_transcription_submission(self): def test_stale_transcription_submission(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() - anon = get_anonymous_user() t1 = Transcription(asset=asset, user=anon, text="test") @@ -879,9 +866,12 @@ def test_stale_transcription_submission(self): t2.full_clean() t2.save() - resp = self.client.post(reverse("submit-transcription", args=(t1.pk,))) - data = self.assertValidJSON(resp, expected_status=400) - self.assertIn("error", data) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post(reverse("submit-transcription", args=(t1.pk,))) + data = self.assertValidJSON(resp, expected_status=400) + self.assertIn("error", data) def test_transcription_review(self): asset = create_asset() @@ -1538,18 +1528,34 @@ def test_ratelimit_view(self): self.assertNotEqual(response["Retry-After"], 0) -class CaptchaTests(TestCase): +class LoginTests(TestCase, CreateTestUsers): def setUp(self): - self.request_factory = RequestFactory() + self.user = self.create_user("test-user") def test_ConcordiaLoginView(self): - request = self.request_factory.post("/") - request.session = {} - view = setup_view(ConcordiaLoginView(), request) - response = view.post(request) - self.assertNotContains(response, "captcha") - - request.limited = True - view = setup_view(ConcordiaLoginView(), request) - response = view.post(request) - self.assertContains(response, "captcha") + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + response = self.client.post( + reverse("registration_login"), + data={"username": self.user.username, "password": self.user._password}, + ) + self.assertIn("user", response.context) + self.assertFalse(response.context["user"].is_authenticated) + + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + response = self.client.post( + reverse("registration_login"), + data={"username": self.user.username, "password": self.user._password}, + follow=True, + ) + self.assertRedirects( + response, + expected_url=reverse("homepage"), + target_status_code=200, + ) + self.assertIn("user", response.context) + self.assertTrue(response.context["user"].is_authenticated) diff --git a/concordia/turnstile/LICENSE b/concordia/turnstile/LICENSE new file mode 100644 index 000000000..ed371f7ee --- /dev/null +++ b/concordia/turnstile/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Zhang Minghan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/concordia/turnstile/__init__.py b/concordia/turnstile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/concordia/turnstile/context_processor.py b/concordia/turnstile/context_processor.py new file mode 100644 index 000000000..ae091a422 --- /dev/null +++ b/concordia/turnstile/context_processor.py @@ -0,0 +1,5 @@ +from django.conf import settings + + +def turnstile_settings(request): + return {"TURN_JS_API_URL": settings.TURN_JS_API_URL} diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py new file mode 100644 index 000000000..24eb5201c --- /dev/null +++ b/concordia/turnstile/fields.py @@ -0,0 +1,76 @@ +# Originally from +# https://github.com/zmh-program/django-turnstile/blob/main/turnstile/fields.py + +import inspect +import json +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import ProxyHandler, Request, build_opener + +from django import forms +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + +from ..turnstile.widgets import TurnstileWidget + + +class TurnstileField(forms.Field): + widget = TurnstileWidget + default_error_messages = { + "error_turnstile": _("Turnstile could not be verified."), + "invalid_turnstile": _("Turnstile could not be verified."), + "required": _("Please prove you are a human."), + } + + def __init__(self, **kwargs): + superclass_parameters = inspect.signature(super().__init__).parameters + superclass_kwargs = {} + widget_settings = settings.TURNSTILE_DEFAULT_CONFIG.copy() + for key, value in kwargs.items(): + if key in superclass_parameters: + superclass_kwargs[key] = value + else: + widget_settings[key] = value + + widget_url_settings = {} + for prop in filter(lambda p: p in widget_settings, ("onload", "render", "hl")): + widget_url_settings[prop] = widget_settings[prop] + del widget_settings[prop] + self.widget_settings = widget_settings + + super().__init__(**superclass_kwargs) + + self.widget.extra_url = widget_url_settings + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + for key, value in self.widget_settings.items(): + attrs["data-%s" % key] = value + return attrs + + def validate(self, value): + super().validate(value) + + opener = build_opener(ProxyHandler(settings.TURNSTILE_PROXIES)) + post_data = urlencode( + { + "secret": settings.TURNSTILE_SECRET, + "response": value, + } + ).encode() + + request = Request(settings.TURNSTILE_VERIFY_URL, post_data) + + try: + response = opener.open(request, timeout=settings.TURNSTILE_TIMEOUT) + except HTTPError as exc: + raise forms.ValidationError( + self.error_messages["error_turnstile"], code="error_turnstile" + ) from exc + + response_data = json.loads(response.read().decode("utf-8")) + + if not response_data.get("success"): + raise forms.ValidationError( + self.error_messages["invalid_turnstile"], code="invalid_turnstile" + ) diff --git a/concordia/turnstile/widgets.py b/concordia/turnstile/widgets.py new file mode 100644 index 000000000..80ddfaa2d --- /dev/null +++ b/concordia/turnstile/widgets.py @@ -0,0 +1,30 @@ +# Originally from +# https://github.com/zmh-program/django-turnstile/blob/main/turnstile/fields.py + +from urllib.parse import urlencode + +from django import forms +from django.conf import settings + + +class TurnstileWidget(forms.Widget): + template_name = "forms/widgets/turnstile_widget.html" + + def __init__(self, *args, **kwargs): + self.extra_url = {} + super().__init__(*args, **kwargs) + + def value_from_datadict(self, data, files, name): + return data.get("cf-turnstile-response") + + def build_attrs(self, base_attrs, extra_attrs=None): + attrs = super().build_attrs(base_attrs, extra_attrs) + attrs["data-sitekey"] = settings.TURNSTILE_SITEKEY + return attrs + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context["api_url"] = settings.TURNSTILE_JS_API_URL + if self.extra_url: + context["api_url"] += "?" + urlencode(self.extra_url) + return context diff --git a/concordia/urls.py b/concordia/urls.py index 10f746a52..941c8a8b7 100644 --- a/concordia/urls.py +++ b/concordia/urls.py @@ -263,8 +263,6 @@ ".well-known/change-password", # https://wicg.github.io/change-password-url/ RedirectView.as_view(pattern_name="password_change"), ), - path("captcha/ajax/", views.ajax_captcha, name="ajax-captcha"), - path("captcha/", include("captcha.urls")), path("admin/", admin.site.urls), # Internal support assists: path("error/500/", server_error), diff --git a/concordia/views.py b/concordia/views.py index 7287299b6..0f1965de5 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -11,9 +11,6 @@ from urllib.parse import urlencode import markdown -from captcha.fields import CaptchaField -from captcha.helpers import captcha_image_url -from captcha.models import CaptchaStore from django.conf import settings from django.contrib import messages from django.contrib.auth import logout @@ -70,6 +67,7 @@ ActivateAndSetPasswordForm, AllowInactivePasswordResetForm, ContactUsForm, + TurnstileForm, UserLoginForm, UserNameForm, UserProfileForm, @@ -339,33 +337,31 @@ class ConcordiaRegistrationView(RegistrationView): @method_decorator(never_cache, name="dispatch") -@method_decorator( - ratelimit( - group="login", key="post:username", rate="3/15m", method="POST", block=False - ), - name="post", -) class ConcordiaLoginView(LoginView): form_class = UserLoginForm def post(self, request, *args, **kwargs): form = self.get_form() - - # This is set by the ratelimit decorator - # True if the request exceeds the rate limit - blocked = request.limited - recent_captcha = ( - time() - request.session.get("captcha_validation_time", 0) - ) < 86400 - - if blocked and not recent_captcha: - form.fields["captcha"] = CaptchaField() - if form.is_valid(): - return self.form_valid(form) + turnstile_form = TurnstileForm(request.POST) + if turnstile_form.is_valid(): + return self.form_valid(form) + else: + form.add_error( + None, "Unable to validate. Please login or complete the challenge." + ) + return self.form_invalid(form) + else: return self.form_invalid(form) + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["turnstile_form"] = TurnstileForm(auto_id=False) + + return ctx + def ratelimit_view(request, exception=None): status_code = 429 @@ -1512,47 +1508,25 @@ def get_context_data(self, **kwargs): ctx["undo_available"] = asset.can_rollback()[0] if transcription else False ctx["redo_available"] = asset.can_rollforward()[0] if transcription else False - return ctx - + ctx["turnstile_form"] = TurnstileForm(auto_id=False) -@never_cache -def ajax_captcha(request): - if request.method == "POST": - response = request.POST.get("response") - key = request.POST.get("key") - - if response and key: - CaptchaStore.remove_expired() - - # Note that CaptchaStore displays the response in uppercase in the - # image and in the string representation of the object but the - # actual value stored in the database is lowercase! - deleted, _ = CaptchaStore.objects.filter( - response=response.lower(), hashkey=key - ).delete() - - if deleted > 0: - request.session["captcha_validation_time"] = time() - return JsonResponse({"valid": True}) - - key = CaptchaStore.generate_key() - return JsonResponse( - {"key": key, "image": request.build_absolute_uri(captcha_image_url(key))}, - status=401, - content_type="application/json", - ) + return ctx -def validate_anonymous_captcha(view): +def validate_anonymous_user(view): @wraps(view) @never_cache def inner(request, *args, **kwargs): - if not request.user.is_authenticated: - captcha_last_validated = request.session.get("captcha_validation_time", 0) - age = time() - captcha_last_validated - if age > settings.ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL: - return ajax_captcha(request) - + if not request.user.is_authenticated and request.method == "POST": + form = TurnstileForm(request.POST) + if not form.is_valid(): + return JsonResponse( + { + "error": "Unable to validate. " + "Please login or complete the challenge." + }, + status=401, + ) return view(request, *args, **kwargs) return inner @@ -1580,7 +1554,7 @@ def get_transcription_superseded(asset, supersedes_pk): @require_POST -@validate_anonymous_captcha +@login_required @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def generate_ocr_transcription(request, *, asset_pk): @@ -1627,7 +1601,7 @@ def generate_ocr_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def rollback_transcription(request, *, asset_pk): @@ -1666,7 +1640,7 @@ def rollback_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def rollforward_transcription(request, *, asset_pk): @@ -1703,7 +1677,7 @@ def rollforward_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic def save_transcription(request, *, asset_pk): asset = get_object_or_404(Asset, pk=asset_pk) @@ -1767,7 +1741,7 @@ def save_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user def submit_transcription(request, *, pk): transcription = get_object_or_404(Transcription, pk=pk) asset = transcription.asset diff --git a/docs/for-developers.md b/docs/for-developers.md index 2b6f438ee..1edbd2d27 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -77,6 +77,16 @@ virtualenv environment: 1. Make sure that [redis](https://redis.io/docs/getting-started/) is installed and running. +1. Configure Turnstile in your `.env` file. Unless specifically testing Turnstile, + you'll probably want the following settings: + + ```bash + echo TURNSTILE_SITEKEY=1x00000000000000000000BB >> .env + echo TURNSTILE_SECRET=1x0000000000000000000000000000000AA >> .env + ``` + + Those two settings ensure all Turnstile tests pass. See [Turnstile Testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) for other options. + ### Local Development Environment You will likely want to run the Django development server on your localhost